fix(extensions): make mandatory-hook dispatch contract self-contained#2901
fix(extensions): make mandatory-hook dispatch contract self-contained#2901Quratulain-bilal wants to merge 2 commits into
Conversation
… for agent-direct invocations 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 (github#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 github#2730
mnriem
left a comment
There was a problem hiding this comment.
Thanks for the contribution. A couple of questions before we can move forward:
-
Orchestrator claim: The PR body references
specify runand "CLI orchestrators that watch agent output" as existing consumers of theEXECUTE_COMMAND:directive. Can you point to the code path where this dispatch actually happens today? If no orchestrator currently consumes the directive, the backward-compatibility framing is misleading and the PR description should be corrected. -
AI disclosure: Was this PR authored with AI assistance? If so, please disclose per contributing guidelines.
The underlying fix (agent self-dispatches mandatory hooks in agent-direct sessions) addresses a real problem, but the bar is high for sweeping template changes and we need confidence the framing accurately reflects the codebase.
|
closed this by accident while editing the description - reopened. on the orchestrator question: you're right, I rechecked and couldn't find anything in the repo that actually consumes and yes, this was built with AI assistance (Claude Code) - code, tests and description, reviewed by me. added the if the template changes are too broad I can cut it down to just the format_hook_message() change + tests and do the |
There was a problem hiding this comment.
Pull request overview
This PR strengthens the extension mandatory-hook “dispatch contract” so agent-direct invocations (Claude Code / Cursor / Codex, etc.) don’t silently emit EXECUTE_COMMAND: without actually running the hook, by explicitly instructing the agent to self-dispatch and wait.
Changes:
- Updates runtime hook messaging to explain that
EXECUTE_COMMAND:is a signal (not a dispatcher) and that mandatory hooks must be invoked and awaited. - Updates all bundled command templates to embed the same dispatch-contract guidance and tightens “Done When” criteria.
- Adds/extends tests to prevent regressions in both runtime messages and bundled templates.
Show a summary per file
| File | Description |
|---|---|
src/specify_cli/extensions.py |
Extends mandatory-hook message text to instruct self-dispatch. |
templates/commands/analyze.md |
Adds dispatch-contract bullet after mandatory hook blocks. |
templates/commands/checklist.md |
Adds dispatch-contract bullet after mandatory hook blocks. |
templates/commands/clarify.md |
Adds dispatch-contract bullet after mandatory hook blocks and tightens Done When. |
templates/commands/constitution.md |
Adds dispatch-contract bullet after mandatory hook blocks. |
templates/commands/implement.md |
Adds dispatch-contract bullet after mandatory hook blocks and tightens Done When. |
templates/commands/plan.md |
Adds dispatch-contract bullet after mandatory hook blocks and tightens Done When. |
templates/commands/specify.md |
Adds dispatch-contract bullet after mandatory hook blocks and tightens Done When. |
templates/commands/tasks.md |
Adds dispatch-contract bullet after mandatory hook blocks and tightens Done When. |
templates/commands/taskstoissues.md |
Adds dispatch-contract bullet after mandatory hook blocks. |
extensions/EXTENSION-API-REFERENCE.md |
Documents the dispatch contract for extension authors. |
tests/test_extensions.py |
Adds tests for the runtime message contract and template coverage. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 12/12 changed files
- Comments generated: 19
…vocation
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 github#2730.
|
|
||
| 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 `/<command>`), and wait for its result before proceeding. |
| Executing: `/{command}` | ||
| EXECUTE_COMMAND: {command} | ||
| ``` | ||
| - **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 `/<command>`), and wait for its result before reporting completion. |
|
|
||
| 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 `/<command>`), and wait for its result before proceeding. |
| Executing: `/{command}` | ||
| EXECUTE_COMMAND: {command} | ||
| ``` | ||
| - **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 `/<command>`), and wait for its result before reporting completion. |
|
|
||
| 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 `/<command>`), and wait for its result before proceeding. |
| Executing: `/{command}` | ||
| EXECUTE_COMMAND: {command} | ||
| ``` | ||
| - **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 `/<command>`), and wait for its result before reporting completion. |
|
|
||
| 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 `/<command>`), and wait for its result before proceeding. |
| Executing: `/{command}` | ||
| EXECUTE_COMMAND: {command} | ||
| ``` | ||
| - **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 `/<command>`), and wait for its result before reporting completion. |
| > **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 `/<command>`). Extension authors should not assume an external dispatcher will | ||
| > pick up the directive. |
| 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}" | ||
| ) |
|
Please address Copilot feedback |
Fixes #2730
Problem
The
EXECUTE_COMMAND:directive emitted foroptional: falsehooks is meant as a dispatch signal. In agent-directinvocations (a slash command run inside Claude Code, Cursor, Codex, etc. — the default pattern for extension users),
nothing watches for it. The agent emits the directive, waits, and moves on — the hook never runs, and the failure is
completely silent.
This is the execution half of #2688: PR #2713 (v0.8.15) fixed the emission half (the directive now reliably appears in
agent output), but nothing dispatches it.
This implements Resolution Path 1 from the issue — the lowest-cost fix: make the contract self-contained so the agent
performs the dispatch itself.
Changes
format_hook_message()(src/specify_cli/extensions.py): after theEXECUTE_COMMAND:/EXECUTE_COMMAND_INVOCATION:lines, the message now tells the agent the directive alone executes nothing, and it mustinvoke the rendered hook command itself and wait for completion before continuing.
templates/commands/*.md(all 9 command templates): added the same dispatch-contract bullet after everymandatory-hook block (pre- and post-execution, 18 sites), and tightened the
Done Whenchecklist — emittingEXECUTE_COMMAND:alone does not count as dispatched.extensions/EXTENSION-API-REFERENCE.md: documented the dispatch contract so extension authors don't assume anexternal dispatcher will pick up the directive.
Backward compatibility
The
EXECUTE_COMMAND:/EXECUTE_COMMAND_INVOCATION:directive lines are unchanged and still emitted. To be precise: Icould not find a code path in this repository that currently consumes the directive —
HookExecutor.execute_hook()exists but has no production callers, and the
auto_execute_hookssetting is written as a default but never read fordispatch. The directive is preserved for any external or future consumer, but nothing in-repo depends on it today, which
is consistent with the failure mode reported in #2730. Optional hooks are untouched: they remain a user-facing prompt
with no self-dispatch mandate.
Tests
test_mandatory_hook_message_instructs_agent_to_dispatch— the runtime hook message contains the self-dispatchinstruction with the rendered invocation.
test_optional_hook_message_has_no_dispatch_mandate— optional hooks stay advisory; no behavioral change.test_bundled_templates_state_directive_dispatch_contract— everyEXECUTE_COMMANDblock in every bundled templatecarries the contract bullet, so a future template edit can't silently regress agent-direct invocations back to
emit-and-stall.
Full
tests/test_extensions.pysuite passes (248 passed). The two pre-existingtest_setup_tasks.pyPowerShell-hintfailures reproduce on clean
mainand are unrelated to this change.AI disclosure
This PR was authored with AI assistance (Claude Code) code changes, tests, and description under my direction and review.