Skip to content

T-2WF7-entities-migrate-route-via-inflight-prs

Status: closed/superseded · Impact: medium · Complexity: small

Today, /sdlc:entities-migrate is blind to in-flight PRs: it always opens a new PR from main carrying the mechanical schema bumps, even when the drifting entity file is already being modified by an open task PR. That guarantees the two PRs collide on the same frontmatter and forces a manual rebase on whichever merges second. The proposed redesign routes the bump onto the existing PR’s task branch when one exists (via the same lease re-acquire dance /sdlc:pr-respond already uses), and falls back to the current new-PR behaviour only for drifting files not touched by any open SDLC PR.

Originating from 2026-05-27-ci-workflow-overhaul in template-tauri-nuxt:

When /sdlc:setup ran the audit after a task-work PR was already open against awaiting-review, the audit reported auto_fixable=true drift (task schema v3 → v4) on the task file the PR was modifying. Running /sdlc:entities-migrate then would have opened a second PR mutating the same frontmatter — guaranteed merge conflict. The cleanest fix that doesn’t exist yet is for entities-migrate to detect the in-flight PR and push the bump onto its branch through the existing lease coordination.

LocationRole today
plugin/skills/entities-migrate/SKILL.mdThe skill spec. Step 3 unconditionally creates one worktree off main (git worktree add .sdlc/worktrees/entities-migrate -b chore/entities-migrate main); Step 6 unconditionally opens one PR off that branch. No PR discovery anywhere.
plugin/skills/entities-migrate/migrate_entities.tsThe deterministic tail. Pulls the audit report, applies each auto_fixable=true fix to files under docs/planning/<plural>/, prints a summary. Operates on whatever project root it is pointed at; has no concept of “this file is also open in a PR.”
plugin/skills/pr-respond/SKILL.mdThe lease re-acquire dance this task reuses: Step 1 parse-footer, Step 2 cd into .sdlc/worktrees/<task_id>, Step 3 fencing check (inspect lease, compare lease_id), Step 4 task reacquire --phase responding, Step 5 heartbeat, Step 8 git push, Step 9 task transition --phase awaiting-review.
plugin/scripts/sdlc_lease.tsThe lease CLI. parse-footer <pr>, inspect <ref> --json, task reacquire <id> --phase <p>, task transition <id> --phase <p> --handoff-* are the subcommands the routed path calls — all already exist (pr-respond uses every one).
plugin/skills/pr-check/SKILL.mdDefines the open/closed PR verdict semantics the routed path mirrors when deciding which PRs are eligible targets (skip CLOSED/MERGED, same as pr-respond’s caller).

Split /sdlc:entities-migrate’s post-audit step into a per-drifting-file routing decision instead of one monolithic worktree+PR. For each file the audit flagged auto_fixable=true, ask: is this file modified by an open SDLC PR right now? If yes, push the bump onto that PR’s task branch through the lease protocol pr-respond already implements. If no, the file flows through the existing new-PR codepath unchanged. The change is almost entirely in SKILL.md prose (the skill’s procedure) plus a thin PR-discovery helper; the mechanical fix logic in migrate_entities.ts is reused verbatim — it already takes a project root, so pointing it at an in-flight worktree instead of the dedicated entities-migrate worktree requires no new fix code.

  1. Discover in-flight SDLC PRs once, up front. After Step 2’s audit dry-run produces the set of auto_fixable=true drifting files, list open PRs via gh pr list --state open --json number,headRefName,body,files. Keep only PRs whose body carries a parseable lease footer — reuse bun run ${CLAUDE_PLUGIN_ROOT}scripts/sdlc_lease.ts parse-footer <pr> (exit 0 = footer present, exit 5 = no footer → skip the PR). Build a map drifting_file → owning_pr by intersecting each PR’s files list with the drifting set. A file appears in at most one map entry (a file open in two PRs is out of scope — see Out of scope).
  2. Partition the drifting set into routed (files with an owning PR) and residual (files with no owning PR).
  3. For each owning PR in routed, run the lease dance exactly as /sdlc:pr-respond Steps 1–9, substituting the bump for the response work: a. cd .sdlc/worktrees/<task_id> (the worktree task-work created; if absent emit WORKTREE-MISSING task=<task_id> and demote that PR’s files to residual). b. Fencing check: sdlc_lease.ts inspect refs/sdlc/tasks/<task_id> --json, compare its lease_id against the footer’s. On mismatch, emit LEASE-FENCING-MISMATCH and demote the files to residual (do not steal a rotated lease). c. sdlc_lease.ts task reacquire <task_id> --phase responding (reuse the existing responding phase — do NOT add a new migrating phase; see Notes). On exit 4 (LEASE-CONFLICT) skip this PR, leave its files unrouted, and report the conflict; another worker holds it. d. Apply the bump in the worktree: run bun run ${CLAUDE_PLUGIN_ROOT}skills/entities-migrate/migrate_entities.ts --project-root .sdlc/worktrees/<task_id> --type <type> restricted to the owned file’s type. migrate_entities.ts is reused unchanged. e. Commit on task/<task_id> with subject chore(entities): apply auto-fixable schema drift (use the mktemp + quoted-heredoc + git commit -F pattern per plugin/conventions/commit-messages.md); git push. f. sdlc_lease.ts task transition <task_id> --phase awaiting-review with the five --handoff-* flags, mirroring pr-respond Step 9. Post a one-line self-comment via ${CLAUDE_PLUGIN_ROOT}skills/pr-check/post_self_comment.sh <pr> <body> so the PR author sees the bump landed and the pr-check cursor does not re-fire NEEDS-RESPONSE on our own push.
  4. For residual, run the existing codepath verbatim: create the chore/entities-migrate worktree off main (Step 3), migrate only the residual files, open one PR (Step 6). If residual is empty, skip the worktree and PR entirely.
  5. Document the heartbeat pairing (pr-respond Steps 5/7): start lease_heartbeat_loop.ts before the sub-step-3d migrate and always stop it after, so a slow bump does not let the lease expire mid-push.
  6. Update the skill’s reference block and Notes to cite plugin/conventions/lease-aware-skills.md (the shared re-acquire pattern) rather than re-inlining pr-respond’s prose, per the no-duplicate-prose rule in plugin/skills/CLAUDE.md.
