T-LWEA-rename-spawn-task-pr-marker-to-break-done-pr-collision
Status: closed/superseded · Impact: high · Complexity: small
spawn-task-pr and task-work both emit terminal markers that begin
with DONE pr=. When task-work Step 8 (post-mortem) dispatches
spawn-from-post-mortem, which transitively calls spawn-task-pr
per friction bullet, the inner skill’s DONE pr=<url> target=... branch=meta-task/...
output bubbles up to the outer task-work LLM sub-agent. The outer
agent — being an LLM eyeballing terminal markers, not a strict
parser — treats it as task-work’s own DONE pr=#<N> final verdict
and short-circuits, skipping Step 9 (sync with origin/main) and
Step 10 (open the task’s own implementation PR). The task’s
implementation commits and post-mortem commit land on the
task/<basename> branch correctly, but the branch is never pushed
and no implementation PR is opened — only the spawn-task-pr’s
meta-task/<slug> PR. The first observed instances of this on
2026-05-28 left two tasks (promote-pluralize-to-shared-helper and
lefthook-project-check-pre-commit) stuck in status: in-progress
with implementation commits on their branches but no PR.
| Location | Role today |
|---|---|
plugin/skills/spawn-task-pr/SKILL.md | Output contract section documents the success marker as DONE pr=<pr-url> target=<owner/name> branch=meta-task/<slug>. Same marker is the terminal stdout line at “Step 8. Report” (line ~269). |
plugin/skills/spawn-task-pr/invariants.yaml | Machine-readable invariants pin the marker text and shape; needs to update in lockstep with the SKILL.md change. |
plugin/skills/task-work/SKILL.md | Documents DONE pr=#<N> as one of the four task-work final verdicts (see “Final verdict — the only four task-work markers” section, ~line 948). Outer task-work sub-agents return this to the orchestrator. |
plugin/skills/orchestrate/SKILL.md | Step 4 dispatch contract includes the task-work verdict allowlist regex `^(DONE pr=#\d+ |
plugin/entities/task/spawn-from-post-mortem.md | The post-mortem procedure dispatched by task-work Step 8. Its “Commit and report” step (step 7) composes the SPAWNED-LOCAL/SPAWNED-CROSS-REPO/LINKED-EXISTING/SKIPPED/PATHS report that aggregates the results of per-bullet spawn-task-pr calls. Anywhere it references spawn-task-pr’s marker by literal text needs updating. |
Proposed
Section titled “Proposed”spawn-task-pr emits a terminal success marker that does NOT share
a prefix with task-work’s DONE pr=#<N>. The replacement should
keep the same semantic shape (one line, prefix + key=value pairs)
and remain greppable. Proposed shape:
SPAWNED-PR pr=<pr-url> target=<owner/name> branch=meta-task/<slug>The SPAWNED-PR prefix matches the existing SPAWNED-LOCAL /
SPAWNED-CROSS-REPO family used by spawn-from-post-mortem and is
mutually distinct from any task-work terminal marker. Every
consumer of spawn-task-pr’s output (currently
plugin/entities/task/spawn-from-post-mortem.md step 4b’s per-bullet
dispatch result handling, plus its step 7 report aggregator) is
updated to read the new marker.
After this task ships, task-work Step 8 → spawn-from-post-mortem →
spawn-task-pr no longer surfaces a DONE pr= line to the outer
task-work LLM sub-agent, and the outer agent reliably proceeds to
Step 9 + Step 10 to push the task branch and open the
implementation PR.
Approach
Section titled “Approach”- Update
plugin/skills/spawn-task-pr/SKILL.md: rename the marker fromDONE pr=...toSPAWNED-PR pr=...in the “Output contract” section and at “Step 8. Report”. TheEXISTING-PR pr=<url>marker (idempotency hit) andERROR reason="..."markers do not collide and stay as-is. - Update
plugin/skills/spawn-task-pr/invariants.yamlto pin the new marker text (and remove the old). - Update
plugin/entities/task/spawn-from-post-mortem.mdeverywhere it references the oldDONE pr=marker as the per-bullet success shape — step 4b’s dispatch result handling, step 5’s brief, step 7’s report aggregator. The SPAWNED-LOCAL / SPAWNED-CROSS-REPO counts in the final report continue to countSPAWNED-PRreturns; the classification (Local vs Upstream-plugin vs Cross-project-request) is what drives which bucket a result lands in, not the marker text itself. - Add an invariants entry to
plugin/skills/task-work/invariants.yaml(if one exists; otherwise scope this AC to documentation only) asserting that task-work’sDONE pr=final verdict shape does NOT collide with any sub-skill it transitively invokes. The assertion is a regex grep acrossplugin/skills/*/SKILL.mdandplugin/entities/*/*.mdfor^DONE pr=lines outside task-work’s own SKILL.md — must return zero hits. - Land a regression doc note in
plugin/skills/task-work/SKILL.mdnear the Step 8 sub-agent dispatch section explaining the marker-collision class of bug (one paragraph, “if you add a new sub-skill that emits aDONEmarker, do not let it share a prefix with task-work’s terminal verdicts”).
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/skills/spawn-task-pr/SKILL.md | modify | Rename terminal success marker DONE pr=... → SPAWNED-PR pr=... in Output contract section and Step 8 (Report). |
plugin/skills/spawn-task-pr/invariants.yaml | modify | Update pinned marker text to match. |
plugin/entities/task/spawn-from-post-mortem.md | modify | Update step 4b / step 5 / step 7 references to the per-bullet success marker from DONE pr=... to SPAWNED-PR pr=.... |
plugin/skills/task-work/invariants.yaml | modify | Add a marker-collision invariant (regex grep across SKILL.md and entity docs for stray ^DONE pr= lines outside task-work’s own SKILL.md). |
plugin/skills/task-work/SKILL.md | modify | Append a one-paragraph note near Step 8 dispatch warning future skill-authors not to share marker prefixes with task-work’s terminal verdicts. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
grep -rE '^DONE pr=' plugin/skills/ plugin/entities/returns hits ONLY inplugin/skills/task-work/SKILL.md. No matches inplugin/skills/spawn-task-pr/SKILL.md,plugin/skills/spawn-task-pr/invariants.yaml, orplugin/entities/task/spawn-from-post-mortem.md. - AC-2:
grep -rE '^SPAWNED-PR pr=' plugin/skills/ plugin/entities/returns at least one hit inplugin/skills/spawn-task-pr/SKILL.mdand at least one inplugin/entities/task/spawn-from-post-mortem.md. - AC-3: A subsequent end-to-end
/sdlc:task-workrun that generates at least one Local friction bullet (and therefore triggers spawn-task-pr) successfully proceeds through Step 9 + Step 10 and opens its owntask/<basename>implementation PR. Manually verified by picking a small ready task and watching the orchestrator dispatch run. - AC-4:
plugin/skills/task-work/invariants.yamlcarries a marker-collision check (or the equivalent assertion inplugin/scripts/lint_skill_prose.py) that future PRs adding new sub-skills withDONE pr=markers fail.
Out of scope
Section titled “Out of scope”- Recovering the two specific stuck tasks
(
2026-05-21-promote-pluralize-to-shared-helperand2026-05-21-lefthook-project-check-pre-commit) — that recovery is being done manually outside this task (push branches, open PRs, transition leases). - Renaming the spawn-task-pr skill itself or restructuring the post-mortem flow. This is a marker-text fix; the architecture is fine.
- Audit of every other skill for similar marker-prefix collisions beyond what AC-4’s check covers.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Surfaced on 2026-05-28 during the first live multi-task orchestrator
tick. Two of three task-work runs that produced friction-bullet
post-mortems landed implementation commits on their task branches
but never pushed/opened a PR — they reported the spawn-task-pr meta
PR as their DONE pr= verdict and exited. The third successful run
in the same tick (extract-phrase-in-prose-shared-helper) didn’t
trip the bug because its post-mortem had no Local-classified
friction bullets, so spawn-task-pr never ran. Root cause confirmed
by inspecting the worktree commit histories of the two stuck tasks
plus the spawn-task-pr/SKILL.md:269 vs task-work/SKILL.md:916
marker contracts. The renamed-to-spawn-task-pr refactor (commit
4b84272) introduced the collision by reusing the DONE pr=
prefix on a now-LLM-visible inner marker.