Investigate the double subagent-stop path: agent prompts pipe into the CLI while the SubagentStop hook also fires #766

Closed
opened 2026-09-27 13:10:48 +00:00 by jared · 1 comment
Owner

Problem

Ten agent prompts pipe a completion event into os-sdlc-runner subagent-stop by hand. The SubagentStop hook also fires on the same event and calls DispatchCommands.subagent_stop_from_stdin. Two paths can record one completion. This can create double rows in the dispatch table and makes the true completion record unclear.

Observed

The following agent prompts instruct the agent to run printf ... | os-sdlc-runner subagent-stop:

plugins/os-sdlc/agents/refactorer.md:60
plugins/os-sdlc/agents/test-reviewer.md:70
plugins/os-sdlc/agents/reviewer.md:46
plugins/os-sdlc/agents/behavior-verifier.md:49
plugins/os-sdlc/agents/seam-designer.md:68
plugins/os-sdlc/agents/suite-check.md:46
plugins/os-sdlc/agents/test-writer.md:163
plugins/os-sdlc/agents/programmer.md:120
plugins/os-sdlc/agents/change-auditor.md:120
plugins/os-sdlc/agents/contract-auditor.md:52

plugins/os-sdlc/hooks/subagent_stop.rb also runs on the native SubagentStop hook event and calls DispatchCommands.subagent_stop_from_stdin. An auto-memory note records that SubagentStart/Stop hooks fire natively, which suggests the prose-instructed pipe predates the working hook.

Reproduce

n/a: investigation, not a reproducible bug.

Approach

VERDICT: CREATE (user-approved 2026-09-27)

Required work:

  1. Trace both paths in lib/os_sdlc/command/dispatch_commands* — the prose-instructed CLI pipe and the native hook call to DispatchCommands.subagent_stop_from_stdin.
  2. Check for an existing idempotency guard on the dispatch table for a repeated subagent-stop call for the same dispatch.
  3. Check the dispatch table for evidence of double rows from one agent completion.
  4. Decide which path is authoritative: the hand-written prompt instruction, or the native hook.
  5. Report the finding. Make no code change in this ticket.

Acceptance criteria

  1. Given both the prompt-instructed pipe and the native SubagentStop hook fire for the same agent run, when the dispatch table is inspected, then the report states whether one or two completion rows result.
  2. Given the trace of both paths, when the report is filed, then it names the authoritative path and the disposition of the other (keep, remove, or fold in).

Owner sketch

n/a: investigation ticket, no new code.

Origin

  • Trigger: user question about hooks calling DispatchCommands directly, during session 9 CRC.
  • Improvised this session: none.
  • Chain: possible double-recorded completion ← two independent triggers for subagent-stop (prompt pipe and native hook) ← DESIGN, prompt instructions written before hooks were confirmed to fire natively (see os-sdlc hooks fire natively memory note).
  • Root candidate: this ticket.
  • Where: plugins/os-sdlc/agents/.md, plugins/os-sdlc/hooks/subagent_stop.rb, plugins/os-sdlc/lib/os_sdlc/command/dispatch_commands.
  • Session: 8e4aac4f-9878-45da-ac68-c7d55261a73f
  • Transcript: /home/jared/dev/cc-os/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-crc-pass-os-sdlc/8e4aac4f-9878-45da-ac68-c7d55261a73f.jsonl
