subagent-stop: validate dispatch_id against caller role and openness; correct or coach on mismatch #621

Closed
opened 2026-09-22 17:06:03 +00:00 by jared · 4 comments
Owner

The bug

subagent-stop accepts 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.

Call Current result Correct result
dispatch_id: D2 (own open programmer dispatch) D2 settled, next step returned same
dispatch_id: D1 (test-reviewer, completed), agent_type: programmer replay of D1: completion_recorded, verdict: pass, next_step: await_completion; D2 stays open until the 15-minute stall watch fires refusal that names D1's role and lists the open programmer dispatch D2
dispatch_id: D2 sent twice (own completed dispatch) replay of D2 same
session_id only, no dispatch_id lookup filters by role and completed_at: nil same
os-sdlc-runner gate D1 from the programmer step results and verdict recorded on D1 (gate_handoff.rb:31 checks existence only) refusal, same rule as subagent-stop

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_completion reply 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

A: refuse on mismatch B: correct when exactly one open same-role dispatch
Reply on the #563 case dispatch D1 belongs to test-reviewer and is completed; open programmer dispatches: D2 D2 settled; dispatch_id corrected from D1 to D2
Rows mutated none D2, chosen by the Runner, not the caller
When two open same-role rows exist refuse, list both must refuse anyway
When agent_type is absent from the payload refuse no key to correct with; must refuse anyway
Resume reuse of an open same-role row (model/implementation/instance_methods.rb:104-108) unaffected a correction can settle a resumed agent's row

Recommendation: A. Deciding reason: two open same-role rows are legal today, because pending_dispatch_for orders 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.

# session-id path: role + openness  (model/dispatch/class_methods.rb:20-25)
def pending_dispatch_for(implementation_id:, agent_type:)
  joined_dataset.where(Sequel[:dispatches][:implementation_id] => implementation_id,
                       Sequel[:agents][:agent_type] => agent_type,
                       Sequel[:dispatches][:completed_at] => nil)
                .order(Sequel.desc(Sequel[:dispatches][:id])).first
end

# explicit-id path: ownership only, and the caller's role is discarded
# (pipeline/completion_intake/lookup.rb:22, 45-49)
def agent_type
  @agent_type ||= dispatch_id_path? ? dispatch.agent_type : requested_agent_type
end

def dispatch_by_id!
  row = Model.for(@db).dispatch[@request.dispatch_id] || raise(UnknownDispatch, ...)
  owning_implementation!(row).owned_dispatch!(row)
end

# (model/implementation/instance_methods.rb:59-64)
def owned_dispatch!(dispatch)
  return dispatch if dispatch.implementation_id == id
  raise StaleDispatch, "..."
end

# a completed id short-circuits to replay before any role check (pipeline/completion_intake.rb:25)
def call
  return already_recorded_payload if @lookup.late_hook_replay?
  return replay_payload if dispatch_id_path? && dispatch.completed?
  record_completion
end

On the explicit-id path the payload's agent_type is 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_id values. 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:81 already records the failure: "the agent relayed the previous node's dispatch id, not its own."

