Skip to content

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.

LocationRole today
plugin/skills/task-work/SKILL.mdStep 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.pyOwns 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.mdThe 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.pyStep 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.

Reorder Step 5 so that the verify-stamp commit lands on top of the start-commit, never under it. The simplest version:

  1. Step 5a is unchanged: runs ensure-ready, which (on success) stamps readiness_verified_at: on a fresh commit on the task branch.
  2. Step 5b is unchanged: commits the status flip + last_reviewed bump on main.
  3. 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-ready against the task file (which now has status: in-progress per the start-commit), which re-stamps readiness_verified_at on a fresh commit on top of the start-commit.

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.

  1. 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 range main..HEAD on the task branch).
  2. Change start_task.py so 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.
  3. Decide and document: should the re-stamp use the same readiness_verified_at ISO as the original Step 5a stamp, or a fresh now() 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.
  4. Update plugin/skills/task-work/SKILL.md Step 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.
  5. Verify the new flow handles the edge case where task-ensure-ready re-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.
LocationKindChange
plugin/scripts/start_task.pymodifyReplace the rebase block with reset-task-branch-to-main-tip + re-invoke ensure-ready stamp.
plugin/skills/task-work/SKILL.mdmodifyStep 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.mdmodifyDocument the re-stamp invocation point (called by start_task.py post-start-commit).
plugin/skills/task-work/invariants.yamlmodifyIf invariants pin “rebase” in Step 5b’s prose, update to match.
  • AC-1: A live /sdlc:task-work <some-basename> run against a task whose initial state is open/ready completes Step 5 end-to-end without any frontmatter conflict surfacing — git log on 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 a readiness_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 sibling test_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.md Step 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.)
  • 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.
  • none

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.


← Back to Tasks