diff --git a/extensions/EXTENSION-API-REFERENCE.md b/extensions/EXTENSION-API-REFERENCE.md index bf85d18826..8097b69a8b 100644 --- a/extensions/EXTENSION-API-REFERENCE.md +++ b/extensions/EXTENSION-API-REFERENCE.md @@ -631,10 +631,23 @@ 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 +> 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. + --- ## CLI Commands diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index adbbedcb94..060bff4bb3 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -3057,6 +3057,21 @@ 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 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( + "\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 5b521cf2a4..ec017b6eea 100644 --- a/templates/commands/analyze.md +++ b/templates/commands/analyze.md @@ -40,11 +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. ``` + - 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 @@ -225,9 +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} ``` + - 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 2e1b1040af..161cbd7054 100644 --- a/templates/commands/checklist.md +++ b/templates/commands/checklist.md @@ -61,11 +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. ``` + - 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 @@ -360,7 +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} ``` + - 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 a83d52f026..8ed427af0d 100644 --- a/templates/commands/clarify.md +++ b/templates/commands/clarify.md @@ -44,11 +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. ``` + - 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 @@ -248,9 +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} ``` + - 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 @@ -278,5 +284,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..6f6507dcd4 100644 --- a/templates/commands/constitution.md +++ b/templates/commands/constitution.md @@ -41,11 +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. ``` + - 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 @@ -144,7 +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} ``` + - 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 c416fa7387..2267dc90f9 100644 --- a/templates/commands/implement.md +++ b/templates/commands/implement.md @@ -40,11 +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. ``` + - 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 @@ -189,9 +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} ``` + - 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 @@ -212,5 +218,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..cb9d9aecad 100644 --- a/templates/commands/plan.md +++ b/templates/commands/plan.md @@ -48,11 +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. ``` + - 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 @@ -88,9 +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} ``` + - 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 @@ -167,5 +173,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..d4fd8d205d 100644 --- a/templates/commands/specify.md +++ b/templates/commands/specify.md @@ -45,11 +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. ``` + - 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,9 +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} ``` + - 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 @@ -338,5 +344,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..6e9229e64f 100644 --- a/templates/commands/tasks.md +++ b/templates/commands/tasks.md @@ -49,11 +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. ``` + - 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 @@ -108,9 +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} ``` + - 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 @@ -212,5 +218,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..456b8e8406 100644 --- a/templates/commands/taskstoissues.md +++ b/templates/commands/taskstoissues.md @@ -41,11 +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. ``` + - 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 @@ -94,7 +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} ``` + - 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 dd231de311..a36ba8d66b 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -5468,6 +5468,144 @@ 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 — unconditionally, since it + cannot reliably detect an orchestrator — to invoke the hook itself + 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 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. + + 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 the hook" 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" + ) + # 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): """Codex projects with skills mode should render $speckit-* invocations.""" init_options = project_dir / ".specify" / "init-options.json"