What happened (#563, implementation 9, dispatches 106 and 107)

  1. test-reviewer settled D1 (row 106, 17:28:51) and relayed a reply containing dispatch_id: 106.
  2. The programmer's prompt carried that content. Its own hook carried dispatch_id: 107.
  3. The programmer sent subagent-stop with dispatch_id: 106, agent_type: programmer.
  4. The Runner replayed 106. Row 106 is unchanged: its handoff still holds the test-reviewer summary. Row 107 stayed open.
  5. Every next call replied await_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.
  6. Session workaround: each later prompt said "your id is the one from the hook, not from handoff content." It stopped the recurrence for that run only.

Reproduce

# implementation with completed test-reviewer dispatch D1 and open programmer dispatch D2
printf '%s' '{"dispatch_id": D1, "agent_type": "programmer", "content": "..."}' | os-sdlc-runner subagent-stop
# reply: completion_recorded, verdict from D1, next_step await_completion
os-sdlc-runner next   # await_completion instead of routing

Expected

def test_subagent_stop_refuses_dispatch_id_of_another_role
  # arrange: D1 completed test-reviewer, D2 open programmer, same implementation
  # act: subagent-stop with dispatch_id: D1, agent_type: programmer
  # assert: D1 unchanged; D2 open; reply names D1's role and lists D2
end

def test_subagent_stop_refuses_dispatch_id_without_agent_type
  # act: subagent-stop with dispatch_id: D1 and no agent_type
  # assert: refusal, no row mutated
end

def test_subagent_stop_replays_own_completed_dispatch
  # act: subagent-stop with dispatch_id: D2, agent_type: programmer, after D2 completed
  # assert: replay payload, unchanged from today
end

def test_subagent_stop_refusal_lists_both_open_same_role_dispatches
  # arrange: two open programmer dispatches (resume reuse)
  # assert: refusal lists both; neither mutated
end

Proposed shape (illustration, not spec)

class CompletionIntake::Lookup
  def dispatch_by_id!(id, agent_type:)
    row = owned!(id)
    return row if row.agent_type == agent_type

    open = Dispatch.pending_for(implementation_id: row.implementation_id, agent_type:)
    raise RoleMismatch.new(supplied: row, agent_type:, open:)
  end
end

Role is checked before the replay branch, so a caller's own completed row still replays. RoleMismatch renders the supplied id, its role and state, and one line per open dispatch of the caller's role.

Also in scope:

  1. Charter fix (root cause): the settlement block says "Use the dispatch id from your SubagentStart hook." Coordinate with #504, which single-sources that block.
  2. Explicit-id requests without session_id accept the dispatch owner's active implementation (lookup.rb:52). The refusal must not present other ids as pickable targets unless identity is checked.
  3. Defense in depth, not the fix: drop dispatch_id from dispatch_reply_fields so 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! in plugins/os-sdlc/lib/os_sdlc/pipeline/completion_intake/lookup.rb owns 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's agent_type, before CompletionIntake#call reaches the replay branch. A mismatch, or a missing agent_type, raises RoleMismatch. 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#dispatch in plugins/os-sdlc/lib/os_sdlc/pipeline/gate_handoff.rb gets the openness half of the same rule: the gate CLI carries no agent_type, so it refuses a completed dispatch and records nothing.

The settlement block in plugins/os-sdlc/agents/programmer.md and plugins/os-sdlc/agents/test-reviewer.md says "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

  1. D1 is a completed test-reviewer dispatch and D2 an open programmer dispatch of the same implementation. subagent-stop with dispatch_id: D1, agent_type: programmer replies with an error that names D1, its role test-reviewer, its completed state, and lists D2. D1 and D2 are unchanged.
  2. Same rows. subagent-stop with dispatch_id: D1 and no agent_type replies with the same error. Nothing is written.
  3. D2 is completed. subagent-stop with dispatch_id: D2, agent_type: programmer returns the replay payload, unchanged from today.
  4. Two open programmer dispatches D2 and D3. subagent-stop with dispatch_id: D1, agent_type: programmer replies with an error that lists D2 and D3. Neither is written.
  5. D1 is completed. os-sdlc-runner gate D1 refuses and records no step result on D1.
  6. The settlement block of programmer.md and test-reviewer.md contains "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_type and completed_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

  • Trigger: /os-sdlc:implement 563, first programmer dispatch, session 8858ead3
  • Chain: 15-minute stall ← await_completion because 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-64 check ownership only) ← charter names no source for the id while two dispatch_id fields are visible (programmer.md:121, completion_intake.rb:135). End: DESIGN (CompletionIntake::Lookup, ADR-0144 agent-initiated settlement).
  • Root candidate: this ticket; #504 covers charter single-sourcing
  • Where: OsSdlc::Pipeline::CompletionIntake::Lookup#dispatch_by_id!, OsSdlc::Pipeline::CompletionIntake#call, OsSdlc::Pipeline::GateHandoff#dispatch
  • Session: 2ed2de63-e2ae-4d11-aa0a-13ff25b27c1b
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-563/2ed2de63-e2ae-4d11-aa0a-13ff25b27c1b.jsonl
  • Rewritten 2026-09-22 (session 25bd05eb) after checking .sdlc/pipeline.db: row 106 was replayed, not re-settled.

Related open: #504, #527, #529, #548.

