Skip to content

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.

LocationRole today
apps/sdlc/lib/services/lease/ops/task/archive.tsSingle-task idempotent archive op (sdlc lease task archive) wrapping releaseToArchive
apps/sdlc/lib/services/lease/ops/list.tssdlc lease list — enumerates refs under a prefix on the authority via fetchNamespace
apps/sdlc/lib/services/lease/index.tsLease library exports (releaseToArchive, fetchNamespace, fetchRef)
apps/sdlc/lib/services/lease/ops/_common.tsShared op plumbing: leaseAuthorityShape, resolveLeaseContext, REF_NS_ARCHIVE
docs/planning/tasks/Task status ground truth — a status: starting closed/ means the lease is dead

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.

  1. New op module apps/sdlc/lib/services/lease/ops/task/sweep.ts following the archive.ts pattern (defineOp, path ["lease","task","sweep"], leaseAuthorityShape input plus a dryRun flag). Ops self-register by discovery — confirm with sdlc lease task sweep --help.
  2. Enumerate the namespace the way list.ts does (fetchNamespace over the tasks prefix).
  3. Read each task’s status from origin/main after one git fetch origin main: show the file at that rev and parse the status: frontmatter line. Missing file → UNKNOWN verdict.
  4. Archive via releaseToArchive with the same conflict handling as archive.ts (LeaseConflict surfaces as the exit-4 marker).
  5. 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.
  6. Run the sweep for real from the branch (archiving is idempotent and safe pre-merge) and record the summary marker in the PR body.
LocationKindChange
apps/sdlc/lib/services/lease/ops/task/sweep.tsnewThe sweep op: enumerate, verdict per ref, archive closed, report
apps/sdlc/lib/services/lease/tests/modifyFixture-authority tests for the four verdict paths
  • AC-1: sdlc lease task sweep --dry-run lists every ref under the tasks namespace on the authority with a per-ref verdict and archives nothing (authority unchanged, verified with sdlc lease list).
  • AC-2: the real run archives exactly the refs whose task status: at origin/main starts closed/, moving each to the archive namespace; leases of non-closed tasks are untouched.
  • AC-3: a ref with no matching task file at origin/main is 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/lease is green.
  • AC-5: the live run’s SWEEP archived=N kept=N unknown=N line is recorded in the PR body, and the dashboard’s active-lease list shrinks accordingly once its mirror syncs.
  • 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.
  • none

Captured by /sdlc:task-work on 2026-07-18. PR: pending.

  • AC-1: auto — bun test apps/sdlc/lib/services/lease (sweep.test.ts: --dry-run reports 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/main is 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 sweep against 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.
  • The op mirrored the archive.ts / list.ts conventions 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-main fixed (#898), the whole task-work flow ran with zero workarounds.
  • --dry-run is a reserved global CLI flag: the adapter (registry_adapter.ts, GLOBAL_FLAGS) threads it as ctx.dryRun and SKIPS any op input field named dryRun, so a spec that asks for a dryRun input field can never receive a value from the CLI. The op honors input.dryRun || ctx.dryRun to cover CLI, programmatic, and test callers. Gap: the op-authoring convention should warn that GLOBAL_FLAGS names (dry-run, …) are reserved and ctx-threaded, so task specs don’t request a same-named input field.
  • Each commit-to-main op (Step 5a/5b) now runs the full ~65s pre-commit hook suite (armed worktree), and the quality gate is a full-monorepo tsc + 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.)

← Back to Tasks