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/readyand flipping toin-progressis a read-then-write race. - Local worktrees and branches are invisible to other clones, machines, or harnesses.
- A crashed worker leaves
in-progressstate 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.
Proposed
Section titled “Proposed”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 operationlease.json,control-plane.json, andhandoff.md. Foundation everything else depends on; primitives and schemas are interdependent so they ship together. - T-QC31-add-sdlc-lease-cli-commands —
sdlc 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 literalrefs/<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:orchestrategains a control-plane compatibility gate and a lease-claim gate; dispatches only after a compatible task lifecycle claim succeeds; routesNEEDS-RESPONSEverdicts 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-workacquires the lease viaacquire_lease(unified operator-direct and orchestrate-dispatched paths), transitionsclaimed → workingvia CAS-REPLACE at start-commit, runs a background heartbeat during implementation, embeds the canonical PR body footer, writes deterministichandoff.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-respondfrom/sdlc:task-workand wires bothpr-respondand/sdlc:task-close-outto re-acquire the lease (CAS-REPLACE with steal-on-expired) before any side effect. PR-fencing check enforcesfooter.lease_id == discovered.lease_id. Close-out archives via atomiccas_createtorefs/sdlc/archive/tasks/<id>+cas_deleteof 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 forstatus: in-progresstasks per the ADR’s three-case phase-derivation tree, stamps open PR bodies with the canonical footer, writes placeholderhandoff.mdforawaiting-reviewtasks, 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 —--fixmodes 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 thestatus:description to namerefs/sdlc/tasks/<id>as the authoritative claim. Includes a stamp-onlyv3-to-v4.pymigration and a bulk/sdlc:entities-migrate taskstamp 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-workStep 10 invokes a new idempotentappend_pr_url.pyhelper immediately aftergh pr createreturns; commits theprs: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-outStep 4 verifies the merged PR URL is inprs: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-planningoperation leases.
Acceptance criteria
Section titled “Acceptance criteria”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 inlease.jsonupdates. - AC-E2: Two concurrent dispatchers running
/sdlc:orchestrateon different machines (or different clones) against the same ref authority never co-claim the same task. Whichever loses the CAS race observes the winner viaFETCH-REFand bails out cleanly. Verified empirically by running two orchestrators in parallel against a populated backlog. - AC-E3:
sdlc reconcile --reportcorrectly 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-progressis no longer the claim. Reconcile reports any task atstatus: in-progresswithout a corresponding active lease ref as an anomaly. Slice 4 closes the last skill (/sdlc:task-workand friends) that wrotein-progressbefore acquiring the lease.
Out of scope
Section titled “Out of scope”- 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_versioncounter 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_hostpolicy is shipped here), but operational guidance for running orchestrators on multiple hosts is documentation work that happens after the protocol is live.
Discovery context
Section titled “Discovery context”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.