## The bug `subagent-stop` accepts 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. | Call | Current result | Correct result | | --- | --- | --- | | `dispatch_id: D2` (own open programmer dispatch) | D2 settled, next step returned | same | | `dispatch_id: D1` (test-reviewer, completed), `agent_type: programmer` | replay of D1: `completion_recorded`, `verdict: pass`, `next_step: await_completion`; D2 stays open until the 15-minute stall watch fires | refusal that names D1's role and lists the open programmer dispatch D2 | | `dispatch_id: D2` sent twice (own completed dispatch) | replay of D2 | same | | `session_id` only, no `dispatch_id` | lookup filters by role and `completed_at: nil` | same | | `os-sdlc-runner gate D1` from the programmer | step results and verdict recorded on D1 (`gate_handoff.rb:31` checks existence only) | refusal, same rule as subagent-stop | 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_completion` reply 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 | | A: refuse on mismatch | B: correct when exactly one open same-role dispatch | | --- | --- | --- | | Reply on the #563 case | `dispatch D1 belongs to test-reviewer and is completed; open programmer dispatches: D2` | D2 settled; `dispatch_id corrected from D1 to D2` | | Rows mutated | none | D2, chosen by the Runner, not the caller | | When two open same-role rows exist | refuse, list both | must refuse anyway | | When `agent_type` is absent from the payload | refuse | no key to correct with; must refuse anyway | | Resume reuse of an open same-role row (`model/implementation/instance_methods.rb:104-108`) | unaffected | a correction can settle a resumed agent's row | Recommendation: A. Deciding reason: two open same-role rows are legal today, because `pending_dispatch_for` orders 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. ```ruby # session-id path: role + openness (model/dispatch/class_methods.rb:20-25) def pending_dispatch_for(implementation_id:, agent_type:) joined_dataset.where(Sequel[:dispatches][:implementation_id] => implementation_id, Sequel[:agents][:agent_type] => agent_type, Sequel[:dispatches][:completed_at] => nil) .order(Sequel.desc(Sequel[:dispatches][:id])).first end # explicit-id path: ownership only, and the caller's role is discarded # (pipeline/completion_intake/lookup.rb:22, 45-49) def agent_type @agent_type ||= dispatch_id_path? ? dispatch.agent_type : requested_agent_type end def dispatch_by_id! row = Model.for(@db).dispatch[@request.dispatch_id] || raise(UnknownDispatch, ...) owning_implementation!(row).owned_dispatch!(row) end # (model/implementation/instance_methods.rb:59-64) def owned_dispatch!(dispatch) return dispatch if dispatch.implementation_id == id raise StaleDispatch, "..." end # a completed id short-circuits to replay before any role check (pipeline/completion_intake.rb:25) def call return already_recorded_payload if @lookup.late_hook_replay? return replay_payload if dispatch_id_path? && dispatch.completed? record_completion end ``` On the explicit-id path the payload's `agent_type` is 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_id` values. 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:81` already records the failure: "the agent relayed the previous node's dispatch id, not its own." ## What happened (#563, implementation 9, dispatches 106 and 107) 1. test-reviewer settled D1 (row 106, 17:28:51) and relayed a reply containing `dispatch_id: 106`. 2. The programmer's prompt carried that content. Its own hook carried `dispatch_id: 107`. 3. The programmer sent subagent-stop with `dispatch_id: 106`, `agent_type: programmer`. 4. The Runner replayed 106. Row 106 is unchanged: its handoff still holds the test-reviewer summary. Row 107 stayed open. 5. Every `next` call replied `await_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. 6. Session workaround: each later prompt said "your id is the one from the hook, not from handoff content." It stopped the recurrence for that run only. ## Reproduce ```sh # implementation with completed test-reviewer dispatch D1 and open programmer dispatch D2 printf '%s' '{"dispatch_id": D1, "agent_type": "programmer", "content": "..."}' | os-sdlc-runner subagent-stop # reply: completion_recorded, verdict from D1, next_step await_completion os-sdlc-runner next # await_completion instead of routing ``` ## Expected ```ruby def test_subagent_stop_refuses_dispatch_id_of_another_role # arrange: D1 completed test-reviewer, D2 open programmer, same implementation # act: subagent-stop with dispatch_id: D1, agent_type: programmer # assert: D1 unchanged; D2 open; reply names D1's role and lists D2 end def test_subagent_stop_refuses_dispatch_id_without_agent_type # act: subagent-stop with dispatch_id: D1 and no agent_type # assert: refusal, no row mutated end def test_subagent_stop_replays_own_completed_dispatch # act: subagent-stop with dispatch_id: D2, agent_type: programmer, after D2 completed # assert: replay payload, unchanged from today end def test_subagent_stop_refusal_lists_both_open_same_role_dispatches # arrange: two open programmer dispatches (resume reuse) # assert: refusal lists both; neither mutated end ``` ## Proposed shape (illustration, not spec) ```ruby class CompletionIntake::Lookup def dispatch_by_id!(id, agent_type:) row = owned!(id) return row if row.agent_type == agent_type open = Dispatch.pending_for(implementation_id: row.implementation_id, agent_type:) raise RoleMismatch.new(supplied: row, agent_type:, open:) end end ``` Role is checked before the replay branch, so a caller's own completed row still replays. `RoleMismatch` renders the supplied id, its role and state, and one line per open dispatch of the caller's role. Also in scope: 1. Charter fix (root cause): the settlement block says "Use the dispatch id from your SubagentStart hook." Coordinate with #504, which single-sources that block. 2. Explicit-id requests without `session_id` accept the dispatch owner's active implementation (`lookup.rb:52`). The refusal must not present other ids as pickable targets unless identity is checked. 3. Defense in depth, not the fix: drop `dispatch_id` from `dispatch_reply_fields` so 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!` in `plugins/os-sdlc/lib/os_sdlc/pipeline/completion_intake/lookup.rb` owns 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's `agent_type`, before `CompletionIntake#call` reaches the replay branch. A mismatch, or a missing `agent_type`, raises `RoleMismatch`. 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#dispatch` in `plugins/os-sdlc/lib/os_sdlc/pipeline/gate_handoff.rb` gets the openness half of the same rule: the gate CLI carries no `agent_type`, so it refuses a completed dispatch and records nothing. The settlement block in `plugins/os-sdlc/agents/programmer.md` and `plugins/os-sdlc/agents/test-reviewer.md` says "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 1. D1 is a completed test-reviewer dispatch and D2 an open programmer dispatch of the same implementation. `subagent-stop` with `dispatch_id: D1`, `agent_type: programmer` replies with an error that names D1, its role test-reviewer, its completed state, and lists D2. D1 and D2 are unchanged. 2. Same rows. `subagent-stop` with `dispatch_id: D1` and no `agent_type` replies with the same error. Nothing is written. 3. D2 is completed. `subagent-stop` with `dispatch_id: D2`, `agent_type: programmer` returns the replay payload, unchanged from today. 4. Two open programmer dispatches D2 and D3. `subagent-stop` with `dispatch_id: D1`, `agent_type: programmer` replies with an error that lists D2 and D3. Neither is written. 5. D1 is completed. `os-sdlc-runner gate D1` refuses and records no step result on D1. 6. The settlement block of `programmer.md` and `test-reviewer.md` contains "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_type` and `completed_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 - Trigger: `/os-sdlc:implement 563`, first programmer dispatch, session 8858ead3 - Chain: 15-minute stall ← `await_completion` because 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-64` check ownership only) ← charter names no source for the id while two `dispatch_id` fields are visible (`programmer.md:121`, `completion_intake.rb:135`). End: DESIGN (`CompletionIntake::Lookup`, ADR-0144 agent-initiated settlement). - Root candidate: this ticket; #504 covers charter single-sourcing - Where: `OsSdlc::Pipeline::CompletionIntake::Lookup#dispatch_by_id!`, `OsSdlc::Pipeline::CompletionIntake#call`, `OsSdlc::Pipeline::GateHandoff#dispatch` - Session: 2ed2de63-e2ae-4d11-aa0a-13ff25b27c1b - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-563/2ed2de63-e2ae-4d11-aa0a-13ff25b27c1b.jsonl - Rewritten 2026-09-22 (session 25bd05eb) after checking `.sdlc/pipeline.db`: row 106 was replayed, not re-settled. Related open: #504, #527, #529, #548.
Author
Owner

Work started on branch ticket-621 via /os-sdlc:implement (session 25bd05eb). Decision A (refuse on mismatch) applied as default.

Work started on branch ticket-621 via /os-sdlc:implement (session 25bd05eb). Decision A (refuse on mismatch) applied as default.
Author
Owner

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 error to 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.

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 `error` to 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.
Author
Owner

Workflow defect captured as #623 (ticket map routes behavior-verifier error to terminal; #618 fixed the other map only).

Workflow defect captured as #623 (ticket map routes behavior-verifier error to terminal; #618 fixed the other map only).
Author
Owner

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 (commit d0f8c823); 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 decision

Follow-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

## 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 (commit d0f8c823); 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 decision **Follow-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
jared closed this issue 2026-09-22 18:52:12 +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#621
No description provided.