Skip to content

T-FFHN-github-ref-leases-coordination

Status: closed/done · Impact: high · Complexity: large

Formerly epic E0002 — retired into a parent Task per D-ORMG-data-model; all member tasks shipped, children link up via parent_key.

Replace sdlc’s current “task frontmatter is the claim” control plane with a Git-ref-based lease protocol described in the GitHub Ref Leases ADR. The end state is that every authoritative coordination operation — claiming a task, transitioning phases, dispatching workers, performing operation-scoped work, archiving on close-out — flows through a CAS-guarded ref under refs/sdlc/... on the configured ref authority. Task frontmatter becomes a human-readable cache, not the lock.

The bundle this epic delivers is the full 11-item Rollout Plan from the ADR plus the in-flight migration step. Slices 1–3 are defined as concrete tasks; slice 4 (offline mode + operation leases + retiring status: in-progress as the claim) remains an outline that gets task stubs once the cutover migration is verified.

The control plane today is task-file frontmatter on main plus local Git state (worktrees, branches, PRs). Captured in the ADR’s Context section; summary:

  • Claiming is not atomic. Scanning status: open/ready and flipping to in-progress is a read-then-write race.
  • Local worktrees and branches are invisible to other clones, machines, or harnesses.
  • A crashed worker leaves in-progress state with no lease expiry or fencing.
  • Duplicate follow-up creation is possible because spawn decisions are concurrent reads.
  • The system cannot tolerate more than one orchestrator without manual coordination.

No tooling for these problems exists yet. The ADR landed in PR #97 (this branch) and is the design source of truth.

A four-slice rollout. Each slice has well-defined exit criteria; nothing in slice N+1 is task-defined until slice N’s exits hold against real use. Slice 1 is task-defined right now; slices 2–4 are outline-only here and get their own task stubs after slice 1 lands.

Slice 1 — Foundation (this epic’s defined tasks). The lease wrapping library + payload schemas + sdlc lease … CLI surface + namespace-conflict guard. After slice 1 lands, leases can be claimed, heartbeat, stolen, and released from the command line, and a fresh /sdlc:setup run refuses to proceed against a repo whose ref namespace conflicts with the lease design. No user-facing skill changes yet — slice 1 is library-and-CLI plumbing only.

Slice 2 — Lifecycle skills (outline). Teach /sdlc:orchestrate to gate on control-plane.json compatibility and to dispatch only after compatible claims. Teach /sdlc:task-work to require a valid lease token, embed the PR body footer, write handoff.md, and exit at PR open under Model B. Split /sdlc:pr-respond and /sdlc:task-close-out into separate skills that re-acquire the lease. This slice is where the protocol becomes load-bearing for normal task work.

Slice 3 — Reconcile and migration (outline). Implement sdlc reconcile against the checklist in the ADR’s Reconcile section. Build the one-shot sdlc lease migrate per the ADR’s Migration of in-flight tasks at cutover. After slice 3, the cutover from frontmatter-as-claim to lease-as-claim becomes safe to execute against this repo’s own state.

Slice 4 — Offline mode + operation leases (outline). Implement offline-speculative mode per Offline Mode. Add operation leases for backlog-triage, reconcile, and import-planning as those workflows mature. Stop using status: in-progress as the claim — keep it as a cache updated only after successful lease acquisition.

Frontmatter tasks: lists every defined task across slices 1, 2, 3, and 4a. Slice 4b (offline mode

  • operation leases) remains outline-only here; its task stubs land once slice 4a closes and the cache-not-claim semantics are exercised end-to-end.

