T-TU0W-rerun-ensure-ready-after-start-commit-to-avoid-frontmatter-conflict
Status: closed/superseded · Impact: high · Complexity: small
Step 5b of /sdlc:task-work produces a frontmatter conflict on
nearly every run that the human has to resolve by hand. The two
commits being rebased touch disjoint frontmatter fields but
they both edit the same --- YAML block in the task file, so
git’s text-based three-way merge surfaces it as a conflict on the
hunk. Resolution is mechanical (“keep both edits”) but it breaks
the unattended-orchestrator promise: every task that goes through
Step 5b needs operator attention mid-flow. PR #125 attempted a
fix; PR #159 (the spawn-task-pr meta task spawned from
promote-pluralize’s post-mortem on 2026-05-28) captures the
regression. This task closes the loop on the proposed solution
that PR #125 didn’t end up landing.
| Location | Role today |
|---|---|
plugin/skills/task-work/SKILL.md | Step 5a runs /sdlc:task-ensure-ready, which (on success) commits readiness_verified_at: '<ISO Z>' onto the task branch in the worktree. Step 5b then commits the status flip + last_reviewed bump on main, then rebases the task branch onto that main-side commit. The rebase replays the verify-stamp commit on top of the start-commit, but both touch the same --- block — git cannot tell that readiness_verified_at and status: are independent YAML fields, so it surfaces a hunk conflict. |
plugin/scripts/start_task.py | Owns the actual rebase invocation Step 5b executes. Where the conflict surfaces. Has no awareness that the conflict is structurally always a disjoint-field one. |
plugin/skills/task-ensure-ready/SKILL.md | The skill that stamps readiness_verified_at as a separate commit on the task branch. Could equally be re-invoked on the post-rebase head; the stamp is idempotent (re-stamping the same value is a no-op, or replaces with a fresh ISO timestamp — either is fine). |
plugin/skills/task-work/check_ancestry.py | Step 9’s helper — irrelevant to this task but cited to anchor that Step 5b’s choice of how to land the start-commit is independent of Step 9’s rebase-onto-origin-main concern. |
Proposed
Section titled “Proposed”Reorder Step 5 so that the verify-stamp commit lands on top of the start-commit, never under it. The simplest version:
- Step 5a is unchanged: runs ensure-ready, which (on success)
stamps
readiness_verified_at:on a fresh commit on the task branch. - Step 5b is unchanged: commits the status flip +
last_reviewedbump onmain. - Step 5b’s rebase changes: instead of replaying the existing
verify-stamp commit (which causes the conflict),
start_task.py:- Resets the task branch to the new main tip (
git reset --hard <new-main-sha>), dropping the verify-stamp commit. - Re-runs
task-ensure-readyagainst the task file (which now hasstatus: in-progressper the start-commit), which re-stampsreadiness_verified_aton a fresh commit on top of the start-commit.
- Resets the task branch to the new main tip (
task-ensure-ready’s “accepts in-progress” behavior (already
shipped — see [T-ZQ4H-task-ensure-ready-accepts-in-progress](/planning/tasks/task-ensure-ready-accepts-in-progress/))
makes this safe: the readiness gate doesn’t refuse a task that’s
already at status: in-progress for the duration of the re-stamp.
Net effect: the start-commit on main is canonical state, the verify-stamp lands on top with no conflict ever, and unattended orchestrator runs no longer need a human to resolve the rebase mid-flow.
Approach
Section titled “Approach”- Audit
plugin/scripts/start_task.py— find the current rebase invocation in Step 5b’s flow. Identify how it currently discovers the verify-stamp commit (likely just by SHA rangemain..HEADon the task branch). - Change
start_task.pyso that after the main-side start-commit lands, it:- Hard-resets the task branch to the new main tip.
- Shells out to the ensure-ready stamp script (whatever Step 5a used) against the worktree, asking it to re-stamp.
- Decide and document: should the re-stamp use the same
readiness_verified_atISO as the original Step 5a stamp, or a freshnow()value? The original is closer to truth (it’s when the gate actually passed); the fresh value is simpler to produce. Pick one and document the choice in the script’s docstring +plugin/skills/task-ensure-ready/SKILL.md. - Update
plugin/skills/task-work/SKILL.mdStep 5b prose to describe the new behavior — the rebase isn’t really a rebase anymore, it’s a reset + re-stamp. The Step header may need renaming. - Verify the new flow handles the edge case where
task-ensure-readyre-fails after the status flip (extremely unlikely but possible if the start-commit somehow invalidates readiness). Surface a clear stderr message and exit non-zero in that case so the operator knows.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/scripts/start_task.py | modify | Replace the rebase block with reset-task-branch-to-main-tip + re-invoke ensure-ready stamp. |
plugin/skills/task-work/SKILL.md | modify | Step 5b prose updated to describe the new reset+re-stamp flow; the word “rebase” replaced where it no longer fits. |
plugin/skills/task-ensure-ready/SKILL.md | modify | Document the re-stamp invocation point (called by start_task.py post-start-commit). |
plugin/skills/task-work/invariants.yaml | modify | If invariants pin “rebase” in Step 5b’s prose, update to match. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: A live
/sdlc:task-work <some-basename>run against a task whose initial state isopen/readycompletes Step 5 end-to-end without any frontmatter conflict surfacing —git logon the resulting task branch shows the start-commit then the verify-stamp commit, in that order, with no conflict-resolution commit in between. - AC-2: The task file on the task branch (post-Step 5) carries
BOTH
status: in-progress(from the start-commit) AND areadiness_verified_at: '<ISO Z>'field — the re-stamp lands on top of the status flip without dropping either. - AC-3: A unit/integration test under
plugin/scripts/tests/(or siblingtest_start_task.py) exercises the reset+re-stamp path against a temp git repo and asserts both the resulting commit order and the final frontmatter shape. - AC-4:
plugin/skills/task-work/SKILL.mdStep 5b’s prose no longer describes a rebase, and the new reset+re-stamp behavior is documented clearly enough that a future implementer can reproduce it from the prose alone. - AC-5: A subsequent orchestrator run that dispatches 3+ task-work sub-agents in parallel sees zero conflict-resolution steps in any of their post-mortems. (Validated by running the loop against a small batch of ready tasks.)
Out of scope
Section titled “Out of scope”- Generalizing to a YAML-aware three-way frontmatter merger. That’s the alternative fix path; reset+re-stamp is simpler and sufficient. If a future task ever needs to merge disjoint frontmatter edits in a context where reset is not viable, it can be opened as a follow-up.
- Refactoring start_task.py’s other responsibilities (stash handling, orphan-stash detection, etc.). One concern per task.
- Recovering the two stuck tasks on 2026-05-28
(
promote-pluralize-to-shared-helper,lefthook-project-check-pre-commit) — that recovery was done manually outside this task.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Surfaced (again) on 2026-05-28 during the post-mortem of
[T-8XI6-promote-pluralize-to-shared-helper](/planning/tasks/promote-pluralize-to-shared-helper/). The promote-pluralize
sub-agent’s Friction list explicitly flagged the issue:
Step 5b’s start_task.py rebase produced a frontmatter conflict between the main-side start-commit (status flip + last_reviewed bump) and the task-branch verify-stamp commit (readiness_verified_at stamp). The two commits touch disjoint frontmatter fields but land on the same
---block; git can’t auto-merge YAML field-by-field. Resolution was trivial (keep both edits) but had to be done by hand. — start_task.py could either: (a) re-run ensure-ready AFTER the main-side start-commit (so the stamp lands on top, no conflict), or (b) embed a YAML-aware three-way frontmatter merger for the canonical case of disjoint field edits.
This task takes path (a). PR #125 originally tried to fix this class of bug; the regression that landed PR #159 (the meta-task PR scaffolded by spawn-task-pr from the promote-pluralize post-mortem) confirms the prior fix didn’t fully close the gap. Replacing the rebase with a reset+re-stamp removes the conflict class entirely — there’s no two-commit merge to do — rather than trying to teach a merge tool about YAML field independence.