* 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 #18 (src/specify_cli/__init__.py event-command
import missing # noqa: E402), #19 (unused console import in commands/event.py),
and #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 #3704 Copilot inline review (findings #18, #19, #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.
- #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().
- #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.
- #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.
- #5: bundled-template fallback now uses _locate_core_pack()/_repo_root()
(core_pack/commands, not the non-existent core_pack/templates/commands).
- #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.
- #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.
- #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 #3704 Copilot inline review (findings #2, #4, #5, #6, #7, #13, #16, #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:
- #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.
- #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 (#10).
- #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.
- #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.
- #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).
- #22/#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 '{}'.
- #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 #3704 Copilot inline review (findings #3, #8, #9, #10, #11, #12, #14, #22, #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).
- #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.
- #15: updated PR #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.
(#21 — user YAML override validation — was addressed in the prior tier.)
Refs: PR #3704 Copilot inline review (findings #1, #15, #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 (da6c20d9)
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 c0fe0e43): 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() (#8, #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 #3704 Copilot review 4790195897 (findings #8, #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:
- #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 #3704 Copilot review 4790195897 (findings #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 #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).
- #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 #3704 Copilot review 4790195897 (findings S5, S1, S7, #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #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 #4, #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 #3704 Copilot review (findings suppressed #4, #6)
Assisted-by: opencode (model: glm-5.2, autonomous)
* docs: add Spec Kit spec for agent-context full opt-in
Use Spec Kit's own specify workflow to author the spec that makes the
agent-context extension a full opt-in, removing all agent-context
configuration/support from the Python codebase and removing the
deprecation message. Force-added despite specs/ being gitignored; the
generated artifact will be purged prior to merge.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: add Spec Kit plan artifacts for agent-context full opt-in
Phase 0/1 of the SDD plan workflow: plan.md, research.md, data-model.md,
quickstart.md, and contracts/cli-behavior.md. Constitution Check is a
documented no-op (repo has no ratified constitution). Force-added despite
specs/ being gitignored; generated artifacts will be purged prior to merge.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: correct Constitution Check against ratified v1.0.0
Earlier draft wrongly treated the gate as a no-op; the fork's main is 16
commits behind upstream/main, which carries .specify/memory/constitution.md.
Re-evaluate the feature against Principles I-V (all PASS) and note that
Principle I mandates keeping context_file as a declared class attribute,
validating the R1 metadata decision.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: refresh plan artifacts against synced upstream/main
After syncing fork main to upstream and rebasing, re-scan the current
agent-context surface. Upstream generalized the single context_file into a
plural context_files concept with new resolver helpers
(_resolve_context_files, _resolve_context_file_values,
_format_context_file_values) and upsert/remove now loop over multiple
files. Update research.md, data-model.md, contracts, quickstart grep
guards, and the plan summary to cover the expanded removal scope.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: add Spec Kit tasks for agent-context full opt-in
Phase 2 of SDD: dependency-ordered tasks.md (30 tasks) organized by the
three user stories, with mandatory test tasks (Constitution Principle II)
and a foundational phase decoupling __CONTEXT_FILE__ resolution from the
extension config. Includes the extension self-seeding task (T015) and a
static guard test (T002) enforcing zero agent-context references in the CLI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat!: remove agent-context lifecycle from the Specify CLI
Make the agent-context extension a full opt-in. The CLI no longer
installs the extension during init, writes agent-context-config.yml,
or creates/updates/removes the managed Spec Kit section in agent
context files. Context-section upsert/remove, marker resolution,
extension-enabled gating, the config helpers, and the obsolete inline
deprecation warning are all removed. Integration context_file stays as
inert metadata; __CONTEXT_FILE__ now resolves from registry metadata.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(agent-context): self-seed context file from the active integration
When agent-context-config.yml has no context_file/context_files, the
bundled bash and PowerShell update scripts now resolve the context file
from the active integration in .specify/init-options.json via the
integration registry, so the extension no longer depends on the CLI
writing its config.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test+docs: update suite and docs for agent-context opt-in
Update integration/extension tests to expect no agent-context install,
config, or context-section writes during init. Add a static guard test
(test_agent_context_cli_free.py) asserting the CLI source is free of
agent-context lifecycle symbols, plus backward-compatibility tests for
legacy projects. Refresh AGENTS.md, the extension README, and add a
CHANGELOG entry describing the opt-in behavior change.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(agent-context): warn on self-seed failure, correct docs, speed up guard test
Address PR review feedback:
- Self-seed scripts (bash + PowerShell) now emit an actionable warning when
an active integration is configured but specify_cli cannot be imported by
the chosen Python (e.g. pipx installs), or when the integration declares no
context file, instead of silently falling through to 'nothing to do'.
- Correct the extension README disable note: command rendering never reads the
extension config; __CONTEXT_FILE__ is always substituted from integration
metadata, so a stale context_files value cannot affect rendering.
- Cache CLI source reads in the static guard test via a module-scoped fixture
so the directory walk happens once instead of once per forbidden symbol.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(agent-context): ship self-owned per-agent context-file defaults
The extension now bundles agent-context-defaults.json (key→context_file
map) and self-seeds from it, dropping any dependency on the Specify CLI
registry. Both the bash and PowerShell update scripts read the bundled
JSON map keyed by the active integration from init-options.json.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat!: remove all agent-context state from the Specify CLI
Strip every context_file reference from the CLI: the field on all 35
integration classes, the IntegrationBase plumbing (process_template
param/step, _context_file_display, docstrings), the __CONTEXT_FILE__
resolution in agents.py, the legacy context_file/context_markers
popping in _helpers.py, and the context_file template in
integration_scaffold.py. Also drop the Agent context update step and
__CONTEXT_FILE__ placeholder from templates/commands/plan.md.
The agent-context extension now solely owns all context-file knowledge,
including the per-agent default mapping.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: drop context_file coverage and guard against CLI reintroduction
Remove CONTEXT_FILE attrs and context_file assertions across the base
mixins, all 35 per-integration test files, shared integration tests, and
conftest stubs. Rewrite the base-mixin context tests to assert no managed
section is written and no __CONTEXT_FILE__ placeholder survives. Extend
the CLI-free static guard to forbid context_file, __CONTEXT_FILE__, and
_context_file_display in src/specify_cli, and have the extension tests
copy the bundled defaults JSON so self-seed runs without the CLI.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: reflect full removal of agent-context state from the CLI
Update AGENTS.md (integration examples, required-fields table, context
behavior section, pitfalls), CHANGELOG, and the SDD spec artifacts
(FR-007, SC-002, data-model) to state that the CLI carries no
context_file and the extension fully owns the per-agent default mapping
via agent-context-defaults.json.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: align SDD artifacts with full context_file removal
Update research.md (R1, R2, R4, summary table), contracts/cli-behavior.md
(C3, C5), tasks.md (Phase 2, T026, notes), plan.md (Principle I, source
map), and checklists/requirements.md so the spec artifacts reflect the
implemented decision: the CLI carries no context_file attribute or
__CONTEXT_FILE__ resolution, and the per-agent defaults map lives in the
extension. Resolves PR review #4548130110.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: scrub stale context-file mentions from CLI docstrings
Update the multi_install_safe docstring (drop the removed "context file"
invariant), the RovoDev setup docstring (no longer upserts a context
section), the Copilot module docstring (drop the context-file line), and
tighten the _update_init_options_for_integration note. Pure docstring
changes — no behavioral impact. Resolves PR review #4548237085.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test+docs: harden agent-context test helper and fix stale docs
- base.py: document multi_install_safe as an optional subclass attribute
in the IntegrationBase docstring.
- test_cli.py: clarify the init-options assertion is guarding against
leftover legacy agent-context keys, not relocation.
- test_extension_agent_context.py: _install_agent_context_config now
asserts the bundled agent-context-defaults.json exists and always
copies it, so self-seeding tests fail loudly instead of silently
skipping when the map is missing.
- test_integration_cursor_agent.py: drop Path/IntegrationManifest imports
left unused after removing the context-section frontmatter tests.
Resolves PR review #4548293116.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: remove gitignored SDD artifacts from specs/
The specs/001-agent-context-full-optin/ artifacts were force-added for
dogfooding visibility, but specs/ is gitignored and these were always
intended to be purged before merge. Remove them so merging does not add
an intentionally-untracked directory to repo history.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: keep CHANGELOG.md identical to upstream
CHANGELOG.md is auto-generated at release time, so the branch should not
carry a manual entry. Restore it to match upstream/main exactly.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: preserve Cursor .mdc frontmatter in agent-context updater scripts
The bundled agent-context updater scripts wrote the managed section as
plain text. For Cursor-style `.mdc` targets this dropped the required
`---\nalwaysApply: true\n---` frontmatter, reintroducing the rule-loading
bug originally fixed in #1699. Port the `_ensure_mdc_frontmatter` logic
into both the bash and PowerShell updaters: prepend frontmatter when
missing, repair `alwaysApply` when set to the wrong value, and leave
non-`.mdc` targets untouched. Add regression tests covering both shells.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* test: scope CLI-free guard to agent-context-specific symbols
Drop the bare "context_file" substring from FORBIDDEN_SYMBOLS so the
guard no longer fails on unrelated future CLI fields named context_file.
The list still covers agent-context-specific identifiers (__CONTEXT_FILE__,
_context_file_display, _resolve_context_files, _resolve_context_file_values).
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: harden agent-context bash self-seed against malformed init JSON
Two robustness fixes in the embedded Python self-seed logic:
- Coerce the integration value from init-options.json to a string only when
it is actually a string; otherwise treat it as unset so a corrupted
dict/list value degrades to the existing nothing-to-do behavior instead of
breaking the agents-map lookup.
- Normalize agent-context-defaults.json: only use 'agents' when both the JSON
root and the 'agents' value are dicts, so a wrong-shaped (but valid) JSON
falls back to the warning path instead of raising on .get.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: correct PowerShell hyphenated key lookup and regex replace count
- Self-seed now reads the defaults mapping via
$defaults.agents.PSObject.Properties[$integrationKey].Value instead of
member access ($defaults.agents.$integrationKey), which parsed hyphenated
keys like 'cursor-agent'/'kiro-cli' as subtraction and failed to resolve.
- Replace the static [regex]::Replace(..., 1) call, whose trailing 1 was
interpreted as RegexOptions.IgnoreCase rather than a replacement count, with
an instance Regex whose Replace(input, replacement, 1) limits to the first
match as intended.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix: make bash .mdc frontmatter guard case-insensitive
The bash updater only injected Cursor .mdc frontmatter when ctx_path ended
in lowercase '.mdc', so a mixed/upper-case extension (e.g. specify-rules.MDC)
was skipped and Cursor would not auto-load the rule file. Compare against the
casefolded path. The PowerShell variant already uses -match, which is
case-insensitive by default, so no change is needed there.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* docs: document separator-agnostic agent-context update invocation
The README hard-coded the dot-notation slash command
(/speckit.agent-context.update), which hyphen-separator agents like Forge and
Cline do not recognize. Document the canonical command ID plus both slash
invocations so users copy the form their agent accepts.
Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* Stage 3: Standard markdown integrations — 19 agents migrated to plugin architecture
Migrate all standard markdown integrations to self-contained subpackages
under integrations/. Each subclasses MarkdownIntegration with config-only
overrides (~10 lines per __init__.py).
Integrations migrated (19):
claude, qwen, opencode, junie, kilocode, auggie, roo, codebuddy,
qodercli, amp, shai, bob, trae, pi, iflow, kiro-cli, windsurf,
vibe, cursor-agent
Changes:
- Create integrations/<key>/ subpackage with __init__.py and scripts/
(update-context.sh, update-context.ps1) for each integration
- Register all 19 in INTEGRATION_REGISTRY (20 total with copilot)
- MarkdownIntegration.setup() processes templates (replaces {SCRIPT},
{ARGS}, __AGENT__; strips frontmatter blocks; rewrites paths)
- Extract install_scripts() to IntegrationBase; refactor copilot to use it
- Generalize --ai auto-promote from copilot-only to registry-driven:
any integration registered in INTEGRATION_REGISTRY auto-promotes.
Unregistered agents (gemini, tabnine, codex, kimi, agy, generic)
continue through the legacy --ai path unchanged.
- Fix cursor/cursor-agent key mismatch in CommandRegistrar.AGENT_CONFIGS
- Add missing vibe entry to CommandRegistrar.AGENT_CONFIGS
- Update kiro alias test to reflect auto-promote behavior
Testing:
- Per-agent test files (test_integration_<agent>.py) with shared mixin
- 1316 tests passing, 0 failures
- Complete file inventory tests for both sh and ps variants
- Byte-for-byte validated against v0.4.3 release packages (684 files)
* Address PR review: fix repo root detection and no-op test
- Fix repo root fallback in all 20 update-context.sh scripts: walk up
from script location to find .specify/ instead of falling back to pwd
- Fix repo root fallback in all 20 update-context.ps1 scripts: walk up
from script location to find .specify/ instead of falling back to $PWD
- Add assertions to test_setup_writes_to_correct_directory: verify
expected_dir exists and all command files reside under it
* Fix REPO_ROOT priority: prefer .specify walk-up over git root
In monorepos the git toplevel may differ from the project root that
contains .specify/. The previous fix still preferred git rev-parse
over the walk-up result.
Bash scripts (20): prefer the discovered _root when it contains
.specify/; only accept git root if it also contains .specify/.
PowerShell scripts (20): validate git root contains .specify/ before
using it; fall back to walking up from script directory otherwise.
* Guard git call with try/catch in PowerShell scripts
With $ErrorActionPreference = 'Stop', an unguarded git rev-parse
throws a terminating CommandNotFoundException when git is not
installed, preventing the .specify walk-up fallback from running.
Wrap the git call in try/catch across all 20 update-context.ps1
scripts so the fallback works reliably without git.
* Rename hyphenated package dirs to valid Python identifiers
Rename kiro-cli → kiro_cli and cursor-agent → cursor_agent so the
packages can be imported with normal Python syntax instead of
importlib. The user-facing integration key (IntegrationBase.key)
stays hyphenated to match the actual CLI tool / binary name.
Also reorganize _register_builtins(): imports and registrations
are now grouped alphabetically with clear section comments.
* Reuse CommandRegistrar path rewriting in process_template()
Replace the duplicated regex-based path rewriting in
MarkdownIntegration.process_template() with a call to the shared
CommandRegistrar._rewrite_project_relative_paths() implementation.
This ensures extension-local paths are preserved and boundary rules
stay consistent across the codebase.
* Promote _rewrite_project_relative_paths to public API
Rename CommandRegistrar._rewrite_project_relative_paths() to
rewrite_project_relative_paths() (drop leading underscore) so
integrations can call it without reaching into a private method
across subsystem boundaries.
Addresses PR review feedback:
https://github.com/github/spec-kit/pull/2038#discussion_r3022105627
* Broaden TestRegistrarKeyAlignment to cover all integration keys
Parametrize across ALL_INTEGRATION_KEYS instead of only checking
cursor-agent and vibe. Keeps a separate negative test for the
stale 'cursor' shorthand.
Addresses PR review feedback:
https://github.com/github/spec-kit/pull/2038#discussion_r3022269032