T-DNN5-add-sdlc-lease-migrate-and-control-plane-bootstrap
Status: closed/done · Impact: high · Complexity: large
Ship the one-shot migration command that converts this repo from frontmatter-as-claim to
lease-as-claim. Without this command, slice 2’s lease-aware skills (/sdlc:orchestrate,
/sdlc:task-work, /sdlc:pr-respond, /sdlc:task-close-out) refuse to operate because
refs/sdlc/control-plane does not exist on the configured authority — that ref is the kill-switch
gate, and the migration command is the only path that creates it. This task lands the bootstrap step
- the in-flight task wrapping logic + an audit log, all as a single deterministic, idempotent run.
| Location | Role today |
|---|---|
plugin/lib/lease/control_plane.py | Read-only: fetch_and_validate_control_plane raises ControlPlaneMissing if refs/sdlc/control-plane is absent. No write path exists. |
plugin/scripts/sdlc_lease.py | Top-level CLI dispatcher exposing task claim/acquire/reacquire/transition/archive, op claim, heartbeat, release, inspect, list, parse-footer. No migrate subcommand. |
plugin/scripts/lease_cli/ | Holds per-subcommand handlers. No migrate.py. |
docs/planning/tasks/*.md | Multiple tasks currently carry status: in-progress from pre-lease work — these need to be wrapped in synthetic leases at cutover or the new dispatcher cannot see them. |
docs/planning/decisions/github-ref-leases/protocol.md (section “Migration of in-flight tasks at cutover”, lines 1303–1374) | Canonical 8-step procedure + 3 edge cases that this task implements verbatim. |
After slice 2’s cutover PR (#127) lands, the lease-aware skills become the only code path but fail-closed at the control-plane gate. This task is the unblock.
Proposed
Section titled “Proposed”Ship sdlc lease migrate as a top-level subcommand that:
- Initializes
refs/sdlc/control-planevia CAS-CREATE with aControlPlanepayload carrying the localsdlc_version. Idempotent-by-construction: re-running fails the CAS-CREATE cleanly. - Discovers all tasks at
status: in-progressfromdocs/planning/tasks/*.md. - Derives initial lease phase per the ADR’s three-case decision tree (open PR →
awaiting-review; work-branch-only →working; neither → flag for operator review, do not auto-migrate). - Mints a fresh
lease_id+lease_tokenfor each migrated task using a sentinelhost_idofmigrated-pre-cutoverso reconcile can identify migration-origin leases later. - CAS-CREATEs
refs/sdlc/tasks/<id>per task; existing refs are skipped with a structured warning (re-run safety). - Stamps open PR bodies with the canonical
<!-- sdlc-lease: task=<id> lease=<lease_id> -->footer viagh pr edit. - Writes placeholder
handoff.mdinto lease commits forawaiting-reviewtasks (content:migrated; see PR description). - Records every action in
.sdlc/migration.log(one JSON line per task) for operator review and reconcile reconciliation.
Multiple-open-PR-per-task halts before any writes. Tasks with in-progress but no branch and no PR
are logged as stale-requires-review and skipped.
Approach
Section titled “Approach”- Add
init_control_plane(authority, *, sdlc_version, cwd=None)toplugin/lib/lease/control_plane.py. Wrapscas_createonrefs/sdlc/control-planewith a validatedControlPlanepayload. Returns the new ref SHA on success; raisesCASFailedif the ref already exists. - Add
discover_inflight_tasks(project_root)to a newplugin/lib/lease/migration.pymodule. Returns a list ofInflightTaskrecords:{task_id, file_path, status, branch_exists, pr_number, pr_state}. PR state viagh pr list --search "<task_id>" --state open,merged --json number,state. Branch state viagit branch --list "task/<task_id>" "feat/<task_id>". - Add
derive_initial_phase(record)in the same module. Returns one of:awaiting-review(open PR exists),working(branch-only), orstale-requires-review(neither — caller skips this task and logs the flag). - Add
mint_migration_lease(task_id, phase, pr_number=None)in the same module. Returns aTaskLifecycleLeasepayload withhost_id = "migrated-pre-cutover", freshlease_id/lease_tokenUUIDv4s, andexpires_atset tonow + TTLforworkingphase ornullforawaiting-reviewphase. - Build
plugin/scripts/lease_cli/migrate.pyas a non-shebanged module imported bysdlc_lease.py. The handler runs the full 8-step procedure inside a single transaction-shaped flow:- Step A: Probe
refs/sdlc/control-plane— if it exists, abort withMIGRATION-ALREADY-RUN authority=<authority>and exit 1. - Step B: Run
discover_inflight_tasks→ halt-on-error for multi-PR-per-task (MULTIPLE-PRS-FOR-TASK task=<id> prs=[...]). - Step C: For each task, derive phase → mint lease →
cas_createref. Record outcome. - Step D: For
awaiting-reviewtasks, write placeholderhandoff.mdviabuild_lease_commit(handoff_md=...)andcas_replacethe freshly-created ref. - Step E: For tasks with open PRs, append footer to PR body via
gh pr edit <num> --body "$(gh pr view <num> --json body -q .body)$'\n\n<!-- sdlc-lease: ... -->'". - Step F: Call
init_control_planeto seedrefs/sdlc/control-plane. - Step G: Write
.sdlc/migration.log— one JSON line per task with{task_id, lease_id, phase_assigned, action_taken, pr_stamped, handoff_written}.
- Step A: Probe
- Wire
migrateintosdlc_lease.pytop-level argparse dispatch. Surface assdlc lease migrate [--dry-run].--dry-runruns steps A–E in report-only mode (nocas_create, nocas_replace, nogh pr edit, no log write) and emits what WOULD happen. - Tests in
plugin/scripts/lease_cli/tests/test_migrate.pyagainst the local-bare-repo authority fixture from slice 1. Cover: fresh migration, idempotency (second run aborts cleanly), the three phase-derivation cases, multiple-PR halt, dry-run output, and synthetic host_id presence in lease commits. - Document the subcommand in
plugin/scripts/lease_cli/README.mdwith the signature, exit codes, and a worked example. Add a one-paragraph callout underplugin/conventions/lease-aware-skills.mdpointing to the migrate command as the only path to a working lease-aware setup.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/lib/lease/control_plane.py | modify | Add init_control_plane(authority, *, sdlc_version, cwd=None) write path. |
plugin/lib/lease/migration.py | new | discover_inflight_tasks, derive_initial_phase, mint_migration_lease, and shared InflightTask dataclass. |
plugin/lib/lease/__init__.py | modify | Re-export init_control_plane, discover_inflight_tasks, mint_migration_lease. |
plugin/scripts/lease_cli/migrate.py | new | CLI handler implementing the 8-step procedure. |
plugin/scripts/sdlc_lease.py | modify | Add migrate to top-level argparse dispatch + module docstring. |
plugin/scripts/lease_cli/tests/test_migrate.py | new | Unit + integration tests against the local-bare-repo authority fixture. |
plugin/lib/lease/tests/test_migration.py | new | Library-level tests for the four new library helpers. |
plugin/scripts/lease_cli/README.md | modify | Document the migrate subcommand contract. |
plugin/conventions/lease-aware-skills.md | modify | Add the “running the cutover migration” section. |
Acceptance criteria
Section titled “Acceptance criteria”-
AC-1: Running
sdlc lease migrateagainst a fresh repo (norefs/sdlc/control-plane) creates the control-plane ref on origin with the localsdlc_version. Verified bygit ls-remote origin refs/sdlc/control-planereturning a non-empty SHA. -
AC-2: For each task at
status: in-progressat migration time, arefs/sdlc/tasks/<id>ref exists post-migration with phase derived per the ADR’s three-case decision tree. Verified by per-case test fixtures. -
AC-3: Open PRs for migrated tasks have their bodies updated to end with
<!-- sdlc-lease: task=<id> lease=<lease_id> -->. Verified viagh pr view <num> --json body -q .body. -
AC-4:
awaiting-reviewleases contain ahandoff.mdblob in their commit tree with placeholder content. Verified bygit show <ref>:handoff.mdreturning the placeholder. -
AC-5:
.sdlc/migration.logexists after a successful run with one JSON line per processed task carryingtask_id,lease_id,phase_assigned,action_taken. -
AC-6: Re-running
sdlc lease migrateagainst a migrated repo exits non-zero withMIGRATION-ALREADY-RUN authority=<authority>on stderr; no refs or PR bodies are mutated. -
AC-7: Tasks with
status: in-progress+ no branch + no PR are logged asstale-requires-reviewand NOT auto-migrated; the run continues for other tasks. -
AC-8: Multiple open PRs for the same task cause the command to halt with
MULTIPLE-PRS-FOR-TASK task=<id> prs=[...]BEFORE any state is written (control-plane ref not created, no leases minted, no PRs edited). -
AC-9: After running
sdlc lease migratesuccessfully,/sdlc:orchestrateproceeds past the control-plane gate on its next tick (noCONTROL-PLANE-MISSINGmarker). Verified by running orchestrate in a test repo after migration. -
AC-10: All migration-origin leases carry
host_id = "migrated-pre-cutover"— visible ingit log refs/sdlc/tasks/<id> --format=%Bfor any migrated task’s lease history. -
AC-11:
sdlc lease migrate --dry-runreports what would happen without mutating any state (no refs created, no PR bodies edited, no log written). Verified by comparinggit ls-remoteandgh pr viewoutput before/after. -
AC-12: All tests pass:
uv run --with pytest --with pyyaml --with pydantic python -m pytest plugin/lib/lease/tests/test_migration.py plugin/scripts/lease_cli/tests/test_migrate.pyQuality checks pass:
plugin/scripts/run_quality_checks.py --config sdlc.yaml.
Out of scope
Section titled “Out of scope”sdlc reconcile— separate slice-3 task (T-Y1JN-add-sdlc-reconcile-reporter). Migrate runs first because it’s the critical-path unblocker; reconcile follows to verify.--fixmodes for reconcile — slice 3 ships reconcile as read-only only.- Rolling-back a migration — out of scope; migrate is one-way by design (per ADR: “running it after cutover is a bug, not a feature”).
- Offline-mode behavior — slice 4 territory.
- Operation leases for
backlog-triage,reconcile,import-planning— slice 4 territory.
Dependencies
Section titled “Dependencies”- E0002 slice 2 cutover PR (#127) must merge first. Migrate uses slice 2’s
build_lease_commit(handoff_md=...)helper and assumes the lease-aware skills on main need the control-plane bootstrap to function. - T-S0PK-add-lease-protocol-library-and-schemas — provides
ControlPlaneschema,cas_create,build_lease_commit. Already merged. - T-QC31-add-sdlc-lease-cli-commands — provides the
sdlc_lease.pydispatch surface. Already merged.
Discovery context
Section titled “Discovery context”- This task is the missing piece for E0002 slice 3 cutover discovered while preparing PR #127 —
refs/sdlc/control-planedoes not exist on origin and the slice-2 skills fail-closed without it. - The ADR’s “Migration of in-flight tasks at cutover” section (protocol.md lines 1303–1374) specifies the exact 8-step procedure and 3 edge cases; this task implements that spec verbatim.
- The decision to bake control-plane bootstrap into the migrate command (rather than as a separate
sdlc lease control init) follows the ADR’s framing: the bootstrap is part of cutover migration, and running it independently would be a footgun (a control-plane ref without minted leases for in-flight tasks would let new dispatch begin while existing work is invisible).
Depends on
Section titled “Depends on”T-FFHN-github-ref-leases-coordination