LocationKindChange
plugin/skills/entities-migrate/SKILL.mdmodifyInsert the per-file routing decision between Step 2 (audit dry-run) and Step 3 (worktree). Add the lease-dance subsection (mirroring pr-respond Steps 1–9 by reference, not by copy), the routed/residual partition, the per-PR fencing + reacquire + push + transition + self-comment flow, and the heartbeat pairing. Make Step 3/6 (worktree + new PR) conditional on a non-empty residual set.
plugin/skills/entities-migrate/invariants.yamlmodifyAdd invariants pinning the new behaviour: the SKILL.md must reference parse-footer, task reacquire, the responding phase, post_self_comment.sh, and lease-aware-skills.md; and must NOT introduce a new migrating lease phase.
plugin/conventions/lease-aware-skills.mdmodifyAdd /sdlc:entities-migrate to the list of skills that adopt the re-acquire-into-responding pattern, so the shared convention names its new consumer.
  • AC-1: plugin/skills/entities-migrate/SKILL.md contains a step that, before creating the chore/entities-migrate worktree, partitions the audit’s auto_fixable=true files into a routed set (files modified by an open lease-footer-bearing PR) and a residual set, using gh pr list ... --json number,headRefName,body,files and sdlc_lease.ts parse-footer.
  • AC-2: For the routed set, the SKILL.md prescribes the pr-respond lease sequence by reference — parse-footercd worktree → inspect fencing check → task reacquire --phase responding → migrate-in-worktree → commit on task/<task_id>git pushtask transition --phase awaiting-review — and explicitly states no second PR is opened for a routed file.
  • AC-3: The SKILL.md states the residual codepath is the unchanged Step 3/6 (worktree off main + one PR), and that the worktree and PR are skipped entirely when the residual set is empty.
  • AC-4: The SKILL.md prescribes CAS-guarded coordination identical to pr-respond: on LEASE-CONFLICT (reacquire exit 4) the PR is skipped and its files left unrouted; on LEASE-FENCING-MISMATCH the lease is not stolen and the files are demoted to residual.
  • AC-5: The routed path reuses migrate_entities.ts with --project-root pointed at the in-flight worktree and reuses the responding lease phase; the SKILL.md introduces no new fix logic and no new lease phase (verifiable: no migrating phase token appears in SKILL.md or sdlc_lease.ts).
  • AC-6: plugin/skills/entities-migrate/invariants.yaml declares the new invariants and bun run .claude/skills/project-check/check_skill_prose.ts passes against the updated SKILL.md + invariants pair.
  • AC-7: plugin/conventions/lease-aware-skills.md lists /sdlc:entities-migrate as a re-acquire-pattern consumer, and the SKILL.md references that convention doc rather than inlining pr-respond’s prose (no verbatim re-copy of the lease steps).
  • Bumping auto_fixable=false drift onto in-flight PRs — only mechanical fixes auto-route; ambiguous drift still surfaces for human review in the residual PR’s “Manual review needed” section.
  • Closed-but-not-merged and merged PRs — skipped, same as /sdlc:pr-check’s action semantics.
  • A drifting file modified by two open PRs simultaneously — left in the residual set and reported, not split across both branches.
  • Adding a new migrating lease phase — the existing responding phase is reused (a bump is a flavour of automated response to drift; a parallel phase would fork the lease state machine for no protocol gain).
  • Changing migrate_entities.ts’s fix logic — it is reused verbatim via --project-root.
  • none

Spawned by /sdlc:spawn-task-pr on 2026-05-28 UTC from 2026-05-27-ci-workflow-overhaul in https://github.com/sksizer/template-tauri-nuxt. The originating task’s /sdlc:setup post-merge re-audit surfaced auto-fixable schema drift on the task file that the originating PR was itself modifying, which made the gap in entities-migrate’s design visible.


← Back to Tasks