subagent-stop: validate dispatch_id against caller role and openness; correct or coach on mismatch #621
Labels
No labels
P0
P1
P2
P3
bug
category/code-change
category/cross-repo
category/eval-run
category/investigation
category/no-test-target
category/owner-ruling
category/prose-artifact
category/recurring-chore
category/structure
category/umbrella
crc-refactor
create
delete
enhancement
filed-by/agent
filed-by/user
frozen
lint-rule
needs-info
needs-triage
next
plugin/cc-architect
plugin/os
plugin/os-adr
plugin/os-aidd-lint
plugin/os-backlog
plugin/os-context
plugin/os-doc-hygiene
plugin/os-sdlc
plugin/os-vault
project/cc-os
ready-for-agent
ready-for-human
recurring
review
rule-proposal
update
waiting
wayfinder:grilling
wayfinder:map
wayfinder:map
wayfinder:research
wayfinder:task
wayfinder:task
wontfix
worklist/deviations
worklist/lint-rule
worklist/new-implement-build
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
jared/cc-os#621
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The bug
subagent-stopaccepts any dispatch id that belongs to the active implementation. It does not check that the id belongs to the caller's role or that the dispatch is still open. A completed id from another role gets a replay reply that looks like a successful settlement, so the caller's own dispatch never closes.dispatch_id: D2(own open programmer dispatch)dispatch_id: D1(test-reviewer, completed),agent_type: programmercompletion_recorded,verdict: pass,next_step: await_completion; D2 stays open until the 15-minute stall watch firesdispatch_id: D2sent twice (own completed dispatch)session_idonly, nodispatch_idcompleted_at: nilos-sdlc-runner gate D1from the programmergate_handoff.rb:31checks existence only)Root cause: the programmer's brief carried the test-reviewer's
dispatch_id, and the charter names no source for the id. The agent followed the brief. The validation below is a guard; the cure is the charter and reply change under "Also in scope".The
await_completionreply is correct given the state. The defect is that the reply reports a completed settlement, so the agent stops and D2 stays open.Decision: refuse, or correct to the caller's sole open dispatch
dispatch D1 belongs to test-reviewer and is completed; open programmer dispatches: D2dispatch_id corrected from D1 to D2agent_typeis absent from the payloadmodel/implementation/instance_methods.rb:104-108)Recommendation: A. Deciding reason: two open same-role rows are legal today, because
pending_dispatch_fororders by id and takes the first with no uniqueness check (model/dispatch/class_methods.rb:21), and resume reuses an open row. So "the caller's sole open dispatch" is not a safe key, and B collapses into A in every case that needs care. Owner decides before implementation. Default if no answer: A.Why it happens
The two lookup paths apply different checks.
On the explicit-id path the payload's
agent_typeis never read. The row's own role wins. A completed row replays its old verdict, and the reply shape is the same as a fresh settlement (completion_recorded,dispatch_id,next_step), so the caller cannot tell replay from settlement.The agent sees two
dispatch_idvalues. Its SubagentStart hook delivers its own id (pipeline/start_subagent.rb:44-46). The previous agent's subagent-stop reply, relayed or read in handoff content, carries another (pipeline/completion_intake.rb:135). The charters say "dispatch_id identifies the dispatch" and do not say which one (agents/programmer.md:121,agents/test-reviewer.md:65-68).skills/responsibility-refactor/SKILL.md:81already records the failure: "the agent relayed the previous node's dispatch id, not its own."What happened (#563, implementation 9, dispatches 106 and 107)
dispatch_id: 106.dispatch_id: 107.dispatch_id: 106,agent_type: programmer.nextcall repliedawait_completion(pipeline/next_step.rb:80) until the stall watch fired and directed a re-dispatch. Row 107 closed at 17:44:55, 16 minutes after row 106.Reproduce
Expected
Proposed shape (illustration, not spec)
Role is checked before the replay branch, so a caller's own completed row still replays.
RoleMismatchrenders the supplied id, its role and state, and one line per open dispatch of the caller's role.Also in scope:
session_idaccept the dispatch owner's active implementation (lookup.rb:52). The refusal must not present other ids as pickable targets unless identity is checked.dispatch_idfromdispatch_reply_fieldsso the agent sees one id. The charters tell agents to relay the returned id (programmer.md:126,test-reviewer.md:72), so reply shape and charters change together.Owner sketch
OsSdlc::Pipeline::CompletionIntake::Lookup#dispatch_by_id!inplugins/os-sdlc/lib/os_sdlc/pipeline/completion_intake/lookup.rbowns the explicit-id lookup. Today it checks only that the row belongs to the active implementation. Change it to also require the row's agent role to equal the payload'sagent_type, beforeCompletionIntake#callreaches the replay branch. A mismatch, or a missingagent_type, raisesRoleMismatch. The CLI renders it as an error reply that names the supplied id, its role and state, and the open dispatches of the caller's role, one line each. No row is written. A caller's own completed row still takes the replay branch.OsSdlc::Pipeline::GateHandoff#dispatchinplugins/os-sdlc/lib/os_sdlc/pipeline/gate_handoff.rbgets the openness half of the same rule: the gate CLI carries noagent_type, so it refuses a completed dispatch and records nothing.The settlement block in
plugins/os-sdlc/agents/programmer.mdandplugins/os-sdlc/agents/test-reviewer.mdsays "Use the dispatch id from your SubagentStart hook." and no longer says "dispatch_id identifies the dispatch". Decision A (refuse on mismatch) applies; do not correct the id to another row.Cases
subagent-stopwithdispatch_id: D1,agent_type: programmerreplies with an error that names D1, its role test-reviewer, its completed state, and lists D2. D1 and D2 are unchanged.subagent-stopwithdispatch_id: D1and noagent_typereplies with the same error. Nothing is written.subagent-stopwithdispatch_id: D2,agent_type: programmerreturns the replay payload, unchanged from today.subagent-stopwithdispatch_id: D1,agent_type: programmerreplies with an error that lists D2 and D3. Neither is written.os-sdlc-runner gate D1refuses and records no step result on D1.programmer.mdandtest-reviewer.mdcontains "Use the dispatch id from your SubagentStart hook." and does not contain "dispatch_id identifies the dispatch".Second opinion (Codex gpt-5.3-codex, 2026-09-22)
Diagnosis confirmed against the code. Recommends refuse-only: require a matching
agent_typeandcompleted_at: nil, refuse on mismatch, never mutate another row. Keep replay for the caller's own completed id. The decision table above carries its arguments.Origin
/os-sdlc:implement 563, first programmer dispatch, session 8858ead3await_completionbecause D2 stays open (next_step.rb:80) ← Runner replayed D1 with a settlement-shaped reply (completion_intake.rb:25,lookup.rb:45-49,instance_methods.rb:59-64check ownership only) ← charter names no source for the id while twodispatch_idfields are visible (programmer.md:121,completion_intake.rb:135). End: DESIGN (CompletionIntake::Lookup, ADR-0144 agent-initiated settlement).OsSdlc::Pipeline::CompletionIntake::Lookup#dispatch_by_id!,OsSdlc::Pipeline::CompletionIntake#call,OsSdlc::Pipeline::GateHandoff#dispatch.sdlc/pipeline.db: row 106 was replayed, not re-settled.Related open: #504, #527, #529, #548.
Work started on branch ticket-621 via /os-sdlc:implement (session 25bd05eb). Decision A (refuse on mismatch) applied as default.
Pipeline run (implementation 11, branch ticket-621) ended implementation_failed at behavior-verifier with verdict:error.
Failed step: behavior-verifier (dispatch 140, 9 s, no model call). Handoff: "Cannot verify: ticket_brief and code_changes were not delivered in the dispatch prompt (SubagentStart context was truncated to a persisted file; preview shows brief: null)."
Why terminal: poodr-ticket-implementation.yaml still routes behavior-verifier
errorto the terminal node. The #618 fix added the bounded self-retry to poodr-implementation.yaml only. Workflow defect ticket to follow.State on branch ticket-621 (uncommitted): cases 1-5 implemented, full suite 1382 runs / 0 failures. Case 6 (charter settlement block text) not done. Awaiting owner decision: hand-finish or reopen.
Workflow defect captured as #623 (ticket map routes behavior-verifier error to terminal; #618 fixed the other map only).
Resolution
Done: subagent-stop refuses a dispatch id of another role or a completed foreign dispatch (RoleMismatch, decision A: refuse, never correct); gate refuses a completed dispatch; programmer.md and test-reviewer.md settlement block reads 'Use the dispatch id from your SubagentStart hook.'
Evidence: merged to main
20f77d9f(commitd0f8c823); full os-sdlc suite 1382 runs / 0 failures; new tests plugins/os-sdlc/tests/pipeline/dispatch_commands_role_mismatch_test.rb (4 runs, 32 assertions); pipeline implementation 11 reached green at programmer (dispatch 139), behavior-verifier errored on map defect, charter line and merge hand-finished by user decisionFollow-ups: #623 ticket map routes behavior-verifier error to terminal (filed, skeptic CREATE); SubagentStart payload size: ticket_brief 11.9 KB + code_changes 6.2 KB = 20.8 KB persisted by the harness, verifier read only the 2 KB preview, tracked as the second observation on #623; none other