Skip to content

fix(extensions): make mandatory-hook dispatch contract self-contained#2901

Open
Quratulain-bilal wants to merge 2 commits into
github:mainfrom
Quratulain-bilal:fix/hook-directive-dispatch-contract
Open

fix(extensions): make mandatory-hook dispatch contract self-contained#2901
Quratulain-bilal wants to merge 2 commits into
github:mainfrom
Quratulain-bilal:fix/hook-directive-dispatch-contract

Conversation

@Quratulain-bilal

@Quratulain-bilal Quratulain-bilal commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Fixes #2730

Problem

The EXECUTE_COMMAND: directive emitted for optional: false hooks is meant as a dispatch signal. In agent-direct
invocations (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 the EXECUTE_COMMAND: /
    EXECUTE_COMMAND_INVOCATION: lines, the message now tells the agent the directive alone executes nothing, and it must
    invoke 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 every
    mandatory-hook block (pre- and post-execution, 18 sites), and tightened the Done When checklist — emitting
    EXECUTE_COMMAND: alone does not count as dispatched.
  • extensions/EXTENSION-API-REFERENCE.md: documented the dispatch contract so extension authors don't assume an
    external dispatcher will pick up the directive.

Backward compatibility

The EXECUTE_COMMAND: / EXECUTE_COMMAND_INVOCATION: directive lines are unchanged and still emitted. To be precise: I
could 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_hooks setting is written as a default but never read for
dispatch. 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-dispatch
    instruction 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 — every EXECUTE_COMMAND block in every bundled template
    carries the contract bullet, so a future template edit can't silently regress agent-direct invocations back to
    emit-and-stall.

Full tests/test_extensions.py suite passes (248 passed). The two pre-existing test_setup_tasks.py PowerShell-hint
failures reproduce on clean main and 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.

… 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
@Quratulain-bilal Quratulain-bilal requested a review from mnriem as a code owner June 9, 2026 03:21
@Quratulain-bilal Quratulain-bilal changed the title fix(extensions): make mandatory-hook dispatch contract self-contained… fix(extensions): make mandatory-hook dispatch contract self-contained Jun 9, 2026

@mnriem mnriem left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution. A couple of questions before we can move forward:

  1. Orchestrator claim: The PR body references specify run and "CLI orchestrators that watch agent output" as existing consumers of the EXECUTE_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.

  2. 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.

@Quratulain-bilal

Quratulain-bilal commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

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
EXECUTE_COMMAND today. execute_hook() has no production callers and auto_execute_hooks gets written as a default but
never read. I picked that framing up from the issue author's wording in #2730 without verifying it myself. fixed the PR
description.

and yes, this was built with AI assistance (Claude Code) - code, tests and description, reviewed by me. added the
disclosure to the PR body.

if the template changes are too broad I can cut it down to just the format_hook_message() change + tests and do the
templates as a follow-up.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread src/specify_cli/extensions.py
Comment thread templates/commands/analyze.md Outdated
Comment thread templates/commands/checklist.md Outdated
Comment thread templates/commands/clarify.md Outdated
Comment thread templates/commands/constitution.md Outdated
Comment thread templates/commands/implement.md Outdated
Comment thread templates/commands/plan.md Outdated
Comment thread templates/commands/specify.md Outdated
Comment thread templates/commands/tasks.md Outdated
Comment thread templates/commands/taskstoissues.md Outdated
…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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot's findings

  • Files reviewed: 12/12 changed files
  • Comments generated: 20


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.
Comment on lines +638 to +645
> **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.
Comment thread tests/test_extensions.py
Comment on lines +5592 to +5596
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}"
)
@mnriem

mnriem commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Please address Copilot feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: PR #2713's EXECUTE_COMMAND directive emits correctly but is never executed in pure-Claude-Code invocations

3 participants