T-ICA5-task-work-sub-agent-verdict-contract-clarity
Status: closed/done · Impact: high · Complexity: small
When /sdlc:orchestrate dispatches a /sdlc:task-work sub-agent and instructs it to “return one
verdict line (DONE / BLOCKED / NEEDS-DEFINITION / ERROR)”, the sub-agent sometimes returns
/sdlc:task-ensure-ready’s READY: <basename> marker instead — exiting after Step 5a without ever
running Steps 5b through 10. This silently leaves the worktree and feat branch in a half-flipped
state (start-commit never landed on main; no implementation; no PR) and the orchestrator only
notices by checking artifacts. Make the verdict contract unambiguous enough that the sub-agent can’t
conflate intermediate sub-skill markers with its own final verdict.
-
plugin/skills/orchestrate/SKILL.mdStep 3 prompts the sub-agent:Run /sdlc:task-work <slug> end-to-end. Return one verdict line: DONE pr=#<N> / BLOCKED reason=... / NEEDS-DEFINITION slug=... / ERROR reason=...This is the dispatch contract.
-
plugin/skills/task-work/SKILL.mdStep 5a invokes/sdlc:task-ensure-readyand its successful return isREADY: <basename>on stdout. Step 5a’s prose says “The task is ready to implement iff the final/sdlc:task-ensure-readycall reportedREADY:” — which makesREADY:look like a milestone marker to the sub-agent. Combined with the fact that a sub-agent emittingREADY:as its own final line is a syntactically valid sentence-shaped marker that the dispatching parent doesn’t reject, this turns into an exit-after-Step-5a bug. -
Observed 2026-05-20 in tick 13: the
2026-05-20-task-close-out-verifies-pr-on-mainsub-agent returnedREADY: 2026-05-20-task-close-out-verifies-pr-on-mainand exited. The orchestrator caught it by reading the actual repo state (worktree present, task on main stillopen/ready, no PR) and dispatched a resumption sub-agent with explicit “do not return READY: — that’s ensure-ready’s marker, not yours” instructions. -
No validation in the orchestrator that the verdict line matches one of the documented task-work markers. The current dispatch loop reads whatever the sub-agent returned and records it.
Proposed
Section titled “Proposed”Three layers of defense:
- Skill prose —
plugin/skills/task-work/SKILL.mdadds an explicit “final verdict” section near the end that enumerates the four valid markers, names them as “task-work verdicts” (distinct from sub-skill markers), and explicitly states that markers from/sdlc:task-ensure-ready(READY:,NEEDS-DEFINITION:) and/sdlc:task-define(DEFINED:,NO CHANGES:) are NEVER acceptable as task-work’s own final line. Reword the Step 5a guard so the sub-agent can’t read it as “stop here”. - Orchestrator validation —
plugin/skills/orchestrate/SKILL.mdStep 3 dispatch loop validates the returned verdict against a hard-coded allowlist (DONE pr=#,BLOCKED reason=,NEEDS-DEFINITION slug=,ERROR reason=). If the verdict doesn’t match, log it as anANOMALYin the digest (with the raw return string) and treat the sub-agent as having failed at the task-work level — the orchestrator should then probe repo state to decide whether to dispatch a resumption or tear down the half-started worktree. - Sub-agent prompt — the orchestrator’s dispatch prompt for task-work gains one extra line
above the verdict enumeration: “These verdicts are FINAL outputs from
/sdlc:task-work, not from the sub-skills it invokes. Markers likeREADY:orNEEDS-DEFINITION:from/sdlc:task-ensure-readyare intermediate; you must continue past them through Steps 5b-10 unless explicitly halting per the task-work spec.”
Approach
Section titled “Approach”- Edit
plugin/skills/task-work/SKILL.mdto add the final-verdict section and reword Step 5a’s guard. The Step 5a guard already says “If not — the user bailed, or a gap remained — stop task-work here” — make the “stop” path explicitly emitNEEDS-DEFINITION slug=<basename>(which is one of the task-work verdicts the orchestrator already accepts), so the sub-agent never has reason to emitREADY:as a final line. - Edit
plugin/skills/orchestrate/SKILL.mdStep 3 dispatch prose to (a) extend the dispatch prompt with the “intermediate vs final marker” clarification and (b) add a validation step that rejects unrecognized verdicts and logs them asANOMALYin the digest. - Update
plugin/skills/orchestrate/invariants.yamlrequired_phrasesto include a sentinel string from the new validation prose so future skill drift trips the lint. - (Optional, if time) Extend
plugin/skills/task-work/invariants.yamlwith a forbidden-phrase guard against the sub-agent emittingREADY:as a top-level verdict in its own prose.
Files to touch
Section titled “Files to touch”plugin/skills/task-work/SKILL.md— add final-verdict section; reword Step 5a’s halt path to emitNEEDS-DEFINITION slug=.plugin/skills/orchestrate/SKILL.md— extend Step 3 dispatch prompt; add verdict validation step.plugin/skills/orchestrate/invariants.yaml— pin the validation sentinel phrase.plugin/skills/task-work/invariants.yaml— (optional) add forbidden-phrase guard.
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
plugin/skills/task-work/SKILL.mdcontains an explicit “final verdict” section enumerating exactly the four task-work markers and stating that sub-skill markers (READY:,NEEDS-DEFINITION:from ensure-ready;DEFINED:,NO CHANGES:from task-define) are NEVER valid as task-work’s final line. The Step 5a halt path explicitly maps toNEEDS-DEFINITION slug=<basename>(task-work’s verdict, not ensure-ready’s). - AC-2:
plugin/skills/orchestrate/SKILL.mdStep 3 dispatch loop describes a validation step that rejects unrecognized verdict lines and records them asANOMALYin the tick digest (with the raw return string included). - AC-3: Running the orchestrator against a contrived sub-agent that returns
READY: fooproduces anANOMALYdigest entry, not a silent success. (agent-manual: spawn a sub-agent with a prompt that hard-returnsREADY: testand confirm digest behavior.) - AC-4:
plugin/scripts/lint_skill_prose.py plugin/skills/task-work/SKILL.md plugin/skills/orchestrate/SKILL.mdexits 0.plugin/scripts/audit_entities.pyexits 0.
Out of scope
Section titled “Out of scope”- A schema or type system for sub-skill markers. The four task-work verdicts plus a hand-maintained allowlist is enough.
- Catching the inverse confusion (an
/sdlc:pr-checksub-agent returning a task-work verdict). That’s a separate dispatch path with its own contract. - Auto-resumption of half-started worktrees by the orchestrator. The digest’s
ANOMALYline surfaces the state; the human (or a follow-up task) handles the recovery.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Surfaced 2026-05-20 in tick 13 of a /loop /sdlc:orchestrate run. The orchestrator dispatched
task-work for 2026-05-20-task-close-out-verifies-pr-on-main with the standard dispatch prompt
enumerating the four task-work verdicts. The sub-agent ran ensure-ready (Step 5a), got READY:
back, and exited with that line as its own final return — never running Step 5b (the status flip +
start-commit on main), Step 6 (implementation), or beyond. The orchestrator only caught the
misbehavior by checking repo state and noticing the worktree was at the verify-stamp commit with no
follow-on work and no PR. A resumption sub-agent was dispatched with explicit “READY: is
ensure-ready’s marker, not yours” prose to recover.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-21. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
plugin/scripts/lint_skill_prose.py plugin/skills/task-work/SKILL.mdchecks the new"Sub-skill markers are NEVER task-work's final verdict"invariant and theNEEDS-DEFINITION slug=<basename>literal phrase invariant intask-work/invariants.yaml, plus the required H2Final verdict. - AC-2: auto —
plugin/scripts/lint_skill_prose.py plugin/skills/orchestrate/SKILL.mdchecks the newTASK_WORK_VERDICT_RE,ANOMALY: task-work, andINTERMEDIATE signalsinvariants pinned inorchestrate/invariants.yaml. - AC-3: agent-manual — verified the regex in
plugin/skills/orchestrate/SKILL.mdStep 4 rejects every sub-skill marker (READY: foo,NEEDS-DEFINITION: foo,DEFINED: foo,NO CHANGES: foo,ALREADY READY: foo) and accepts all four valid task-work verdicts. Spawning a contrived live sub-agent that hard-returnsREADY: testagainst the actual orchestrator was not run as a separate process in this session — the regex test demonstrates the validation logic the orchestrator implements per the new dispatch-loop prose, which is the load-bearing claim. - AC-4: auto —
lint_skill_prose.py plugin/skills/task-work/SKILL.md plugin/skills/orchestrate/SKILL.mdexits 0;audit_entities.pyexits 0 (pre-existing prose drift on six unrelated closed tasks does not block the audit’s exit code, by design).
What worked
Section titled “What worked”- The invariants-as-tests pattern: pinning the new contract phrases in
invariants.yamlturned AC-1 and AC-2 from agent-manual into auto in a single line each. - The dispatch-prompt extension lands inside the existing fenced prompt block, so the change reads as one unit with the rest of the verdict enumeration.
Friction and automation gaps
Section titled “Friction and automation gaps”- Step 5b’s
start_task.pyhit a rebase conflict on the task file because Step 5a (ensure-ready) bumpedreadiness_verified_at:on the feat branch and Step 5b bumpedlast_reviewed:on main — both edits land on the frontmatter and textually overlap. The skill spec says “do not auto-resolve” but this is the textbook case the sibling task2026-05-20-task-work-rebase-frontmatter-conflictexists to fix. Resolved manually by taking the newer stamps from each side. Once that fix lands,start_task.pyshould auto-resolve frontmatter-only conflicts on the task file (take the later timestamp per field, take the later status per the lifecycle order). → T-H0W9-task-work-rebase-frontmatter-conflict - Step 5b’s contract is ambiguous between “stop and surface” and “auto-resolve” for known-trivial frontmatter collisions. A follow-up task could codify the auto-resolve narrowly to the task-file frontmatter only, leaving body-content conflicts as the genuine surface-to-user case. → T-H0W9-task-work-rebase-frontmatter-conflict
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-H0W9-task-work-rebase-frontmatter-conflict — linked-existing; both friction bullets resolve to this in-progress task.