## Problem Ten agent prompts pipe a completion event into `os-sdlc-runner subagent-stop` by hand. The `SubagentStop` hook also fires on the same event and calls `DispatchCommands.subagent_stop_from_stdin`. Two paths can record one completion. This can create double rows in the dispatch table and makes the true completion record unclear. ## Observed The following agent prompts instruct the agent to run `printf ... | os-sdlc-runner subagent-stop`: ``` plugins/os-sdlc/agents/refactorer.md:60 plugins/os-sdlc/agents/test-reviewer.md:70 plugins/os-sdlc/agents/reviewer.md:46 plugins/os-sdlc/agents/behavior-verifier.md:49 plugins/os-sdlc/agents/seam-designer.md:68 plugins/os-sdlc/agents/suite-check.md:46 plugins/os-sdlc/agents/test-writer.md:163 plugins/os-sdlc/agents/programmer.md:120 plugins/os-sdlc/agents/change-auditor.md:120 plugins/os-sdlc/agents/contract-auditor.md:52 ``` `plugins/os-sdlc/hooks/subagent_stop.rb` also runs on the native `SubagentStop` hook event and calls `DispatchCommands.subagent_stop_from_stdin`. An auto-memory note records that SubagentStart/Stop hooks fire natively, which suggests the prose-instructed pipe predates the working hook. ## Reproduce n/a: investigation, not a reproducible bug. ## Approach VERDICT: CREATE (user-approved 2026-09-27) Required work: 1. Trace both paths in `lib/os_sdlc/command/dispatch_commands*` — the prose-instructed CLI pipe and the native hook call to `DispatchCommands.subagent_stop_from_stdin`. 2. Check for an existing idempotency guard on the dispatch table for a repeated `subagent-stop` call for the same dispatch. 3. Check the dispatch table for evidence of double rows from one agent completion. 4. Decide which path is authoritative: the hand-written prompt instruction, or the native hook. 5. Report the finding. Make no code change in this ticket. ## Acceptance criteria 1. Given both the prompt-instructed pipe and the native `SubagentStop` hook fire for the same agent run, when the dispatch table is inspected, then the report states whether one or two completion rows result. 2. Given the trace of both paths, when the report is filed, then it names the authoritative path and the disposition of the other (keep, remove, or fold in). ## Owner sketch n/a: investigation ticket, no new code. ## Origin - Trigger: user question about hooks calling `DispatchCommands` directly, during session 9 CRC. - Improvised this session: none. - Chain: possible double-recorded completion ← two independent triggers for `subagent-stop` (prompt pipe and native hook) ← DESIGN, prompt instructions written before hooks were confirmed to fire natively (see os-sdlc hooks fire natively memory note). - Root candidate: this ticket. - Where: plugins/os-sdlc/agents/*.md, plugins/os-sdlc/hooks/subagent_stop.rb, plugins/os-sdlc/lib/os_sdlc/command/dispatch_commands*. - Session: 8e4aac4f-9878-45da-ac68-c7d55261a73f - Transcript: /home/jared/dev/cc-os/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-crc-pass-os-sdlc/8e4aac4f-9878-45da-ac68-c7d55261a73f.jsonl
Author
Owner

Resolution

Done: Investigated. The two subagent-stop paths are not a race. The prompt-driven pipe (dispatch_id + YAML handoff, run by the agent after its gate) is the authoritative settlement; it records verdict, objections and next step. The native SubagentStop hook is keyed by session_id + agent_type and carries only last_assistant_message; in practice it fires after the prompt pipe and Lookup#late_hook_replay? makes it a silent no-op. Neither path writes a second row or overwrites state.

Evidence: lib/os_sdlc/command/completion_intake.rb:23-28 (guard: late_hook_replay? then dispatch_id_path? && completed? replay); lib/os_sdlc/query/completion_intake/lookup.rb:29-37; lib/os_sdlc/command/dispatch_commands.rb:56-66 (hook content = last_assistant_message); tests/pipeline/cli_subagent_stop_test.rb:179 and tests/hooks/subagent_stop_test.rb:53 pin both no-ops. Caller inventory 2026-09-27: 11 prompt lines pipe subagent-stop; brief has a live caller via presenter/hook_payload.rb:32 (Brief exceeds the hook cap message), so #768's uncalled set is four, not five.

Follow-ups: Hook safety-net test: dropped by the user as speculative hardening, none. #768 amended: brief is live, uncalled set is four, prompt lines and the hook-cap message rename in the same commit, stdout diff per command is the acceptance test. #767 unchanged: rewiring the hook to DispatchCommands changes nothing the pipeline observes.

## Resolution **Done:** Investigated. The two subagent-stop paths are not a race. The prompt-driven pipe (dispatch_id + YAML handoff, run by the agent after its gate) is the authoritative settlement; it records verdict, objections and next step. The native SubagentStop hook is keyed by session_id + agent_type and carries only last_assistant_message; in practice it fires after the prompt pipe and Lookup#late_hook_replay? makes it a silent no-op. Neither path writes a second row or overwrites state. **Evidence:** lib/os_sdlc/command/completion_intake.rb:23-28 (guard: late_hook_replay? then dispatch_id_path? && completed? replay); lib/os_sdlc/query/completion_intake/lookup.rb:29-37; lib/os_sdlc/command/dispatch_commands.rb:56-66 (hook content = last_assistant_message); tests/pipeline/cli_subagent_stop_test.rb:179 and tests/hooks/subagent_stop_test.rb:53 pin both no-ops. Caller inventory 2026-09-27: 11 prompt lines pipe subagent-stop; brief has a live caller via presenter/hook_payload.rb:32 (Brief exceeds the hook cap message), so #768's uncalled set is four, not five. **Follow-ups:** Hook safety-net test: dropped by the user as speculative hardening, none. #768 amended: brief is live, uncalled set is four, prompt lines and the hook-cap message rename in the same commit, stdout diff per command is the acceptance test. #767 unchanged: rewiring the hook to DispatchCommands changes nothing the pipeline observes.
jared closed this issue 2026-09-27 13:44:29 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#766
No description provided.