fix(workflows): centralize path constants and add init-options.json fallback for auto-detect - #2
Merged
markuswondrak merged 23 commits intoMay 3, 2026
Conversation
* fix: migrate extension commands on integration switch When switching integrations (e.g. kimi → opencode), extension commands were not re-registered for the new agent, leaving the new agent without extension support and orphaning files in the old agent's directory. Changes: - Add ExtensionManager.unregister_agent_artifacts() to clean up old agent extension files and registry entries during switch - Add ExtensionManager.register_enabled_extensions_for_agent() to re-register all enabled extensions for the new agent - Wire both into integration_switch() after uninstall/install phases - Handle skills mode (Copilot --skills) correctly - Add tests for kimi→opencode→claude migration, Copilot skills mode, and disabled extension handling Fixes extension commands not appearing after integration switch. * Update src/specify_cli/extensions.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…low (github#2412) * feat(extensions): add Spec2Cloud extension for Azure deployment workflow Co-authored-by: Copilot <copilot@github.com> * Update extensions/catalog.community.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update extensions/catalog.community.json Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * feat(extensions): update Spec2Cloud extension details and remove duplicate entry Co-authored-by: Copilot <copilot@github.com> * fix(extensions): correct formatting of updated_at timestamp * fix(extensions): update Spec2Cloud extension version and timestamps --------- Co-authored-by: Copilot <copilot@github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Your Name <your@email.example>
Co-authored-by: Your Name <your@email.example>
Co-authored-by: Your Name <your@email.example>
Co-authored-by: Your Name <your@email.example>
Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 23.0.0 to 23.1.0. - [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases) - [Commits](DavidAnson/markdownlint-cli2-action@ce4853d...6b51ade) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-version: 23.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore: bump version to 0.8.4 * chore: begin 0.8.5.dev0 development --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
… auto-detect integration Agent-Logs-Url: https://github.com/markuswondrak/spec-kit/sessions/54bfb375-ba13-4531-b9b0-4140ea6b0edc Co-authored-by: markuswondrak <245696895+markuswondrak@users.noreply.github.com>
…ub#2389) * support controlled multi-install integrations * fix: harden multi-install integration state * refactor: isolate integration runtime helpers * fix: address copilot review feedback * fix: address follow-up copilot feedback * fix: tighten integration switch semantics * fix: address final copilot review feedback * fix: harden integration manifest read errors * fix: refuse symlinked shared infra paths * test: filter expected self-test preset warning * test: address copilot review nits * refactor: centralize safe shared infra writes * fix: use no-follow writes for shared infra * fix: keep default integration atomic on template refresh * fix: harden shared infra error paths * fix: preflight shared infra and future state schemas * fix: support nested shared scripts during preflight * test: tolerate wrapped schema error output * fix: use safe default mode for shared text writes * fix: use posix paths in shared skip output * fix: share project guard for integration use * fix: centralize spec-kit project guards * fix: use posix project paths in cli output * fix: harden shared manifest and upgrade refresh
Copilot created this pull request from a session on behalf of
markuswondrak
May 1, 2026 16:55
View session
…github#2429) * chore: update DyanGalih extensions to latest versions * chore: update catalog root timestamp to current
markuswondrak
approved these changes
May 1, 2026
markuswondrak
changed the base branch from
main
to
fix/workflow-integration-auto-detect
May 1, 2026 18:02
…2165) (github#2432) Add a non-blocking Panel notice during `specify init` when the git extension auto-enables, informing users that starting in v0.10.0 this will require explicit opt-in via `specify extension add git`. - src/specify_cli/__init__.py: track successful git extension install and display yellow "Notice: Git Default Changing" panel - tests/integrations/test_cli.py: integration test validating notice content (v0.10.0 timeline, opt-in messaging, migration command) - docs/reference/core.md: user-facing NOTE about the upcoming change Closes github#2165
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extension ID: token-analyzer Version: 0.1.0 Author: Chris Roberts | coderandhiker Description: Captures, analyzes, and compares token consumption across SDD workflows Repository: https://github.com/coderandhiker/spec-kit-token-analyzer Co-authored-by: Chris Roberts <chris@Chriss-MacBook-Pro.local>
Use SPECIFY_DIR-derived constants for integration.json and init-options.json. Co-authored-by: Cursor <cursoragent@cursor.com>
Sync fork branch with latest changes from the original repository. Co-authored-by: Cursor <cursoragent@cursor.com>
…b#2292) * fix: honor template overrides for tasks-template (github#2278) - Add scripts/bash/setup-tasks.sh mirroring setup-plan.sh pattern - Add scripts/powershell/setup-tasks.ps1 mirroring setup-plan.ps1 pattern - Update tasks.md frontmatter to use dedicated setup-tasks scripts - Resolve tasks template via override stack and emit path as TASKS_TEMPLATE in JSON output - Reference resolved TASKS_TEMPLATE path in generate step instead of hardcoded path * fix: remove stray EOF tokens from setup-tasks scripts * fix: improve error messages for unresolved tasks-template * test: update file inventory tests to include setup-tasks scripts * fix: use Console::Error.WriteLine instead of Write-Error in setup-tasks.ps1 * fix: write prerequisite error messages to stderr in setup-tasks.ps1 * fix: validate tasks template is a file and normalize path in setup-tasks.ps1 * fix: improve tasks-template error message to mention full override stack * test: add setup-tasks.sh to TestCopilotSkillsMode file inventory * fix: skip feature-branch validation when feature.json pins FEATURE_DIR * fix: correct override path in tasks-template error messages * test: add integration tests for setup-tasks template resolution and branch validation * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * fix: correct fixture paths and add spec.md prerequisite checks * fix: use correct .registry schema in preset priority test * fix: remove stale aaa-preset block and duplicate comment in preset priority test * fix: align preset directory names with registry IDs in priority test --------- Co-authored-by: Nimraakram22 <nimra.akram123451@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
markuswondrak
marked this pull request as ready for review
May 3, 2026 06:49
added 2 commits
May 3, 2026 08:57
markuswondrak
merged commit May 3, 2026
1b1031a
into
fix/workflow-integration-auto-detect
8 checks passed
markuswondrak
pushed a commit
that referenced
this pull request
Jul 17, 2026
* feat(workflows): expose workflow source directory to steps (github#3467) Propagate WorkflowDefinition.source_path to steps via {{ context.workflow_dir }} in template expressions and SPECKIT_WORKFLOW_DIR env var for shell steps. The original source directory is persisted in state.json so resume restores the correct value instead of the run-directory copy path. Closes github#3467 Assisted-By: 🤖 Claude Code * fix: apply bot review suggestions (#2) Applied fixes from bot review comments: - Comment #3563319058: prevent stale SPECKIT_WORKFLOW_DIR leak from parent env - Comment #3563319094: use cross-platform Python one-liner instead of printenv - Comment #3563319103: add monkeypatch.delenv for deterministic env var test - Comment #3563319116: same env leak fix as #3563319058 Assisted-By: 🤖 Claude Code * fix: use YAML single-quotes and forward-slash paths for Windows CI (#2) sys.executable on Windows returns backslash paths (D:\a\...) which YAML double-quoted strings interpret as escape sequences. Switch to single-quoted YAML strings and normalize paths with replace("\\", "/"). Assisted-By: 🤖 Claude Code * fix: resolve workflow_dir to absolute path and add installed-by-ID test (github#3469) Applied fixes from bot review comments: - Comment #3563382853: resolve source_path before taking parent to ensure absolute paths - Comment #3563382864: add test for installed-by-ID workflow_dir semantics Assisted-By: 🤖 Claude Code * docs: document context.workflow_dir and SPECKIT_WORKFLOW_DIR Add reference documentation for the new workflow_dir runtime value in both workflows/README.md and docs/reference/workflows.md so workflow authors can discover the feature and its semantics. Assisted-By: 🤖 Claude Code * fix: clarify installed workflow_dir is an absolute path (github#3469) The documentation for context.workflow_dir described the installed-by-ID case as ".specify/workflows/<id>/" which appears relative, contradicting the "resolved absolute path" semantics. Clarified that it is the absolute path to the installation directory. Assisted-By: 🤖 Claude Code * fix: apply bot review suggestions (github#3469) Applied fixes from bot review comments: - Comment #3580005128: Quote sys.executable in shell step env var test - Comment #3580005174: Quote sys.executable in no-env-var test Assisted-By: 🤖 Claude Code * fix: apply bot review suggestions (github#3469) Applied fixes from bot review comments: - Comment #3587146944: Quote interpolated workflow_dir path in example Assisted-By: 🤖 Claude Code
markuswondrak
pushed a commit
that referenced
this pull request
Jul 30, 2026
…#3704) * feat: first-class agent-native runtime hooks for integrations * refactor: rework integration events per maintainer review - Rename hooks terminology to 'events' (events:, --events flag, events.py). - Use snake_case names for canonical events consistent with spec-kit vocabulary. - Fold event config adapters into integration classes via class attributes (CANONICAL_TO_NATIVE, events_config_file, events_format). - Lift event command-script resolution to core 'specify event run' command. - Split events sourcing from integration config writing. - Support first-class Copilot CLI events JSON generation under '.github/hooks/speckit.json'. - Rewrite and expand full test suite under 'tests/integrations/test_events.py'. Assisted-by: opencode (model: litellm/gemini-3.5-flash, autonomous) * fix(events): resolve ruff lint errors blocking CI Address Copilot review finding github#18 (src/specify_cli/__init__.py event-command import missing # noqa: E402), github#19 (unused console import in commands/event.py), and github#20 (unused patch/yaml/Path/integration imports in test_events.py). Also fix two stray F541 f-string prefixes in _build_opencode_plugin that ruff flagged in the same job. Bump dev version 0.14.2.dev0 -> 0.14.2.dev1 and add a CHANGELOG entry per the AGENTS.md convention for Specify CLI __init__.py changes. Refs: PR github#3704 Copilot inline review (findings github#18, github#19, github#20) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): make generated native hooks actually execute Address Copilot review findings that left generated event hooks inert or schema-invalid after the rework: - #2: the resolved events map now carries an ordered list of handlers per event (dict[str, list[dict]]) so two extensions declaring the same event both run instead of the last one silently winning. collect_extension_events accumulates; every adapter emits one native entry per handler. - github#6: Claude/Gemini/Qwen/Devin/Tabnine native schema accepts a single 'command' string, not command+args. Each adapter now renders one complete shell invocation of the dispatcher via _dispatcher_command(). - github#7: Gemini measures hook timeouts in milliseconds; add events_timeout_unit attr and _native_timeout() so the 60s default becomes 60000ms instead of terminating the dispatcher after 60ms. - github#4: _resolve_event_command_argv() replaces _extract_script_path() — scripts: values are command strings (e.g. 'scripts/bash/setup-plan.sh --json'), not bare paths. Resolves the project's sh/ps/py variant, splits safely into argv, and prepends the interpreter for .py. - github#5: bundled-template fallback now uses _locate_core_pack()/_repo_root() (core_pack/commands, not the non-existent core_pack/templates/commands). - github#16: all formatters use IntegrationBase.resolve_python_interpreter() so generated commands honor the project venv and never hard-code python3 (absent on Windows). The opencode TS plugin bakes in the same resolved interpreter. - github#13: opencode TS plugin runEvent() now throws on failure instead of process.exit(2), which killed the OpenCode host process; only the failing hook is rejected. - github#21: user YAML override is validated (event names, non-empty command strings) before returning; a malformed override is warned about and ignored rather than crashing installation on cfg.get(). Bump dev version 0.14.2.dev1 -> 0.14.2.dev2 (gemini/__init__.py change) and add a CHANGELOG entry. Refs: PR github#3704 Copilot inline review (findings #2, github#4, github#5, github#6, github#7, github#13, github#16, github#21) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): merge/teardown idempotency and data safety Address Copilot review findings on native-config merge and teardown: - github#9: _has_marker now recurses into nested 'hooks' arrays so a matcher-group containing Specify-owned inner hooks is recognized and replaced on upgrade instead of accumulating duplicates. - github#11: _merge_json_fragment strips ALL Specify-marked entries from every event before adding the new set, so an override that drops an event (pre_tool_use -> stop) removes the stale marked entry instead of leaving it active. - #3: an empty resolved map (--events false / disabled override) now runs the native-config removal path instead of early-returning, so prior Specify hooks are stripped. The shared dispatcher is left untouched (github#10). - github#14: teardown deletes a Spec-Kit-created config that is now empty of user content (rather than leaving '{}' that confused manifest.uninstall()), while preserving pre-existing configs with user hooks/settings. - github#10: the shared .specify/events.py dispatcher is deleted only when no other installed event-capable integration's manifest still references it, so uninstalling one multi-install integration doesn't break the others. - github#8: Copilot's .github/hooks/speckit.json now merges owned entries (with markers) into a pre-existing file instead of overwriting, and teardown removes only owned entries (deleting the file when no user hooks remain). - github#22/github#23: JSON/JSONC parse failures in native configs (Claude/Cursor/etc. and opencode.json) abort the merge with a warning instead of resetting user content to '{}'. - github#12: write destinations are validated (symlinked-ancestor rejection + containment) before any bytes are written, so a symlinked .specify or native config directory can't redirect writes outside the repository. Refs: PR github#3704 Copilot inline review (findings #3, github#8, github#9, github#10, github#11, github#12, github#14, github#22, github#23) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): honor enabled flag, refresh on extension lifecycle, strict command validation Address Copilot review findings on sourcing, validation, and lifecycle: - #1: collect_extension_events now honors the extension registry's 'enabled' flag — a disabled extension's events are skipped so disabling an extension actually deactivates its runtime hooks. Adds refresh_integration_events(), wired into extension add/remove/enable/disable, so installing, removing, enabling, or disabling an extension regenerates each installed event-capable integration's native event config (the documented install-after-init flow is no longer inert, and disabled/removed extension events are stripped). - github#17: validate_events now requires 'command' to be a non-empty string, not merely truthy, so a value like 'command: [foo]' is rejected at manifest load instead of rendering into invalid native configuration. - github#15: updated PR github#3704 description to the implemented events terminology (.specify/events.py, events:, --events, integration-events.yml) replacing the stale bridge.py / runtime_hooks: / --hooks false / integration-hooks.yml references that no longer match the shipped API. (github#21 — user YAML override validation — was addressed in the prior tier.) Refs: PR github#3704 Copilot inline review (findings #1, github#15, github#17) Assisted-by: opencode (model: glm-5.2, autonomous) * revert: drop CHANGELOG.md/pyproject.toml version bumps from events fixes Per maintainer request, the events PR no longer carries CHANGELOG entries or pyproject version revs. This restores both files to their pre-PR (da6c20d) state: pyproject.toml back to 0.14.2.dev0 and the [Unreleased] block removed from CHANGELOG.md. The AGENTS.md version-rev convention for __init__.py changes is intentionally waived for this PR by maintainer decision. This also clears the pending merge conflicts with upstream/main on these two files (upstream's 0.14.2 release commit c0fe0e4): our side now makes no net change to them relative to the merge-base, so a future upstream merge takes theirs on both without conflict. Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): compose --events into Copilot/Devin options() (github#8, github#9) Copilot and Devin are event-capable, but their options() overrides returned only --skills without calling super(), so the base class never declared --events. The documented --integration-options "--events false" opt-out was therefore rejected as unknown for both adapters. Both now compose with super().options() (mirroring Codex and Cursor) so --events is declared alongside --skills. Added a TestEventCapableOptionsCompo sition test class asserting --events appears in Copilot, Devin, Cursor, and Codex options() output. Refs: PR github#3704 Copilot review 4790195897 (findings github#8, github#9) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): Cursor version field, matcher grouping, Copilot cross-OS Address three Copilot review findings on native-config generation: - github#7: Cursor's .cursor/hooks.json schema requires top-level "version": 1, but json-flat used _merge_json_fragment() which only writes hooks, so a freshly generated file was missing the required schema version. Added a version kwarg to _merge_json_fragment (preserving a user's value if present) and the Cursor json-flat branch now passes version=1. - S3: json-nested placed all handlers under the first handler's matcher, so two extensions registering the same event with different matchers both ran for the first matcher and neither for the later. Handlers are now grouped by distinct matcher, emitting one matcher-group per matcher (handlers sharing a matcher stay in one group). - S4: Copilot's bash and powershell fields both received the same host-resolved command, so a config generated on Linux wrote a POSIX venv path into the PowerShell hook (and vice-versa). _dispatcher_command gains a target_os kwarg; Copilot now emits an independent POSIX interpreter (python3) for bash and a Windows interpreter (python) for powershell, so the checked-in config works on either OS. Tests: added TestCursorJsonWriting (version present + preserved) and matcher-grouping regressions (per-distinct-matcher, shared-matcher); updated the Copilot generation test to assert bash != powershell with OS-appropriate interpreters. Refs: PR github#3704 Copilot review 4790195897 (findings github#7, S3, S4) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): anchor py scripts and prefix ps launcher in command runner Address two Copilot review findings on the core command runner: - S2: the py variant called build_python_invocation() on the raw scripts: command string, which left 'scripts/...' anchored at the project root instead of under .specify/ (or .specify/extensions/<id>/). Every event command in a project configured with --script py launched a nonexistent project-root path. The py branch now shares the same base-anchoring as sh/ps and prepends the resolved interpreter as argv (no shell quoting needed for subprocess.run(shell=False)). - S6: the ps variant returned the .ps1 path as the executable, but Windows subprocess.run(shell=False) cannot execute a PowerShell script directly, so event dispatch failed on the default Windows script type. The ps branch now prefixes argv with 'pwsh -File' (PowerShell 7+), falling back to 'powershell -File' (Windows PowerShell) when pwsh is absent. Tests: added test_py_variant_anchored_under_specify and test_ps_variant_prefixed_with_powershell_launcher covering the new argv shapes (interpreter + .specify-anchored path; launcher -File + path). Refs: PR github#3704 Copilot review 4790195897 (findings S2, S6) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): skip-tracking on parse fail, drop dispatcher claim on retain, honor --events false in refresh, preserve layers on invalid override Address four Copilot review findings on merge/teardown/refresh safety: - S5: _merge_json_fragment/_merge_opencode_plugin_ref/_merge_copilot_json now return bool (wrote). Install branches skip manifest.record_existing() and created.append() when a merge was skipped on parse failure, so a user's JSONC/malformed native config is not tracked and manifest.uninstall() can't later delete the untouched file. - S1: remove_integration_events now drops this integration's manifest claim on the shared dispatcher (manifest.remove) even when the file is retained because another integration references it. Previously the retained file stayed tracked, so the subsequent manifest.uninstall() in teardown() saw the matching hash and deleted the file another integration still depended on. The unit test now exercises full teardown() (not just remove_integration_events) to cover the gap. - S7: refresh_integration_events reads each integration's stored parsed_options via _resolve_integration_options and passes them to resolve_events, so a persisted --events false is honored across extension add/enable/disable instead of being discarded (which re-enabled events the user had disabled). - github#10: an invalid override entry now abandons the entire override and keeps the accumulated built-in + extension layers, instead of resetting resolved_override to {} and assigning that empty map to events (which silently disabled all hooks on a single typo). Only a fully-valid override (including an explicit events: {}) replaces the prior layers. Tests: added TestOverridePreserveLayers (invalid entry keeps layers; explicit empty disables), TestSkippedMergeNotTracked (JSONC not recorded), and TestDispatcherManifestClaimDroppedOnRetain (full teardown keeps dispatcher when another integration references it). Added S7 refresh-honors-events-false regression. Refs: PR github#3704 Copilot review 4790195897 (findings S5, S1, S7, github#10) Assisted-by: opencode (model: glm-5.2, autonomous) * test(extensions): update stale validation-message assertion The 'no commands/hooks/events' validation message changed to 'Extension must provide at least one command, hook, or event' when the events feature added a third provider kind, but test_no_commands_no_hooks still matched the old 'must provide at least one command or hook' text and failed on every CI job. Update the regex to the current message. Refs: PR github#3704 CI failure (test_extensions.py:579) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): forced-teardown data safety, manifest-driven command resolution, toml teardown safe-dest Address three findings from Copilot review 4791088500: - S9: _remove_native_event_hooks now unconditionally drops this integration's manifest claim on the native config, not only when the file was deleted. Previously a config whose owned entries were cleaned but user content retained stayed tracked, so teardown(force=True) -> manifest.uninstall( force=True) deleted the entire user-owned settings file. This is the config-file mirror of the earlier shared-dispatcher fix. - S8: _find_command_template resolved extension event commands via a broken registry lookup (the registry stores per-agent registered_commands name-lists, not a {name, file} map) and a file-stem scan that only matched when the .md stem equaled the command name. A manifest mapping speckit.selftest.extension -> commands/selftest.md resolved as missing. It now enumerates installed extensions via ExtensionManager.get_extension() and matches provides.commands[].name -> file, with the directory scan and core-template lookups kept as fallbacks. - R3: _remove_toml_entries now validates the destination with _ensure_safe_destination before read/write, matching the merge path, so a symlink swap of .codex/config.toml after install can't make teardown overwrite a file outside the project. Tests: forced full teardown preserves a user settings file; an extension command whose file stem differs from its name resolves via the manifest; TOML teardown rejects a symlinked config destination. Refs: PR github#3704 Copilot review 4791088500 (findings S8, S9, R3) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): subprocess cwd, shell quoting, TOML matcher escaping, Tabnine ms Address four findings from Copilot review 4791088500: - R1: the generated dispatcher and resolve_and_run_event_command now run their subprocesses with cwd set to the dispatcher-derived project root. Previously 'specify event run' (and the resolved script) inherited the agent's working directory, but event_run resolves the project via Path.cwd(), so a hook fired from a subdirectory targeted the wrong project and reported the command missing. - R2: _dispatcher_command now shell-quotes each component (interpreter, command, event) for the target shell (POSIX via shlex.quote; PowerShell via single-quoted literals with doubled quotes). An interpreter path containing spaces or an extension/override command containing shell metacharacters is passed as a single argument instead of being reinterpreted by the native hook shell. Claude's prefix is left unquoted so the shell still expands it (prefix + relative path are fixed, safe strings). - R4: the Codex TOML matcher is now rendered through the shared TOML escaper like command, so a matcher containing a quote/backslash/newline/control character no longer produces malformed config.toml. - R5: Tabnine declares events_timeout_unit='ms' (its hook schema mirrors Gemini's BeforeTool/AfterTool), so the 60s default becomes 60000ms instead of timeout: 60 (60 ms), which would terminate the dispatcher immediately. Tests: cwd-forced execution from a subdirectory; POSIX/PowerShell quoting of metacharacter and space-bearing components; TOML matcher with a quote parses cleanly; Tabnine timeout converts to 60000. Updated the Copilot generation test for the new quoted args. Refs: PR github#3704 Copilot review 4791088500 (findings R1, R2, R4, R5) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): POSIX dispatcher path constant + platform-agnostic tests Three Windows test failures, one a real cross-OS bug: - W1 (bug): EVENTS_DISPATCHER_REL was str(Path('.specify')/'events.py'), which yields '.specify\events.py' on Windows. Manifest keys are stored in POSIX form (.as_posix()), so 'dispatcher_rel in manifest.files' was always False on Windows: the shared-dispatcher manifest-claim drop was skipped and manifest.uninstall(force=True) deleted the dispatcher another integration still depended on. Make it a POSIX constant (.as_posix()) so it matches manifest keys on every platform. - W2/W3 (tests): the py/ps argv assertions used endswith() and an exact launcher-name set that broke on Windows backslash paths and the pwsh.EXE/full-path launcher returned by shutil.which. Compare in POSIX form and match the launcher by case-insensitive stem. Refs: PR github#3704 Windows CI failures Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): override layer preservation, matcher validation, event command-ref canonicalization Address four Copilot review findings: - C4: a malformed override handler (e.g. "stop: []" or "stop: bad-value") normalizes to no handlers. Previously the entry was skipped and the override still adopted, so an override whose only entry was malformed silently disabled every built-in and extension hook. The empty-handler case now abandons the whole override (keeps prior layers); an explicit "events: {}" (no entries) remains a valid disable. - C6: a non-mapping integration entry (e.g. "claude: bad") was coerced to "events: {}" and treated as a valid explicit disable. It now warns and abandons the override, keeping the accumulated layers. Only an explicitly present, mapping-valued "events" field replaces the prior layers. - C10: matcher is now validated as a string (or absent) in both validate_events (manifest) and _validate_resolved_event (override). A non-string matcher such as "matcher: []" previously passed validation but crashed by_matcher.setdefault(matcher, ...) with TypeError: unhashable type, aborting init or refresh. - C11: ExtensionManifest._validate now applies the same rename + alias-lift canonicalization to event command references that it already applies to hook references. An event referencing an auto-corrected command (e.g. my-ext.boot -> speckit.my-ext.boot) previously kept the obsolete name, so dispatch reported no command and the event silently no-oped. Tests: empty-handler/non-mapping override preserves layers; non-string matcher rejected in manifest and abandoned in override; event command ref lifted to canonical form with a warning. Refs: PR github#3704 Copilot review (findings C4, C6, C10, C11) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): protect shared dispatcher from stale cleanup, delete Cursor version stub, non-destructive refresh Address three Copilot review findings: - C3: the shared .specify/events.py dispatcher is now in events_stale_exclusions(). It is written into every event-capable integration's manifest but reference-counted across them; an upgrade with --events false omits events.py from the new manifest, so the generic stale pass would delete it without the refcount check, breaking any other installed event-capable integration. Its deletion is left to remove_integration_events(), which checks the refcount. - C5: _remove_json_entries now deletes a Spec-Kit-created Cursor file that retains only {"version": 1} after all owned hooks are removed (we added the version field), mirroring _remove_copilot_entries. Previously the generic remover only deleted a literally-empty object, so clean teardown left a generated stub behind. - C12: refresh_integration_events now resolves first and calls install_integration_events once, instead of running the destructive _remove_native_event_hooks pre-step before resolution. A later failure (invalid destination, write error, formatter error) no longer destroys the working native config before the new one is written. install_integration_events already removes stale Specify-marked entries and handles an empty map (stripping prior hooks), so the pre-step was both unsafe and redundant. Tests: dispatcher in stale exclusions; Cursor version-only stub deleted on teardown; refresh failure preserves the pre-existing config (no pre-strip). Refs: PR github#3704 Copilot review (findings C3, C5, C12) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): host target uses POSIX quoting, Claude dispatcher double-quoted, & for windows Address two Copilot review findings on the shell-quoting added in the prior round (R2): - C1: _shell_quote("host") now always uses POSIX shlex.quote, not PowerShell single-quoting on Windows. The single-command-string formats (Claude/Gemini/Qwen/Devin/Tabnine) are run via the agent's POSIX-ish shell (Git Bash on Windows), and a single-quoted 'python' is not invoked as a command by PowerShell without the call operator — so generated hooks failed to launch the dispatcher on Windows. Safe tokens pass through bare (python3, speckit.ext.cmd) on every platform. PowerShell single-quoting is now used only for the explicit target_os="windows" (Copilot's powershell field), where the quoted interpreter is prefixed with "& " so it is actually invoked. - C2: Claude's ${CLAUDE_PROJECT_DIR} dispatcher path is now double-quoted ("${CLAUDE_PROJECT_DIR}/.specify/events.py") so the variable still expands (double quotes allow expansion in POSIX shells) but a project path containing spaces no longer word-splits and breaks dispatcher launch. Tests: host target never emits PowerShell quotes; windows target carries the & call operator; Claude dispatcher is double-quoted; updated Copilot generation assertions for the &-prefixed powershell command. Refs: PR github#3704 Copilot review (findings C1, C2) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): opencode TS plugin resolves dispatcher from directory, execFileSync argv, forwards input+output Address three Copilot review findings on the opencode TS plugin: - C8: the dispatcher and interpreter are now resolved per-project at plugin load from the `directory` OpenCode passes to the plugin factory, not process.cwd(). OpenCode may be launched from a parent directory or host another workspace, in which case process.cwd() pointed at the wrong project and every event failed. The resolver prefers a project-local venv interpreter, then falls back to python3. - C9: the dispatcher is launched with execFileSync and an argv array [interpreter, dispatcher, command, event] instead of a shell command string built by interpolating the interpreter/command/event into a template literal. Command/event strings are only validated as non-empty, so quotes or backticks could previously break the generated TypeScript and shell metacharacters could execute outside the dispatcher; an interpreter path with spaces also failed. No shell is involved now. - C7: tool callbacks now forward both `input` and `output` to runEvent (combined into one JSON payload), so pre_tool_use can inspect the tool arguments and post_tool_use can inspect the result — the primary payload for those events. Previously only `input` was forwarded. Tests: plugin resolves dispatcher/interpreter from `directory` (no process.cwd() path.join), uses execFileSync (no shell string), and forwards output to runEvent for both pre/post_tool_use. Refs: PR github#3704 Copilot review (findings C7, C8, C9) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): Qwen ms timeout, Devin root-nested format, Copilot agentStop Address three Copilot review findings on adapter mappings (verified against each agent's published hook documentation): - U1: Qwen Code command hooks measure timeout in milliseconds (default 60000), per the Qwen Code hooks docs. The adapter previously inherited the seconds default, so every generated handler got timeout: 60 (60 ms) and was killed before the dispatcher could start. Declare events_timeout_unit="ms". - U2: Devin's .devin/hooks.v1.json is a root event map ({"PreToolUse": [...]}) with no top-level "hooks" wrapper (the docs state "the hooks object is the entire file"). The adapter reused json-nested, which writes events under a "hooks" key Devin never reads. Add a json-root-nested format with a matching writer (_merge_json_root) and remover (_remove_json_root_entries) that operate on the root event keys, sharing the matcher-grouping, marker, and JSONC-abort behavior of the nested variants. - U3: Copilot CLI supports the canonical per-turn stop lifecycle as native agentStop; add "stop": "agentStop" to the mapping so an extension's stop handler fires for Copilot. Tests: Qwen timeout converts to 60000; Devin events written at the root (no "hooks" wrapper) and teardown preserves user root entries; Copilot stop maps to agentStop. Refs: PR github#3704 Copilot review (findings U1, U2, U3) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): collect events via validated manifest, surface refresh failures Address two Copilot review findings: - R1: collect_extension_events now reads events from a validated ExtensionManifest (whose command refs were canonicalized at install validation, C11) instead of the raw extension.yml YAML. Previously an event command ref like my-ext.boot was normalized to speckit.my-ext.boot during install validation, but the on-disk YAML kept the obsolete name; refresh then emitted it and _find_command_template could not match it, leaving the hook silently inert. Registry-tracked extensions use the validated manifest; on-disk extensions not yet in the registry fall back to the raw YAML (preserving the partial-staged-install scan behavior). - R3: refresh_integration_events now accumulates per-integration failures and raises EventRefreshError at the end (after refreshing the others) so the extension lifecycle commands (add/remove/enable/disable) can't claim an extension was fully deactivated while a stale native hook may still be active. A new _refresh_events_and_warn helper surfaces the aggregated failures as a warning at each call site without aborting the overall command (the extension was already added/removed/enabled/disabled). Tests: event command ref canonicalized via the validated manifest; refresh failure raises EventRefreshError (aggregated) while still preserving the pre-existing config. Refs: PR github#3704 Copilot review (findings R1, R3) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): probe venv for specify_cli before selecting it; python on Windows Address two Copilot review findings on interpreter resolution: - R2: the dispatcher's _find_specify and the opencode TS resolver both selected a project-local venv python and ran `-m specify_cli` without checking that specify_cli is importable there. In a typical project where Spec Kit is installed globally (or via uv tool) but the project has its own unrelated virtualenv, every event invoked that interpreter and failed instead of reaching the PATH `specify` fallback. Both now probe the candidate interpreter (subprocess `import specify_cli` / execFileSync probe) before selecting it, falling through to the fallback when the venv lacks Spec Kit. - S2: the opencode TS PATH fallback was always `python3`, which is commonly unavailable on Windows. It is now `python` on Windows (process.platform === 'win32') and `python3` on POSIX. Tests: the generated dispatcher contains the _has_specify_cli probe and the PATH fallback; the opencode TS plugin probes for specify_cli and uses a platform-appropriate PATH interpreter. Refs: PR github#3704 Copilot review (findings R2, S2) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): serialize opencode TS plugin string literals as JSON Address Copilot review finding S1: command and matcher values come from user/extension YAML but were interpolated into single-quoted TypeScript literals without escaping. A quote, backslash, or backtick in a command or matcher produced invalid generated TypeScript and could inject code into the plugin. _build_opencode_plugin now serializes every interpolated value (command, event name, native hook key, matcher tool names) as a JSON string literal via json.dumps, which produces a valid double-quoted, fully-escaped TS/JS string. Tests: a command and matcher containing quotes/backticks render inside JSON double-quoted literals; the dangerous single-quoted form is absent. Updated the forwards-output test for the new double-quoted literals. Refs: PR github#3704 Copilot review (finding S1) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): thread per-handler timeout through dispatcher, bash launcher for sh on Windows Address two Copilot review findings: - S4: the dispatcher and inner runner both hardcoded timeout=120, so a valid handler configured with a timeout above 120 seconds could never run for its full duration. The resolved per-handler timeout now flows through the chain: _dispatcher_command appends it (in the integration's native unit, plus a small buffer) as a 4th argument; the generated dispatcher reads sys.argv[3] and uses it for its inner subprocess and the `event run` invocation; `event run` accepts a timeout argument and passes it to resolve_and_run_event_command, which uses it for the script subprocess. Defaults to 120s when absent (backward compat with already-deployed dispatchers that don't pass the arg). - S5: for a project configured with the sh script type on Windows, subprocess.run(shell=False) cannot execute a .sh file directly (chmod doesn't change that). The sh variant now prefixes a bash/sh launcher (resolved via shutil.which) on Windows, mirroring the ps branch's pwsh -File handling. Tests: dispatcher reads the timeout arg and uses it; the native command appends the resolved timeout; the sh variant uses a launcher on Windows. Refs: PR github#3704 Copilot review (findings S4, S5) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): delete shared dispatcher when last event integration disables events Address Copilot review finding S3: the empty-resolved-map install path (--events false upgrade, or override disabling events) stripped prior native hooks but left the shared dispatcher behind. Because the new manifest no longer claims it and stale cleanup excludes it (C3), .specify/events.py became permanently orphaned when this was the last event-capable integration — uninstall could not remove it. Extracted the dispatcher refcount cleanup into _cleanup_shared_dispatcher (shared by remove_integration_events and the empty-map install path) and called it from the empty-map path so the dispatcher is deleted when no other installed event-capable integration's manifest references it, while still being retained when another integration does. Tests: an --events false upgrade of the last event integration deletes the dispatcher; with another integration still referencing it, the dispatcher is retained. Refs: PR github#3704 Copilot review (finding S3) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): map user_prompt_submit/stop for Gemini and Tabnine Address two Copilot review findings on adapter mappings: - S6: Gemini exposes BeforeAgent for the per-turn prompt-submit lifecycle point (verified against Gemini CLI's hooks docs — BeforeAgent fires after the user submits a prompt, before planning). The mapping omitted user_prompt_submit, so valid extension handlers were skipped. Added user_prompt_submit -> BeforeAgent. - S7: Tabnine's Gemini-compatible schema also provides BeforeAgent and AfterAgent, but the mapping omitted user_prompt_submit and stop. Added user_prompt_submit -> BeforeAgent and stop -> AfterAgent so those extension events fire instead of being warned about and skipped. Tests: Gemini and Tabnine mappings include BeforeAgent/AfterAgent. Refs: PR github#3704 Copilot review (findings S6, S7) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): correct timeout unit threading through dispatcher and opencode TS Address two Copilot review findings on the per-handler timeout threading added in the prior round (S4): - R2: _dispatcher_command passed _native_timeout(timeout_seconds) as the dispatcher's 4th argument, but the dispatcher interprets that argument as seconds. For Gemini/Qwen/Tabnine (ms adapters), 60 seconds became 60000 seconds (~16h). It now passes the raw seconds (no unit conversion). The +5s buffer moves to the native hook timeout field (_native_timeout(seconds + EVENT_TIMEOUT_BUFFER)) so the agent's outer cap fires after the dispatcher's inner subprocess timeout — letting the inner kill its child cleanly instead of being killed mid-flight (which orphaned the grandchild script process). - S3: the opencode TS runEvent hardcoded timeout: 60000 (60s) and invoked the dispatcher without its timeout argument, so handlers configured above 60s were killed early while the inner runner defaulted to 120s. runEvent now accepts a timeoutSec parameter (seconds); execFileSync uses (timeoutSec + buffer) * 1000 ms and appends String(timeoutSec) to the dispatcher argv, so both layers honor the per-handler timeout. Tests: the dispatcher arg is raw seconds for ms adapters (60, not 60000); the native timeout field carries the buffer (65 for a 60s Claude handler); opencode runEvent threads the per-handler timeout as the 5th argument. Refs: PR github#3704 Copilot review (findings R2, S3) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): skip disabled extensions in _find_command_template and disk fallback Address Copilot review finding S1: _find_command_template resolved event commands without filtering enabled: false — the registry loop used registry.keys() and the raw directory fallback could also rediscover disabled extensions. If native cleanup is skipped (e.g. a JSONC config cannot be parsed), a stale hook would therefore continue executing a disabled extension. Extracted the disabled-ID logic into _disabled_extension_ids (shared with collect_extension_events) and applied it to both the manifest-resolution loop and the on-disk fallback scan in _find_command_template, so a disabled extension's command is never resolved for dispatch. Tests: a disabled extension's command resolves to None via both the manifest loop and the disk-fallback path. Refs: PR github#3704 Copilot review (finding S1) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): delete shared dispatcher regardless of fresh manifest claim Address Copilot review finding S2: _cleanup_shared_dispatcher gated the no-other-references deletion on `dispatcher_rel in manifest.files`. An `integration upgrade --integration-options "--events false"` passes a fresh manifest (created in _migrate_commands) that never recorded the dispatcher, so the condition was false even though the old on-disk manifest owned the file — and stale cleanup explicitly excludes it (C3), leaving .specify/events.py orphaned after the last integration disabled events. The refcount deletion now runs independently of whether the new manifest contains the key; manifest.remove() stays conditional (a no-op when the key is absent). Tests: an upgrade passing a fresh manifest (no dispatcher claim) still deletes the shared dispatcher when no other integration references it. Refs: PR github#3704 Copilot review (finding S2) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): refresh native event config after extension update Address Copilot review finding S4: the _refresh_events_and_warn helper was wired to extension add/remove/enable/disable, but not to extension_update, which replaces the installed extension.yml (remove + install_from_zip). If an update adds, removes, or changes event declarations, native configs remained stale until a manual integration upgrade. extension_update now refreshes once after the update loop finalizes its successful updates (skipped on rollback/failure), mirroring the other lifecycle commands. Refs: PR github#3704 Copilot review (finding S4) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): make the dispatcher self-contained for one-time/temporary installs Address Copilot review finding R1: the dispatcher required a persistent `specify` executable at runtime. The supported one-time flow runs `specify init` through a temporary `uvx` environment that is discarded, so generated hooks later reached the PATH fallback with no `specify` on PATH and every event failed. The generated .specify/events.py is now self-contained: - Preferred path: it imports specify_cli.events.resolve_and_run_event_command when the package is importable (durable pip/pipx/uv-tool install), which handles extension manifests whose file stem differs from the command name and the project's custom script selection, staying in sync with the CLI. - Fallback path: an inline stdlib-only resolver finds the command template, parses its scripts: frontmatter, resolves the project's script variant (reading .specify/init-options.json directly), and runs the script with the correct launcher (pwsh/bash/interpreter), so one-time and temporary installs work without a persistent `specify` executable on PATH. The `event run` CLI command remains available for manual use; the dispatcher no longer depends on it. Tests: the dispatcher delegates to specify_cli when importable and falls back to the inline resolver when it is not; the inline fallback finds the command template and runs its script end-to-end (shadowing specify_cli with an empty package to force the fallback); the preferred path also runs end-to-end. Refs: PR github#3704 Copilot review (finding R1) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): validate safe destination on all removers and teardown unlinks Address Copilot review findings (inline #1, suppressed #2, #3): - Guard all removers (_remove_json_entries, _remove_copilot_entries, _remove_json_root_entries, _remove_opencode_entries, _remove_native_event_hooks), _cleanup_shared_dispatcher, and remove_integration_events with _ensure_safe_destination(dst) before reading, rewriting, or unlinking. - Prevents teardown or removal operations from overwriting or unlinking external files if a config file, plugin path, or .specify directory is replaced with a symlink post-installation. Tests: added unit tests in TestSafeWriteDestination covering JSON config, OpenCode plugin, and TOML teardown symlink rejection. Refs: PR github#3704 Copilot review (findings inline #1, suppressed #2, #3) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): manifest-driven resolution and disabled-extension filter in dispatcher template Address Copilot review finding (suppressed #1): - In _EVENTS_DISPATCHER_TEMPLATE's _find_command_template, read .specify/extensions/.registry to identify disabled extensions (enabled == false). - Parse provides.commands in each enabled extension's extension.yml to match command_name to its declared file, so commands whose file stem differs from the command name (e.g. speckit.selftest.extension -> commands/selftest.md) resolve correctly when specify_cli is unavailable (one-time uvx installs). - Skip disabled extensions in both manifest-driven and on-disk fallback scans. Refs: PR github#3704 Copilot review (finding suppressed #1) Assisted-by: opencode (model: glm-5.2, autonomous) * fix(events): positive integer timeout validation and OpenCode multi-handler error aggregation Address Copilot review findings (suppressed github#4, github#6): - In validate_events and _validate_resolved_event, validate that timeout (when present) is a positive integer (isinstance(t, int) and not isinstance(t, bool) and t > 0). Rejects string, boolean, zero, or negative timeouts at manifest and override validation time instead of crashing during setup/refresh. - In _build_opencode_plugin, wrap each runEvent invocation inside _ev() in a try/catch block, collect error messages, and throw an aggregate error at the end if any handler failed. Guarantees that all handlers for an event execute to completion even if an earlier handler throws. Tests: added TestTimeoutValidation testing string, boolean, and zero timeout rejections; updated OpenCode plugin merging tests for try/catch error collection. Refs: PR github#3704 Copilot review (findings suppressed github#4, github#6) Assisted-by: opencode (model: glm-5.2, autonomous)
markuswondrak
pushed a commit
that referenced
this pull request
Sep 14, 2026
* fix(workflows): harden community submission workflows per security review
Address two hardening suggestions from a GitHub Security Lab PVR against the
community catalog submission workflows (bundle, extension, preset):
1. Validate the sanitized event snapshot instead of the live issue. Step 1 now
consumes ${{ steps.sanitized.outputs.text }} — the documented gh-aw sanitized
full-context output — rather than instructing the agent to re-fetch the issue
body, which could change between the maintainer applying the label and the
agent reading it. This ties validation to the triggering submission.
2. Make threat detection block safe outputs. Add
safe-outputs.threat-detection.continue-on-error: false so a detected threat
fails the run instead of only warning and still producing a draft PR.
Recompiled the three .lock.yml files with gh-aw v0.79.8.
(Finding #2, create-pull-request allowed-files, is already implemented on all
three workflows upstream, so no change was needed there.)
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(workflows): preserve action pins and add fail-closed regression test
Address PR review feedback:
- Restore actions/checkout (v7.0.1) and actions/setup-node (v7.0.0) pins in the
three regenerated lock files. A local recompile had resolved older cached
pins; the lock files now differ from the base only by the intended snapshot
and threat-detection changes.
- Add a regression test asserting each community submission workflow enables
threat detection with continue-on-error: false in source and compiles to the
fail-closed detection gate, so a later regeneration cannot silently restore
warning-only behavior.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
* fix(workflows): drop sanitized-snapshot input, keep fail-closed detection
The regenerated sanitized snapshot (steps.sanitized.outputs.text) redacts any
HTTPS host absent from the workflow's allowed-domains list. These submission
workflows record off-allowlist URLs verbatim (extension homepage/documentation/
changelog, bundle required component catalogs, and the proposed catalog-entry
JSON), so a snapshot input would corrupt otherwise-valid submissions.
Revert Step 1 to reading the triggering issue and keep the separate maintainer
PR review as the control for issue edits. The fail-closed threat-detection
change (continue-on-error: false) and its regression test are retained.
Recompiled the three lock files; action pins and the pin database are unchanged
from the base.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses the two unresolved review findings from github/spec-kit#2408.
Changes
Finding r3173403182 — centralize
_INTEGRATION_JSONconstant_INTEGRATION_JSONinengine.pywas a duplicate ofINTEGRATION_JSONin__init__.py, risking string drift. Fixed by creating a new side-effect-freesrc/specify_cli/paths.pymodule that both files now import from.Finding r3173403151 — secondary fallback to
init-options.json_load_project_integration()previously only consulted.specify/integration.json. Older projects or partially migrated state (whereintegration.jsonis absent butinit-options.jsonexists) incorrectly fell back to"copilot". Fixed by adding a secondary lookup ininit-options.json(checkingintegrationthenaikeys) before the final"copilot"fallback.Files changed
src/specify_cli/paths.py(new) —INTEGRATION_JSONandINIT_OPTIONS_FILEconstants; dependency-freesrc/specify_cli/__init__.py— importsINTEGRATION_JSONandINIT_OPTIONS_FILEfrompaths.pyinstead of defining them locallysrc/specify_cli/workflows/engine.pyspecify_cli.paths_resolve_default()and_load_project_integration()methods for auto-detection_resolve_inputs()calls_resolve_default()for defaults; post-loop check resolves explicitintegration=autoinputs too_load_project_integration()readsintegration.jsonfirst, falls back toinit-options.json, then"copilot"workflows/speckit/workflow.yml— default changed"copilot"→"auto", prompt updated to includeopencode/auto,requires.integrations.anystatic list removedtests/test_workflows.py—TestIntegrationAutoDetect(10 tests) covering all resolution paths