T-Z698-lease-task-sweep-archive-closed
Status: closed/done · Impact: medium · Complexity: small
The lease authority accumulates active leases for tasks that already
closed — 15 orphans were sitting under the refs/sdlc/tasks/ namespace
on 2026-07-18, rendered as ACTIVE LEASES by the dashboard.
sdlc lease task archive reconciles one task at a time; nothing
reconciles the whole namespace against task status in bulk.
| Location | Role today |
|---|---|
| apps/sdlc/lib/services/lease/ops/task/archive.ts | Single-task idempotent archive op (sdlc lease task archive) wrapping releaseToArchive |
| apps/sdlc/lib/services/lease/ops/list.ts | sdlc lease list — enumerates refs under a prefix on the authority via fetchNamespace |
| apps/sdlc/lib/services/lease/index.ts | Lease library exports (releaseToArchive, fetchNamespace, fetchRef) |
| apps/sdlc/lib/services/lease/ops/_common.ts | Shared op plumbing: leaseAuthorityShape, resolveLeaseContext, REF_NS_ARCHIVE |
| docs/planning/tasks/ | Task status ground truth — a status: starting closed/ means the lease is dead |
Proposed
Section titled “Proposed”A new discovered op, sdlc lease task sweep: enumerate every ref under
the tasks namespace on the authority; for each, resolve the task file
under docs/planning/tasks/ at origin/main (lease refs are named by
full task basename); when its status: starts closed/, archive the
lease via releaseToArchive; otherwise keep it. Refs with no matching
task file are reported as unknown and kept — archiving those is a human
call. --dry-run prints the plan without touching the authority. Text
render: one line per ref with an ARCHIVE, KEEP, or UNKNOWN verdict plus
task id and status, ending with a summary marker
SWEEP archived=N kept=N unknown=N; --output json emits structured
rows.
Approach
Section titled “Approach”- New op module
apps/sdlc/lib/services/lease/ops/task/sweep.tsfollowing thearchive.tspattern (defineOp, path["lease","task","sweep"],leaseAuthorityShapeinput plus adryRunflag). Ops self-register by discovery — confirm withsdlc lease task sweep --help. - Enumerate the namespace the way
list.tsdoes (fetchNamespaceover the tasks prefix). - Read each task’s status from
origin/mainafter onegit fetch origin main: show the file at that rev and parse thestatus:frontmatter line. Missing file → UNKNOWN verdict. - Archive via
releaseToArchivewith the same conflict handling asarchive.ts(LeaseConflict surfaces as the exit-4 marker). - Tests against a fixture authority (a local bare repo, the pattern
already used under
apps/sdlc/lib/services/lease/tests/) covering the archive, keep, unknown, and dry-run paths. - Run the sweep for real from the branch (archiving is idempotent and safe pre-merge) and record the summary marker in the PR body.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
| apps/sdlc/lib/services/lease/ops/task/sweep.ts | new | The sweep op: enumerate, verdict per ref, archive closed, report |
| apps/sdlc/lib/services/lease/tests/ | modify | Fixture-authority tests for the four verdict paths |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
sdlc lease task sweep --dry-runlists every ref under the tasks namespace on the authority with a per-ref verdict and archives nothing (authority unchanged, verified withsdlc lease list). - AC-2: the real run archives exactly the refs whose task
status:atorigin/mainstartsclosed/, moving each to the archive namespace; leases of non-closed tasks are untouched. - AC-3: a ref with no matching task file at
origin/mainis reported UNKNOWN and not archived. - AC-4: tests cover archive, keep, unknown, and dry-run against a
fixture authority;
bun test apps/sdlc/lib/services/leaseis green. - AC-5: the live run’s
SWEEP archived=N kept=N unknown=Nline is recorded in the PR body, and the dashboard’s active-lease list shrinks accordingly once its mirror syncs.
Out of scope
Section titled “Out of scope”- Archiving UNKNOWN refs (no task file) — surfaced only.
- Expiry-based sweeping of leases on open tasks — that is a liveness question, not status reconciliation.
- Dashboard mirror freshness (T-TVC6-dashboard-lease-mirror-sync).
- Scheduling or automating the sweep — it stays a manual verb.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”- The lifecycle outage era (see B-AAQ5-backlog-ephemeral-worktree-node-modules) left leases unarchived at close; the 15-orphan pile-up surfaced on the dashboard during T-2KK8’s close-out on 2026-07-18.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-07-18. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
bun test apps/sdlc/lib/services/lease(sweep.test.ts:--dry-runreports a per-ref verdict and archives nothing). - AC-2: auto — same suite (real run archives the
closed/*leases; non-closed untouched). - AC-3: auto — same suite (a ref with no task file at
origin/mainis UNKNOWN and not archived). - AC-4: auto —
bun test apps/sdlc/lib/services/lease→ 145 pass / 0 fail (7 new); archive/keep/unknown/dry-run all covered against a fixture authority. - AC-5: agent-manual — ran the live
sdlc lease task sweepagainst the real authority →SWEEP archived=7 kept=8 unknown=6. Archived 7 closed-task leases, kept every in-progress/open lease (including the three active task-work leases), and surfaced 6 short-id orphans (no task file) as UNKNOWN. Marker recorded in the PR body.
What worked
Section titled “What worked”- The op mirrored the
archive.ts/list.tsconventions cleanly — the discovered-op substrate made adding a namespace-wide reconciler a small, uniform change. - The fixture-authority test pattern (one bare repo doubling as authority + origin, matching
production’s
lease_authority: origin) is faithful rather than a contrivance. - With
commit-to-mainfixed (#898), the whole task-work flow ran with zero workarounds.
Friction and automation gaps
Section titled “Friction and automation gaps”--dry-runis a reserved global CLI flag: the adapter (registry_adapter.ts,GLOBAL_FLAGS) threads it asctx.dryRunand SKIPS any op input field nameddryRun, so a spec that asks for adryRuninput field can never receive a value from the CLI. The op honorsinput.dryRun || ctx.dryRunto cover CLI, programmatic, and test callers. Gap: the op-authoring convention should warn thatGLOBAL_FLAGSnames (dry-run, …) are reserved and ctx-threaded, so task specs don’t request a same-named input field.- Each
commit-to-mainop (Step 5a/5b) now runs the full ~65s pre-commit hook suite (armed worktree), and the quality gate is a full-monorepotsc+bun test. Combined 5a+5b exceeded a 2-minute shell timeout once; they must be run separately with extended timeouts. (Infra observation — not a correctness gap.)