Skip to content

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.
LocationRole today
plugin/lib/lease/control_plane.pyRead-only: fetch_and_validate_control_plane raises ControlPlaneMissing if refs/sdlc/control-plane is absent. No write path exists.
plugin/scripts/sdlc_lease.pyTop-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/*.mdMultiple 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.

Ship sdlc lease migrate as a top-level subcommand that:

  1. Initializes refs/sdlc/control-plane via CAS-CREATE with a ControlPlane payload carrying the local sdlc_version. Idempotent-by-construction: re-running fails the CAS-CREATE cleanly.
  2. Discovers all tasks at status: in-progress from docs/planning/tasks/*.md.
  3. 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).
  4. Mints a fresh lease_id + lease_token for each migrated task using a sentinel host_id of migrated-pre-cutover so reconcile can identify migration-origin leases later.
  5. CAS-CREATEs refs/sdlc/tasks/<id> per task; existing refs are skipped with a structured warning (re-run safety).
  6. Stamps open PR bodies with the canonical <!-- sdlc-lease: task=<id> lease=<lease_id> --> footer via gh pr edit.
  7. Writes placeholder handoff.md into lease commits for awaiting-review tasks (content: migrated; see PR description).
  8. 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.

  1. Add init_control_plane(authority, *, sdlc_version, cwd=None) to plugin/lib/lease/control_plane.py. Wraps cas_create on refs/sdlc/control-plane with a validated ControlPlane payload. Returns the new ref SHA on success; raises CASFailed if the ref already exists.
  2. Add discover_inflight_tasks(project_root) to a new plugin/lib/lease/migration.py module. Returns a list of InflightTask records: {task_id, file_path, status, branch_exists, pr_number, pr_state}. PR state via gh pr list --search "<task_id>" --state open,merged --json number,state. Branch state via git branch --list "task/<task_id>" "feat/<task_id>".
  3. Add derive_initial_phase(record) in the same module. Returns one of: awaiting-review (open PR exists), working (branch-only), or stale-requires-review (neither — caller skips this task and logs the flag).
  4. Add mint_migration_lease(task_id, phase, pr_number=None) in the same module. Returns a TaskLifecycleLease payload with host_id = "migrated-pre-cutover", fresh lease_id/lease_token UUIDv4s, and expires_at set to now + TTL for working phase or null for awaiting-review phase.
  5. Build plugin/scripts/lease_cli/migrate.py as a non-shebanged module imported by sdlc_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 with MIGRATION-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_create ref. Record outcome.
    • Step D: For awaiting-review tasks, write placeholder handoff.md via build_lease_commit(handoff_md=...) and cas_replace the 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_plane to seed refs/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}.
  6. Wire migrate into sdlc_lease.py top-level argparse dispatch. Surface as sdlc lease migrate [--dry-run]. --dry-run runs steps A–E in report-only mode (no cas_create, no cas_replace, no gh pr edit, no log write) and emits what WOULD happen.
  7. Tests in plugin/scripts/lease_cli/tests/test_migrate.py against 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.
  8. Document the subcommand in plugin/scripts/lease_cli/README.md with the signature, exit codes, and a worked example. Add a one-paragraph callout under plugin/conventions/lease-aware-skills.md pointing to the migrate command as the only path to a working lease-aware setup.
LocationKindChange
plugin/lib/lease/control_plane.pymodifyAdd init_control_plane(authority, *, sdlc_version, cwd=None) write path.
plugin/lib/lease/migration.pynewdiscover_inflight_tasks, derive_initial_phase, mint_migration_lease, and shared InflightTask dataclass.
plugin/lib/lease/__init__.pymodifyRe-export init_control_plane, discover_inflight_tasks, mint_migration_lease.
plugin/scripts/lease_cli/migrate.pynewCLI handler implementing the 8-step procedure.
plugin/scripts/sdlc_lease.pymodifyAdd migrate to top-level argparse dispatch + module docstring.
plugin/scripts/lease_cli/tests/test_migrate.pynewUnit + integration tests against the local-bare-repo authority fixture.
plugin/lib/lease/tests/test_migration.pynewLibrary-level tests for the four new library helpers.
plugin/scripts/lease_cli/README.mdmodifyDocument the migrate subcommand contract.
plugin/conventions/lease-aware-skills.mdmodifyAdd the “running the cutover migration” section.
  • AC-1: Running sdlc lease migrate against a fresh repo (no refs/sdlc/control-plane) creates the control-plane ref on origin with the local sdlc_version. Verified by git ls-remote origin refs/sdlc/control-plane returning a non-empty SHA.

  • AC-2: For each task at status: in-progress at migration time, a refs/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 via gh pr view <num> --json body -q .body.

  • AC-4: awaiting-review leases contain a handoff.md blob in their commit tree with placeholder content. Verified by git show <ref>:handoff.md returning the placeholder.

  • AC-5: .sdlc/migration.log exists after a successful run with one JSON line per processed task carrying task_id, lease_id, phase_assigned, action_taken.

  • AC-6: Re-running sdlc lease migrate against a migrated repo exits non-zero with MIGRATION-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 as stale-requires-review and 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 migrate successfully, /sdlc:orchestrate proceeds past the control-plane gate on its next tick (no CONTROL-PLANE-MISSING marker). Verified by running orchestrate in a test repo after migration.

  • AC-10: All migration-origin leases carry host_id = "migrated-pre-cutover" — visible in git log refs/sdlc/tasks/<id> --format=%B for any migrated task’s lease history.

  • AC-11: sdlc lease migrate --dry-run reports what would happen without mutating any state (no refs created, no PR bodies edited, no log written). Verified by comparing git ls-remote and gh pr view output 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.py

    Quality checks pass: plugin/scripts/run_quality_checks.py --config sdlc.yaml.

  • 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.
  • --fix modes 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.
  • 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 ControlPlane schema, cas_create, build_lease_commit. Already merged.
  • T-QC31-add-sdlc-lease-cli-commands — provides the sdlc_lease.py dispatch surface. Already merged.
  • This task is the missing piece for E0002 slice 3 cutover discovered while preparing PR #127 — refs/sdlc/control-plane does 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).

T-FFHN-github-ref-leases-coordination


← Back to Tasks