Skip to content

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.md Step 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.md Step 5a invokes /sdlc:task-ensure-ready and its successful return is READY: <basename> on stdout. Step 5a’s prose says “The task is ready to implement iff the final /sdlc:task-ensure-ready call reported READY:” — which makes READY: look like a milestone marker to the sub-agent. Combined with the fact that a sub-agent emitting READY: 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-main sub-agent returned READY: 2026-05-20-task-close-out-verifies-pr-on-main and exited. The orchestrator caught it by reading the actual repo state (worktree present, task on main still open/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.

Three layers of defense:

  1. Skill proseplugin/skills/task-work/SKILL.md adds 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”.
  2. Orchestrator validationplugin/skills/orchestrate/SKILL.md Step 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 an ANOMALY in 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.
  3. 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 like READY: or NEEDS-DEFINITION: from /sdlc:task-ensure-ready are intermediate; you must continue past them through Steps 5b-10 unless explicitly halting per the task-work spec.”
  1. Edit plugin/skills/task-work/SKILL.md to 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 emit NEEDS-DEFINITION slug=<basename> (which is one of the task-work verdicts the orchestrator already accepts), so the sub-agent never has reason to emit READY: as a final line.
  2. Edit plugin/skills/orchestrate/SKILL.md Step 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 as ANOMALY in the digest.
  3. Update plugin/skills/orchestrate/invariants.yaml required_phrases to include a sentinel string from the new validation prose so future skill drift trips the lint.
  4. (Optional, if time) Extend plugin/skills/task-work/invariants.yaml with a forbidden-phrase guard against the sub-agent emitting READY: as a top-level verdict in its own prose.
  • plugin/skills/task-work/SKILL.md — add final-verdict section; reword Step 5a’s halt path to emit NEEDS-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.
  • AC-1: plugin/skills/task-work/SKILL.md contains 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 to NEEDS-DEFINITION slug=<basename> (task-work’s verdict, not ensure-ready’s).
  • AC-2: plugin/skills/orchestrate/SKILL.md Step 3 dispatch loop describes a validation step that rejects unrecognized verdict lines and records them as ANOMALY in the tick digest (with the raw return string included).
  • AC-3: Running the orchestrator against a contrived sub-agent that returns READY: foo produces an ANOMALY digest entry, not a silent success. (agent-manual: spawn a sub-agent with a prompt that hard-returns READY: test and confirm digest behavior.)
  • AC-4: plugin/scripts/lint_skill_prose.py plugin/skills/task-work/SKILL.md plugin/skills/orchestrate/SKILL.md exits 0. plugin/scripts/audit_entities.py exits 0.
  • 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-check sub-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 ANOMALY line surfaces the state; the human (or a follow-up task) handles the recovery.
  • none

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.

Captured by /sdlc:task-work on 2026-05-21. PR: pending.

  • AC-1: auto — plugin/scripts/lint_skill_prose.py plugin/skills/task-work/SKILL.md checks the new "Sub-skill markers are NEVER task-work's final verdict" invariant and the NEEDS-DEFINITION slug=<basename> literal phrase invariant in task-work/invariants.yaml, plus the required H2 Final verdict.
  • AC-2: auto — plugin/scripts/lint_skill_prose.py plugin/skills/orchestrate/SKILL.md checks the new TASK_WORK_VERDICT_RE, ANOMALY: task-work, and INTERMEDIATE signals invariants pinned in orchestrate/invariants.yaml.
  • AC-3: agent-manual — verified the regex in plugin/skills/orchestrate/SKILL.md Step 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-returns READY: test against 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.md exits 0; audit_entities.py exits 0 (pre-existing prose drift on six unrelated closed tasks does not block the audit’s exit code, by design).
  • The invariants-as-tests pattern: pinning the new contract phrases in invariants.yaml turned 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.
  • Step 5b’s start_task.py hit a rebase conflict on the task file because Step 5a (ensure-ready) bumped readiness_verified_at: on the feat branch and Step 5b bumped last_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 task 2026-05-20-task-work-rebase-frontmatter-conflict exists to fix. Resolved manually by taking the newer stamps from each side. Once that fix lands, start_task.py should 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

← Back to Tasks