From 862f8e15e61b7debc9e1f64f20d0f009143af0be Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Tue, 9 Jun 2026 08:18:11 +0500 Subject: [PATCH 1/3] fix(extensions): make mandatory-hook dispatch contract self-contained for agent-direct invocations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The EXECUTE_COMMAND: directive emitted for optional:false hooks is a dispatch signal for CLI orchestrators that watch agent output. In agent-direct invocations (a slash command run inside Claude Code, Cursor, etc.) no watcher exists, so the agent emits the directive and moves on — the hook never runs and the failure is silent (#2730). - format_hook_message(): after the directive lines, instruct the agent that the directive alone executes nothing and it must invoke the rendered hook command itself and wait for completion. - templates/commands/*.md: add the same dispatch-contract bullet after every mandatory-hook block (pre- and post-execution), and tighten the Done When checklist so emitting the directive alone does not count as dispatched. - EXTENSION-API-REFERENCE.md: document the contract for extension authors. - tests: assert the self-dispatch instruction on mandatory hooks, its absence on optional hooks, and that every EXECUTE_COMMAND block in bundled templates carries the contract bullet. Fixes #2730 --- extensions/EXTENSION-API-REFERENCE.md | 7 +++ src/specify_cli/extensions.py | 13 ++++ templates/commands/analyze.md | 2 + templates/commands/checklist.md | 2 + templates/commands/clarify.md | 4 +- templates/commands/constitution.md | 2 + templates/commands/implement.md | 4 +- templates/commands/plan.md | 4 +- templates/commands/specify.md | 4 +- templates/commands/tasks.md | 4 +- templates/commands/taskstoissues.md | 2 + tests/test_extensions.py | 85 +++++++++++++++++++++++++++ 12 files changed, 128 insertions(+), 5 deletions(-) diff --git a/extensions/EXTENSION-API-REFERENCE.md b/extensions/EXTENSION-API-REFERENCE.md index bf85d18826..5195eefe54 100644 --- a/extensions/EXTENSION-API-REFERENCE.md +++ b/extensions/EXTENSION-API-REFERENCE.md @@ -635,6 +635,13 @@ Executing: `/{command}` EXECUTE_COMMAND: {command} ``` +> **Dispatch contract**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI +> orchestrators that watch agent output — emitting it does not execute anything by +> itself. In agent-direct invocations (e.g. a slash command run inside Claude Code +> with no `specify` CLI orchestrator), no watcher exists, so the agent is instructed +> to invoke the hook command itself and wait for the result. Extension authors should +> not assume an external dispatcher will pick up the directive. + --- ## CLI Commands diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index adbbedcb94..b4e847b4ae 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -3057,6 +3057,19 @@ def format_hook_message( lines.append(f"Executing: `{display_invocation}`") lines.append(f"EXECUTE_COMMAND: {command_text}") lines.append(f"EXECUTE_COMMAND_INVOCATION: {display_invocation}") + # The directive lines above are a dispatch signal for CLI + # orchestrators that watch agent output. In agent-direct + # invocations (e.g. a slash command run inside Claude Code) + # no such watcher exists, so the message must tell the agent + # to perform the dispatch itself — otherwise mandatory hooks + # are silently skipped (issue #2730). + lines.append( + "\nThe EXECUTE_COMMAND directive above is a dispatch " + "signal for orchestrators — emitting it does not run " + "anything by itself. If no orchestrator is handling " + f"this session, you MUST now invoke `{display_invocation}` " + "yourself and wait for it to complete before continuing." + ) return "\n".join(lines) diff --git a/templates/commands/analyze.md b/templates/commands/analyze.md index 5b521cf2a4..bdc4cb3f4c 100644 --- a/templates/commands/analyze.md +++ b/templates/commands/analyze.md @@ -45,6 +45,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Goal. ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Goal @@ -228,6 +229,7 @@ After reporting, check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Operating Principles diff --git a/templates/commands/checklist.md b/templates/commands/checklist.md index 2e1b1040af..03a239e414 100644 --- a/templates/commands/checklist.md +++ b/templates/commands/checklist.md @@ -66,6 +66,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Execution Steps. ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Execution Steps @@ -363,4 +364,5 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/commands/clarify.md b/templates/commands/clarify.md index a83d52f026..a9a2a5ef9a 100644 --- a/templates/commands/clarify.md +++ b/templates/commands/clarify.md @@ -49,6 +49,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -251,6 +252,7 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - **Optional hook** (`optional: true`): ``` ## Extension Hooks @@ -278,5 +280,5 @@ Report completion (after questioning loop ends or early termination): - [ ] Spec ambiguities identified and clarifications integrated into spec file - [ ] Spec quality checklist re-validated against updated spec (if `FEATURE_DIR/checklists/requirements.md` exists) -- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above +- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above (mandatory hooks actually invoked and completed — emitting `EXECUTE_COMMAND:` alone does not count as dispatched) - [ ] Completion reported to user with questions answered, sections touched, checklist status, and coverage summary diff --git a/templates/commands/constitution.md b/templates/commands/constitution.md index 29ae9a09e2..240cb0e3ab 100644 --- a/templates/commands/constitution.md +++ b/templates/commands/constitution.md @@ -46,6 +46,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -147,4 +148,5 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/commands/implement.md b/templates/commands/implement.md index c416fa7387..e5c4cabd05 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -45,6 +45,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -192,6 +193,7 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - **Optional hook** (`optional: true`): ``` ## Extension Hooks @@ -212,5 +214,5 @@ Report final status with summary of completed work. - [ ] All tasks in tasks.md completed and marked `[X]` - [ ] Implementation validated against specification, plan, and test coverage -- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above +- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above (mandatory hooks actually invoked and completed — emitting `EXECUTE_COMMAND:` alone does not count as dispatched) - [ ] Completion reported to user with summary of completed work diff --git a/templates/commands/plan.md b/templates/commands/plan.md index 44ab8403ac..28f3fd8e33 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -53,6 +53,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -91,6 +92,7 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - **Optional hook** (`optional: true`): ``` ## Extension Hooks @@ -167,5 +169,5 @@ Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generate ## Done When - [ ] Plan workflow executed and design artifacts generated -- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above +- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above (mandatory hooks actually invoked and completed — emitting `EXECUTE_COMMAND:` alone does not count as dispatched) - [ ] Completion reported to user with branch, plan path, and generated artifacts diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 469ecbfbd7..641e47073f 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -50,6 +50,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -252,6 +253,7 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - **Optional hook** (`optional: true`): ``` ## Extension Hooks @@ -338,5 +340,5 @@ Success criteria must be: ## Done When - [ ] Specification written to `SPEC_FILE` and validated against quality checklist -- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above +- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above (mandatory hooks actually invoked and completed — emitting `EXECUTE_COMMAND:` alone does not count as dispatched) - [ ] Completion reported to user with feature directory, spec file path, and checklist results diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index f863e7787f..9eaadcd4be 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -54,6 +54,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -111,6 +112,7 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - **Optional hook** (`optional: true`): ``` ## Extension Hooks @@ -212,5 +214,5 @@ Every task MUST strictly follow this format: ## Done When - [ ] tasks.md generated with all phases, task IDs, and file paths -- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above +- [ ] Extension hooks dispatched or skipped according to the rules in Mandatory Post-Execution Hooks above (mandatory hooks actually invoked and completed — emitting `EXECUTE_COMMAND:` alone does not count as dispatched) - [ ] Completion reported to user with task count, story breakdown, and MVP scope diff --git a/templates/commands/taskstoissues.md b/templates/commands/taskstoissues.md index b24e84ee14..95dd450319 100644 --- a/templates/commands/taskstoissues.md +++ b/templates/commands/taskstoissues.md @@ -46,6 +46,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -97,4 +98,5 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/tests/test_extensions.py b/tests/test_extensions.py index dd231de311..6563d6a9e1 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -5468,6 +5468,91 @@ def test_kimi_hooks_render_skill_invocation(self, project_dir): assert "EXECUTE_COMMAND: speckit.plan" in message assert "EXECUTE_COMMAND_INVOCATION: /skill:speckit-plan" in message + def test_mandatory_hook_message_instructs_agent_to_dispatch(self, project_dir): + """Mandatory hook messages tell the agent to run the hook itself. + + The ``EXECUTE_COMMAND:`` directive is a dispatch signal for CLI + orchestrators that watch agent output. In agent-direct invocations + (a slash command run inside Claude Code, Cursor, etc.) no watcher + exists, so without an explicit self-dispatch instruction the agent + emits the directive and moves on — the hook never runs (#2730). + The message must therefore (a) say the directive alone executes + nothing, and (b) instruct the agent to invoke the rendered command + and wait for it. + """ + hook_executor = HookExecutor(project_dir) + message = hook_executor.format_hook_message( + "before_plan", + [ + { + "extension": "test-ext", + "command": "speckit.test-ext.hello", + "optional": False, + } + ], + ) + + # The directive itself is still emitted for orchestrators... + assert "EXECUTE_COMMAND: speckit.test-ext.hello" in message + # ...but the agent is told it is only a signal, not a dispatcher. + assert "does not run anything by itself" in message + # And told to perform the dispatch itself with the rendered + # invocation (whatever form the agent mode renders it in). + assert "you MUST now invoke" in message + assert "wait for it to complete" in message + + def test_optional_hook_message_has_no_dispatch_mandate(self, project_dir): + """Optional hooks stay advisory — no self-dispatch instruction. + + The #2730 fix applies only to ``optional: false`` hooks. Optional + hooks are a user-facing prompt, so adding a "you MUST invoke" + mandate to them would change their semantics. + """ + hook_executor = HookExecutor(project_dir) + message = hook_executor.format_hook_message( + "after_plan", + [ + { + "extension": "test-ext", + "command": "speckit.test-ext.hello", + "optional": True, + "prompt": "Run hello?", + } + ], + ) + + assert "Optional Hook" in message + assert "EXECUTE_COMMAND" not in message + assert "you MUST now invoke" not in message + + def test_bundled_templates_state_directive_dispatch_contract(self): + """Every mandatory-hook block in bundled templates carries the contract. + + The command templates instruct agents to emit ``EXECUTE_COMMAND:`` + for mandatory hooks. Each such block must be followed by the + dispatch-contract bullet telling the agent the directive alone runs + nothing and it must invoke the hook command itself (#2730) — + otherwise a template edit could silently regress agent-direct + invocations back to emit-and-stall. + """ + commands_dir = ( + Path(__file__).resolve().parent.parent / "templates" / "commands" + ) + for command_file in sorted(commands_dir.glob("*.md")): + text = command_file.read_text(encoding="utf-8") + n_directives = text.count("EXECUTE_COMMAND: {command}") + n_contracts = text.count("Dispatch contract for mandatory hooks") + assert n_directives > 0, ( + f"{command_file.name}: expected at least one mandatory-hook " + "block with an EXECUTE_COMMAND directive" + ) + assert n_contracts == n_directives, ( + f"{command_file.name}: {n_directives} EXECUTE_COMMAND " + f"block(s) but {n_contracts} dispatch-contract bullet(s); " + "every mandatory-hook block must state that emitting the " + "directive alone does not run the hook" + ) + def test_codex_hooks_render_dollar_skill_invocation(self, project_dir): """Codex projects with skills mode should render $speckit-* invocations.""" init_options = project_dir / ".specify" / "init-options.json" From 6768f2b347053301d9ac71cfbd2e44ce4cd090f4 Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Wed, 10 Jun 2026 02:43:31 +0500 Subject: [PATCH 2/3] fix(extensions): make hook dispatch unconditional and use rendered invocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback on the dispatch-contract wording: - The self-dispatch instruction was gated on "if no orchestrator is handling this session", which the agent cannot reliably determine. Make it unconditional — the agent always invokes the hook itself — since it cannot detect an external orchestrator. - The instruction (runtime message + all 9 templates) named the hook as /{command}, but invocation format varies by agent: skills mode renders /skill:speckit-* or $speckit-* via HookExecutor._render_hook_invocation. Point the agent at the EXECUTE_COMMAND_INVOCATION line (the rendered, environment-correct form) instead of a hardcoded slash-command. - Tests: assert the unconditional wording, that the instruction uses the rendered invocation in skills mode (not the raw /command), and that no template hardcodes /{command} in its contract bullet. Follow-up to #2730. --- extensions/EXTENSION-API-REFERENCE.md | 12 +++--- src/specify_cli/extensions.py | 24 ++++++------ templates/commands/analyze.md | 4 +- templates/commands/checklist.md | 4 +- templates/commands/clarify.md | 4 +- templates/commands/constitution.md | 4 +- templates/commands/implement.md | 4 +- templates/commands/plan.md | 4 +- templates/commands/specify.md | 4 +- templates/commands/tasks.md | 4 +- templates/commands/taskstoissues.md | 4 +- tests/test_extensions.py | 54 ++++++++++++++++++++++++--- 12 files changed, 86 insertions(+), 40 deletions(-) diff --git a/extensions/EXTENSION-API-REFERENCE.md b/extensions/EXTENSION-API-REFERENCE.md index 5195eefe54..2348fa2fea 100644 --- a/extensions/EXTENSION-API-REFERENCE.md +++ b/extensions/EXTENSION-API-REFERENCE.md @@ -635,12 +635,14 @@ Executing: `/{command}` EXECUTE_COMMAND: {command} ``` -> **Dispatch contract**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI +> **Dispatch contract**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI > orchestrators that watch agent output — emitting it does not execute anything by -> itself. In agent-direct invocations (e.g. a slash command run inside Claude Code -> with no `specify` CLI orchestrator), no watcher exists, so the agent is instructed -> to invoke the hook command itself and wait for the result. Extension authors should -> not assume an external dispatcher will pick up the directive. +> itself. Because the agent cannot reliably tell whether such an orchestrator is +> present, it is instructed to invoke the hook itself and wait for the result. It does +> so using the `EXECUTE_COMMAND_INVOCATION:` line, which carries the environment-correct +> invocation (skills mode renders it as e.g. `/skill:speckit-*` or `$speckit-*` rather +> than `/`). Extension authors should not assume an external dispatcher will +> pick up the directive. --- diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index b4e847b4ae..060bff4bb3 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -3057,18 +3057,20 @@ def format_hook_message( lines.append(f"Executing: `{display_invocation}`") lines.append(f"EXECUTE_COMMAND: {command_text}") lines.append(f"EXECUTE_COMMAND_INVOCATION: {display_invocation}") - # The directive lines above are a dispatch signal for CLI - # orchestrators that watch agent output. In agent-direct - # invocations (e.g. a slash command run inside Claude Code) - # no such watcher exists, so the message must tell the agent - # to perform the dispatch itself — otherwise mandatory hooks - # are silently skipped (issue #2730). + # The directive lines above are only a dispatch signal for + # CLI orchestrators that watch agent output; emitting them + # does not run anything. The agent cannot reliably tell + # whether such an orchestrator is present, so the contract is + # unconditional: invoke the hook yourself. ``display_invocation`` + # is the environment-correct form (skills mode renders e.g. + # ``/skill:speckit-*`` rather than ``/``). Without + # this, mandatory hooks are silently skipped (issue #2730). lines.append( - "\nThe EXECUTE_COMMAND directive above is a dispatch " - "signal for orchestrators — emitting it does not run " - "anything by itself. If no orchestrator is handling " - f"this session, you MUST now invoke `{display_invocation}` " - "yourself and wait for it to complete before continuing." + "\nEmitting the EXECUTE_COMMAND directive above does not " + "run the hook. You MUST now invoke the hook yourself using " + f"`{display_invocation}` (the invocation shown on the " + "EXECUTE_COMMAND_INVOCATION line) and wait for it to " + "complete before continuing." ) return "\n".join(lines) diff --git a/templates/commands/analyze.md b/templates/commands/analyze.md index bdc4cb3f4c..35d41a4feb 100644 --- a/templates/commands/analyze.md +++ b/templates/commands/analyze.md @@ -45,7 +45,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Goal. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Goal @@ -229,7 +229,7 @@ After reporting, check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Operating Principles diff --git a/templates/commands/checklist.md b/templates/commands/checklist.md index 03a239e414..24d0a68650 100644 --- a/templates/commands/checklist.md +++ b/templates/commands/checklist.md @@ -66,7 +66,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Execution Steps. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Execution Steps @@ -364,5 +364,5 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/commands/clarify.md b/templates/commands/clarify.md index a9a2a5ef9a..0ea5029bc6 100644 --- a/templates/commands/clarify.md +++ b/templates/commands/clarify.md @@ -49,7 +49,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -252,7 +252,7 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. - **Optional hook** (`optional: true`): ``` ## Extension Hooks diff --git a/templates/commands/constitution.md b/templates/commands/constitution.md index 240cb0e3ab..ecd9553607 100644 --- a/templates/commands/constitution.md +++ b/templates/commands/constitution.md @@ -46,7 +46,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -148,5 +148,5 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/commands/implement.md b/templates/commands/implement.md index e5c4cabd05..5fd7c17bed 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -45,7 +45,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -193,7 +193,7 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. - **Optional hook** (`optional: true`): ``` ## Extension Hooks diff --git a/templates/commands/plan.md b/templates/commands/plan.md index 28f3fd8e33..9afe297d36 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -53,7 +53,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -92,7 +92,7 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. - **Optional hook** (`optional: true`): ``` ## Extension Hooks diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 641e47073f..8a034618c2 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -50,7 +50,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -253,7 +253,7 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. - **Optional hook** (`optional: true`): ``` ## Extension Hooks diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 9eaadcd4be..15a8110ed0 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -54,7 +54,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -112,7 +112,7 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. - **Optional hook** (`optional: true`): ``` ## Extension Hooks diff --git a/templates/commands/taskstoissues.md b/templates/commands/taskstoissues.md index 95dd450319..5cba1352a3 100644 --- a/templates/commands/taskstoissues.md +++ b/templates/commands/taskstoissues.md @@ -46,7 +46,7 @@ You **MUST** consider the user input before proceeding (if not empty). Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before proceeding. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -98,5 +98,5 @@ Check if `.specify/extensions.yml` exists in the project root. Executing: `/{command}` EXECUTE_COMMAND: {command} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything by itself. After emitting the block, you MUST actually invoke `/{command}` yourself and wait for its result before reporting completion. In agent-direct sessions (no `specify` CLI orchestrator), nothing else will run the hook. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 6563d6a9e1..204374ea8c 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -5477,7 +5477,8 @@ def test_mandatory_hook_message_instructs_agent_to_dispatch(self, project_dir): exists, so without an explicit self-dispatch instruction the agent emits the directive and moves on — the hook never runs (#2730). The message must therefore (a) say the directive alone executes - nothing, and (b) instruct the agent to invoke the rendered command + nothing, and (b) instruct the agent — unconditionally, since it + cannot reliably detect an orchestrator — to invoke the hook itself and wait for it. """ hook_executor = HookExecutor(project_dir) @@ -5495,12 +5496,44 @@ def test_mandatory_hook_message_instructs_agent_to_dispatch(self, project_dir): # The directive itself is still emitted for orchestrators... assert "EXECUTE_COMMAND: speckit.test-ext.hello" in message # ...but the agent is told it is only a signal, not a dispatcher. - assert "does not run anything by itself" in message - # And told to perform the dispatch itself with the rendered - # invocation (whatever form the agent mode renders it in). - assert "you MUST now invoke" in message + assert "does not run the hook" in message + # The instruction is unconditional — no "if no orchestrator" hedge + # the agent can't actually evaluate. + assert "You MUST now invoke the hook" in message assert "wait for it to complete" in message + def test_mandatory_hook_dispatch_uses_rendered_invocation(self, project_dir): + """The self-dispatch instruction names the environment-correct command. + + Hook invocation format varies by agent/session: skills-mode + commands render as e.g. ``/skill:speckit-*`` (kimi) or + ``$speckit-*`` (codex), not ``/``. The instruction must + point at the rendered invocation (``EXECUTE_COMMAND_INVOCATION``), + not a hardcoded ``/`` that would tell the agent to run a + command that doesn't exist in that environment. + """ + init_options = project_dir / ".specify" / "init-options.json" + init_options.parent.mkdir(parents=True, exist_ok=True) + init_options.write_text(json.dumps({"ai": "kimi", "ai_skills": False})) + + hook_executor = HookExecutor(project_dir) + message = hook_executor.format_hook_message( + "before_plan", + [ + { + "extension": "test-ext", + "command": "speckit.test-ext.hello", + "optional": False, + } + ], + ) + + # Kimi renders the skill form; the dispatch instruction must use it. + assert "/skill:speckit-test-ext-hello" in message + # And must not tell the agent to run the raw slash-command form, + # which is wrong for this environment. + assert "`/speckit.test-ext.hello`" not in message + def test_optional_hook_message_has_no_dispatch_mandate(self, project_dir): """Optional hooks stay advisory — no self-dispatch instruction. @@ -5523,7 +5556,7 @@ def test_optional_hook_message_has_no_dispatch_mandate(self, project_dir): assert "Optional Hook" in message assert "EXECUTE_COMMAND" not in message - assert "you MUST now invoke" not in message + assert "You MUST now invoke the hook" not in message def test_bundled_templates_state_directive_dispatch_contract(self): """Every mandatory-hook block in bundled templates carries the contract. @@ -5552,6 +5585,15 @@ def test_bundled_templates_state_directive_dispatch_contract(self): "every mandatory-hook block must state that emitting the " "directive alone does not run the hook" ) + # The contract bullet must point at the rendered invocation, not + # a hardcoded ``/{command}`` — invocation form varies by agent + # (skills mode renders ``/skill:speckit-*`` etc.), so telling the + # agent to run ``/{command}`` would be wrong in those sessions. + assert text.count("EXECUTE_COMMAND_INVOCATION") >= n_contracts, ( + f"{command_file.name}: dispatch-contract bullet must reference " + "the EXECUTE_COMMAND_INVOCATION line for the environment-" + "correct invocation rather than hardcoding /{command}" + ) def test_codex_hooks_render_dollar_skill_invocation(self, project_dir): """Codex projects with skills mode should render $speckit-* invocations.""" From 53391235d2f6e11511d3d404a8ddf7ccb0b01bca Mon Sep 17 00:00:00 2001 From: Quratulain-bilal Date: Wed, 10 Jun 2026 22:04:45 +0500 Subject: [PATCH 3/3] fix(extensions): emit EXECUTE_COMMAND_INVOCATION in template hook examples The dispatch-contract bullet tells the agent to self-dispatch using the EXECUTE_COMMAND_INVOCATION line, but the mandatory-hook example blocks in the templates (and the API-reference example) never emitted that line and hardcoded `Executing: /{command}`. In skills-mode sessions /{command} isn't a runnable form, so the instruction wasn't self-contained. - All 9 templates (pre- and post-hook example blocks, 18 total): emit `EXECUTE_COMMAND_INVOCATION: {invocation}` and use {invocation} for the Executing line. Define {invocation} as the environment-correct command form (usually /{command}; skills mode renders /skill:speckit-* or $speckit-*) so the placeholder is self-explaining. - EXTENSION-API-REFERENCE.md: mandatory-hook example now shows the INVOCATION line and explains {invocation}. - Test: count emitted EXECUTE_COMMAND_INVOCATION directive lines (indented, inside the fence) and require one per EXECUTE_COMMAND block, so a prose mention can't mask a missing line and templates can't regress to emitting only EXECUTE_COMMAND. Follow-up to #2730. --- extensions/EXTENSION-API-REFERENCE.md | 6 +++++- templates/commands/analyze.md | 12 ++++++++---- templates/commands/checklist.md | 12 ++++++++---- templates/commands/clarify.md | 12 ++++++++---- templates/commands/constitution.md | 12 ++++++++---- templates/commands/implement.md | 12 ++++++++---- templates/commands/plan.md | 12 ++++++++---- templates/commands/specify.md | 12 ++++++++---- templates/commands/tasks.md | 12 ++++++++---- templates/commands/taskstoissues.md | 12 ++++++++---- tests/test_extensions.py | 27 +++++++++++++++++++-------- 11 files changed, 96 insertions(+), 45 deletions(-) diff --git a/extensions/EXTENSION-API-REFERENCE.md b/extensions/EXTENSION-API-REFERENCE.md index 2348fa2fea..8097b69a8b 100644 --- a/extensions/EXTENSION-API-REFERENCE.md +++ b/extensions/EXTENSION-API-REFERENCE.md @@ -631,10 +631,14 @@ Or for mandatory hooks: ```markdown **Automatic Hook**: {extension} -Executing: `/{command}` +Executing: `{invocation}` EXECUTE_COMMAND: {command} +EXECUTE_COMMAND_INVOCATION: {invocation} ``` +Here `{invocation}` is the environment-correct command form: usually `/{command}`, but +skills-mode sessions render it as e.g. `/skill:speckit-*` or `$speckit-*`. + > **Dispatch contract**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI > orchestrators that watch agent output — emitting it does not execute anything by > itself. Because the agent cannot reliably tell whether such an orchestrator is diff --git a/templates/commands/analyze.md b/templates/commands/analyze.md index 35d41a4feb..ec017b6eea 100644 --- a/templates/commands/analyze.md +++ b/templates/commands/analyze.md @@ -40,12 +40,14 @@ You **MUST** consider the user input before proceeding (if not empty). ## Extension Hooks **Automatic Pre-Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} Wait for the result of the hook command before proceeding to the Goal. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Goal @@ -226,10 +228,12 @@ After reporting, check if `.specify/extensions.yml` exists in the project root. ## Extension Hooks **Automatic Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before reporting completion. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Operating Principles diff --git a/templates/commands/checklist.md b/templates/commands/checklist.md index 24d0a68650..161cbd7054 100644 --- a/templates/commands/checklist.md +++ b/templates/commands/checklist.md @@ -61,12 +61,14 @@ You **MUST** consider the user input before proceeding (if not empty). ## Extension Hooks **Automatic Pre-Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} Wait for the result of the hook command before proceeding to the Execution Steps. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Execution Steps @@ -361,8 +363,10 @@ Check if `.specify/extensions.yml` exists in the project root. ## Extension Hooks **Automatic Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before reporting completion. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/commands/clarify.md b/templates/commands/clarify.md index 0ea5029bc6..8ed427af0d 100644 --- a/templates/commands/clarify.md +++ b/templates/commands/clarify.md @@ -44,12 +44,14 @@ You **MUST** consider the user input before proceeding (if not empty). ## Extension Hooks **Automatic Pre-Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -249,10 +251,12 @@ Check if `.specify/extensions.yml` exists in the project root. ## Extension Hooks **Automatic Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before reporting completion. - **Optional hook** (`optional: true`): ``` ## Extension Hooks diff --git a/templates/commands/constitution.md b/templates/commands/constitution.md index ecd9553607..6f6507dcd4 100644 --- a/templates/commands/constitution.md +++ b/templates/commands/constitution.md @@ -41,12 +41,14 @@ You **MUST** consider the user input before proceeding (if not empty). ## Extension Hooks **Automatic Pre-Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -145,8 +147,10 @@ Check if `.specify/extensions.yml` exists in the project root. ## Extension Hooks **Automatic Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before reporting completion. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/templates/commands/implement.md b/templates/commands/implement.md index 5fd7c17bed..2267dc90f9 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -40,12 +40,14 @@ You **MUST** consider the user input before proceeding (if not empty). ## Extension Hooks **Automatic Pre-Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -190,10 +192,12 @@ Check if `.specify/extensions.yml` exists in the project root. ## Extension Hooks **Automatic Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before reporting completion. - **Optional hook** (`optional: true`): ``` ## Extension Hooks diff --git a/templates/commands/plan.md b/templates/commands/plan.md index 9afe297d36..cb9d9aecad 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -48,12 +48,14 @@ You **MUST** consider the user input before proceeding (if not empty). ## Extension Hooks **Automatic Pre-Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -89,10 +91,12 @@ Check if `.specify/extensions.yml` exists in the project root. ## Extension Hooks **Automatic Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before reporting completion. - **Optional hook** (`optional: true`): ``` ## Extension Hooks diff --git a/templates/commands/specify.md b/templates/commands/specify.md index 8a034618c2..d4fd8d205d 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -45,12 +45,14 @@ You **MUST** consider the user input before proceeding (if not empty). ## Extension Hooks **Automatic Pre-Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -250,10 +252,12 @@ Check if `.specify/extensions.yml` exists in the project root. ## Extension Hooks **Automatic Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before reporting completion. - **Optional hook** (`optional: true`): ``` ## Extension Hooks diff --git a/templates/commands/tasks.md b/templates/commands/tasks.md index 15a8110ed0..6e9229e64f 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -49,12 +49,14 @@ You **MUST** consider the user input before proceeding (if not empty). ## Extension Hooks **Automatic Pre-Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -109,10 +111,12 @@ Check if `.specify/extensions.yml` exists in the project root. ## Extension Hooks **Automatic Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before reporting completion. - **Optional hook** (`optional: true`): ``` ## Extension Hooks diff --git a/templates/commands/taskstoissues.md b/templates/commands/taskstoissues.md index 5cba1352a3..456b8e8406 100644 --- a/templates/commands/taskstoissues.md +++ b/templates/commands/taskstoissues.md @@ -41,12 +41,14 @@ You **MUST** consider the user input before proceeding (if not empty). ## Extension Hooks **Automatic Pre-Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} Wait for the result of the hook command before proceeding to the Outline. ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before proceeding. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before proceeding. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently ## Outline @@ -95,8 +97,10 @@ Check if `.specify/extensions.yml` exists in the project root. ## Extension Hooks **Automatic Hook**: {extension} - Executing: `/{command}` + Executing: `{invocation}` EXECUTE_COMMAND: {command} + EXECUTE_COMMAND_INVOCATION: {invocation} ``` - - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself, using the invocation shown on the `EXECUTE_COMMAND_INVOCATION:` line (which is the correct command form for this environment — skills mode may render it as `/skill:speckit-*` or `$speckit-*` rather than `/`), and wait for its result before reporting completion. + - Here `{invocation}` is the command form for the current environment: usually `/{command}`, but skills-mode sessions render it differently (e.g. `/skill:speckit-*` or `$speckit-*`). Use the same value for both the `Executing:` and `EXECUTE_COMMAND_INVOCATION:` lines. + - **Dispatch contract for mandatory hooks**: the `EXECUTE_COMMAND:` line is only a dispatch signal for CLI orchestrators that watch agent output — emitting it does not run anything. You MUST then invoke the hook yourself using `{invocation}` (the value on the `EXECUTE_COMMAND_INVOCATION:` line) and wait for its result before reporting completion. - If no hooks are registered or `.specify/extensions.yml` does not exist, skip silently diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 204374ea8c..a36ba8d66b 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -5585,14 +5585,25 @@ def test_bundled_templates_state_directive_dispatch_contract(self): "every mandatory-hook block must state that emitting the " "directive alone does not run the hook" ) - # The contract bullet must point at the rendered invocation, not - # a hardcoded ``/{command}`` — invocation form varies by agent - # (skills mode renders ``/skill:speckit-*`` etc.), so telling the - # agent to run ``/{command}`` would be wrong in those sessions. - assert text.count("EXECUTE_COMMAND_INVOCATION") >= n_contracts, ( - f"{command_file.name}: dispatch-contract bullet must reference " - "the EXECUTE_COMMAND_INVOCATION line for the environment-" - "correct invocation rather than hardcoding /{command}" + # Every mandatory-hook example block must actually emit the + # ``EXECUTE_COMMAND_INVOCATION:`` directive line (indented inside + # the code fence), not just mention it in prose. The contract + # tells the agent to self-dispatch using that line, so a block + # that emits only ``EXECUTE_COMMAND:`` leaves skills-mode agents + # without a runnable invocation. Count the emitted directive + # lines, not a free-floating substring, so the prose mention in + # the contract bullet can't mask a missing line. + n_invocation_lines = sum( + 1 + for line in text.splitlines() + if line.strip().startswith("EXECUTE_COMMAND_INVOCATION:") + ) + assert n_invocation_lines == n_directives, ( + f"{command_file.name}: {n_directives} EXECUTE_COMMAND " + f"directive line(s) but {n_invocation_lines} " + "EXECUTE_COMMAND_INVOCATION directive line(s); every " + "mandatory-hook block must emit the rendered invocation so " + "skills-mode agents have an environment-correct command to run" ) def test_codex_hooks_render_dollar_skill_invocation(self, project_dir):