T-Y0SQ-task-work-emit-completion-marker
Status: closed/done · Impact: high · Complexity: small
Auto-generated from a /sdlc:task-work post-mortem. Review and
promote to ready before picking up.
When an orchestrator dispatches /sdlc:task-work to a sub-agent, the
sub-agent has no unambiguous signal for “the whole flow finished”. It
sees READY: <basename> emitted by /sdlc:task-ensure-ready in Step
5a and treats that as the end of the run, even though Steps 5b through
10 still need to execute. This has happened twice on
T-ZQ4H-task-ensure-ready-accepts-in-progress alone — the sub-
agent returned early both times. Closing this gap means task-work
emits one final stdout marker whose absence is unambiguous evidence the
sub-agent stopped early, so orchestrators can detect partial runs
automatically.
plugin/skills/task-work/SKILL.md ends with Step 10 (“Open the PR”)
followed by Step 11 (“After the PR merges — close the loop”) and Step
12 (“Failure modes”). None of those steps prescribe a single final
stdout line. Sub-agents that quote stdout markers back to their parent
have no way to distinguish “I finished” from “I returned in the
middle”. The intermediate READY: from ensure-ready (Step 5a) looks
plausibly terminal because it’s the first stdout marker in the flow.
Proposed
Section titled “Proposed”/sdlc:task-work emits a single, distinctive marker as its last
stdout line — e.g. TASK-WORK COMPLETE: <basename> PR=<url> — at the
successful end of Step 10. Orchestrators check for this marker before
trusting the sub-agent’s report. Absence of the marker means the
sub-agent stopped early; the orchestrator can re-dispatch with a
“continue from
Approach
Section titled “Approach”- Add a “Final marker” subsection at the end of
plugin/skills/task- work/SKILL.mdStep 10 specifying the exact stdout line. - Pin the literal marker text in
plugin/skills/task-work/invariants. yamlsolint_skill_prose.pyenforces its presence. - Add a note to Step 5a (
/sdlc:task-ensure-readyreturnsREADY:) explicitly saying “this is NOT the end of the flow — continue to Step 5b” so future sub-agent readings don’t conflate the two markers.
Files to touch
Section titled “Files to touch”plugin/skills/task-work/SKILL.md— add the final-marker section and the early-exit warning.plugin/skills/task-work/invariants.yaml— pin the marker phrase.
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
/sdlc:task-work’s SKILL.md prescribes an exact final stdout line at the end of Step 10. - AC-2:
lint_skill_prose.pyenforces that exact phrase as a required-phrase invariant. - AC-3: Step 5a’s prose names the
READY:marker explicitly as “intermediate, not terminal”.
Out of scope
Section titled “Out of scope”- Wiring the marker through the Agent-tool sub-agent harness — that’s a separate orchestration change.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:task-work post-mortem of T-ZQ4H-task-ensure-ready-accepts-in-progress on 2026-05-19.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-20. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto — the new “Final marker” subsection in Step 10 contains
the literal line
TASK-WORK COMPLETE: <basename> PR=<pr-url>inside a fenced code block, andlint_skill_prose.pyconfirms its presence as a required phrase pinned to Step 10. - AC-2: auto —
plugin/scripts/lint_skill_prose.pyrun againstplugin/skills/task-work/SKILL.mdreportsno violations. The invariants.yaml now pins bothTASK-WORK COMPLETE: <basename> PR=<pr-url>(section “10.”) andintermediate, not terminal(section “5a.”). - AC-3: auto — Step 5a now contains a bolded paragraph that names
READY:explicitly as “intermediate, not terminal” and tells sub-agents an orchestrator should re-dispatch when only the intermediate marker is seen. The phrase is lint-pinned to Step 5a.
What worked
Section titled “What worked”- The existing
invariants.yaml+lint_skill_prose.pymachinery made AC-2 trivially mechanical: add one required-phrase entry per ACs that asks for literal prose, and the linter is the verification. - The task spec named the exact marker text it wanted, so there was no design decision to defer.
Friction and automation gaps
Section titled “Friction and automation gaps”- The skill prompt rendered to the runner contained
just full-check/just cireferences at Step 7, but the live SKILL.md file already uses${CLAUDE_PLUGIN_ROOT}scripts/run_quality_checks.py. The discrepancy cost one trip through “what does this project actually use?” The gap is upstream of this task: the skill prompt delivered to a child invocation should reflect the file’s current state, not a snapshot. Worth a follow-up to investigate skill-prompt freshness when /sdlc:task-work is invoked from another skill — does the Skill tool re-read the file each call, or cache? →2026-05-21-investigate-skill-prompt-freshness - The post-mortem template instructs the runner to spawn a sub-agent
for follow-up tasks even when gaps are
- none observed. For a one- or two-line gap list this is overhead — the spawn-from-post-mortem procedure already supports the no-op case, but in environments where the Agent/Task tool isn’t available (this run had no such tool exposed) the runner has to inline-execute the procedure manually. Worth an inline fast-path when the friction list is empty or trivially short, plus an explicit fallback contract for when the Agent tool isn’t available. → T-TZD2-task-work-spawn-fast-path-no-op
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-TZD2-task-work-spawn-fast-path-no-op — inline fast-path for empty post-mortem friction lists, plus documented fallback when the Agent tool isn’t available (created).
2026-05-21-investigate-skill-prompt-freshness— investigate whether sub-skill prompt delivery re-reads SKILL.md each call or caches stale prose (created).