Slice 1 (defined):

  • T-S0PK-add-lease-protocol-library-and-schemas — the wrapping library implementing the five ref operation primitives (CAS-CREATE, CAS-REPLACE, CAS-DELETE, FETCH-REF, FETCH-NAMESPACE) plus payload-schema definitions and validators for lease.json, the operation lease.json, control-plane.json, and handoff.md. Foundation everything else depends on; primitives and schemas are interdependent so they ship together.
  • T-QC31-add-sdlc-lease-cli-commandssdlc lease task claim <id>, sdlc lease task transition <id> --phase <phase>, sdlc lease op claim <op> <key>, sdlc lease heartbeat, sdlc lease release, sdlc lease inspect, sdlc lease list. Library-backed CLI commands so leases can be exercised manually before any skill changes depend on them. Depends on the library task.
  • T-K3RR-add-lease-namespace-conflict-guard — at library startup and inside /sdlc:setup, refuse to proceed if a literal refs/<namespace> ref exists on the authority (per the ADR’s Namespace conflict guard section). Independent of the CLI task; can run in parallel with it once the library task lands.

Slice 2 (defined — shipped via PRs #118, #124, #126, integrated via cutover PR #127):

  • T-X24I-orchestrate-lease-aware-dispatch/sdlc:orchestrate gains a control-plane compatibility gate and a lease-claim gate; dispatches only after a compatible task lifecycle claim succeeds; routes NEEDS-RESPONSE verdicts to /sdlc:pr-respond. No state passed via env or argv — branch-name derivation discovers the lease.
  • T-TZSN-task-work-lease-integration-model-b/sdlc:task-work acquires the lease via acquire_lease (unified operator-direct and orchestrate-dispatched paths), transitions claimed → working via CAS-REPLACE at start-commit, runs a background heartbeat during implementation, embeds the canonical PR body footer, writes deterministic handoff.md, and exits at PR open under Model B. SKILL.md is pure shell-out prose — zero inline Python.
  • T-SSB8-split-pr-respond-and-close-out-with-lease-reacquire — extracts /sdlc:pr-respond from /sdlc:task-work and wires both pr-respond and /sdlc:task-close-out to re-acquire the lease (CAS-REPLACE with steal-on-expired) before any side effect. PR-fencing check enforces footer.lease_id == discovered.lease_id. Close-out archives via atomic cas_create to refs/sdlc/archive/tasks/<id> + cas_delete of the active ref; idempotent.

Slice 3 (defined):

  • T-DNN5-add-sdlc-lease-migrate-and-control-plane-bootstrap — one-shot cutover migration. Seeds refs/sdlc/control-plane (the kill-switch gate that slice 2’s skills check), mints leases for status: in-progress tasks per the ADR’s three-case phase-derivation tree, stamps open PR bodies with the canonical footer, writes placeholder handoff.md for awaiting-review tasks, records every action to .sdlc/migration.log. Idempotent by construction (CAS-CREATE) — re-running aborts cleanly. This is the unblock for slice 2’s lease-aware skills to function on this repo.
  • T-Y1JN-add-sdlc-reconcile-reporter — read-only multi-source anomaly reporter covering all 14 categories from the ADR’s Reconcile section. Refreshes the local mirror via batch FETCH-NAMESPACE, runs 14 independent detector functions, emits plaintext (default) or JSON (--json), with exit codes graded by severity. Read-only by design — --fix modes defer to a later slice.

Slice 4a (defined — retire status: in-progress as the claim, Shape C):

  • T-YQMJ-bump-task-schema-v4-add-prs-and-status-cache-semantics — bumps the task entity schema v3→v4: adds an optional prs: array of merged-PR URLs (the Obsidian static-viewer affordance) and tightens the status: description to name refs/sdlc/tasks/<id> as the authoritative claim. Includes a stamp-only v3-to-v4.py migration and a bulk /sdlc:entities-migrate task stamp commit across active tasks. Foundation for the other two slice-4a tasks.
  • T-KPOU-task-work-writes-pr-url-to-prs-at-open/sdlc:task-work Step 10 invokes a new idempotent append_pr_url.py helper immediately after gh pr create returns; commits the prs: update on main as the second of three main commits per task lifecycle (claim → PR-open → close-out). Depends on the schema bump.
  • T-TEJX-task-close-out-verifies-prs-against-merged-pr/sdlc:task-close-out Step 4 verifies the merged PR URL is in prs: and appends it if drift is detected (e.g. a PR opened outside /sdlc:task-work). No-op in the happy path. Bundled into the existing Step 4 commit; no new main commit. Depends on the schema bump; lands in parallel with the writer task.

Slice 4b (outline):

  • Stub: offline-speculative mode per offline-mode.md.
  • Stub: backlog-triage / reconcile / import-planning operation leases.

These are bundle-level outcomes spanning the full four-slice rollout, not the sum of member-task ACs.

  • AC-E1: A task can be claimed, worked, PR’d, reviewed, and closed end-to-end with every authoritative transition gated by a refs/sdlc/tasks/<id> lease ref. The lease ref’s commit history contains every phase transition for that task in lease.json updates.
  • AC-E2: Two concurrent dispatchers running /sdlc:orchestrate on different machines (or different clones) against the same ref authority never co-claim the same task. Whichever loses the CAS race observes the winner via FETCH-REF and bails out cleanly. Verified empirically by running two orchestrators in parallel against a populated backlog.
  • AC-E3: sdlc reconcile --report correctly identifies every anomaly category from the Reconcile checklist in the ADR against a fixture repo seeded with each anomaly type (dangling lease, missing handoff, missing PR footer, etc.). Output is deterministic.
  • AC-E4: The cutover migration (sdlc lease migrate) is idempotent: running it twice against the same repo produces identical lease state on the first run and a no-op + audit log on the second. Failed runs leave the repo in a recoverable state (no half-minted leases without their corresponding PR footers).
  • AC-E5: Workers operating offline under the Offline Mode protocol cannot publish authoritative state until reconnect, and the reconnect-and-publish algorithm correctly handles the three partition timing cases enumerated in offline-mode.md. Verified by simulating each timing case in test fixtures.
  • AC-E6: Task frontmatter status: in-progress is no longer the claim. Reconcile reports any task at status: in-progress without a corresponding active lease ref as an anomaly. Slice 4 closes the last skill (/sdlc:task-work and friends) that wrote in-progress before acquiring the lease.
  • GitHub App with fine-grained ACL on the lease namespace. Today’s access control is honor-system + reconcile, per the ADR’s Security and Permissions. The app is forward-pointed but not in this epic.
  • Persistent worker through review (Model A). This epic implements Model B only; the persistent-worker mode is documented as future work in the ADR’s Consequences section.
  • Signed lease commits. Deferred until adversarial concerns arise (multi-org adoption).
  • Monotonic lease_version counter for side-effect fencing. Deferred until downstream systems hold lease references.
  • Internet-time fallback for clock-sync. The ADR’s Open Questions notes this as deferred; this epic assumes participating hosts run NTP/chrony/equivalent.
  • Steal-event audit log (a refs/sdlc/events/... aggregation). The lease ref’s commit history is the audit trail for this epic; an aggregated event log is deferred until a reconcile UI consumes it.
  • Multi-orchestrator deployment guides. The protocol supports it (the dispatcher’s prefer_same_host policy is shipped here), but operational guidance for running orchestrators on multiple hosts is documentation work that happens after the protocol is live.

This epic operationalizes the ADR at docs/planning/decisions/github-ref-leases/, which evolved across PR #97 through extensive review iteration. The Rollout Plan section of the ADR’s protocol.md is the canonical sequencing; this epic’s slice structure groups those items by codebase/dependency boundaries so each slice’s PRs share scaffolding cleanly. Slice 1’s “library + schemas + CLI + guard” boundary is chosen because nothing user-facing changes until slice 2; that lets us land the foundation as a self-contained shippable unit and run it manually before any lease-aware skill goes live.

One precondition lives outside this epic: T-75LX-establish-plugin-lib-convention establishes the plugin/lib/ directory, the python-runtime.md convention doc, and the root pytest.ini that the lease library follows. It was spawned out of slice 1 planning to keep the project-wide Python organization convention separate from the lease-specific work. It must land before slice 1’s library task starts.


← Back to Tasks