Skip to content

T-TVC6-dashboard-lease-mirror-sync

Status: closed/done · Impact: medium · Complexity: small

The dashboard enumerates the LOCAL refs/sdlc/* mirror while the authority is origin, so the panel is stale the moment any other session or host touches a lease: a live task-work lease stays invisible and archived leases linger as ACTIVE until someone manually fetches the lease refspecs. Both failure modes were observed on 2026-07-18 during T-2KK8 (live lease missing; 15 dead leases shown).

LocationRole today
apps/sdlc/lib/services/dashboard/server.ts#readLocalLeasesfor-each-ref over the local mirror; never contacts the authority
apps/sdlc/lib/services/dashboard/server.ts#buildStateCalls readLocalLeases for the tasks and archive namespaces; already carries a networkEnabled flag
apps/sdlc/lib/services/dashboard/contract.tsZod contract for the /api/state payload
apps/sdlc/lib/services/dashboard/gen-client.tsEmits packages/ts/dashboard-client/src/types.ts from the contract; --check is the drift gate
packages/ts/dashboard-client/src/types.tsGenerated client types consumed by the desktop dashboard
apps/sdlc/lib/config/sdlc_yaml.tslease_authority: loader (the authority remote name; origin in this repo)

buildState syncs the mirror first when network is enabled: one git fetch against the authority with the refspecs +refs/sdlc/tasks/*:refs/sdlc/tasks/* and +refs/sdlc/archive/tasks/*:refs/sdlc/archive/tasks/* plus --prune, guarded by a short timeout (about 5s). The outcome lands in a new lease_mirror field on the state payload — an object with synced (boolean) and error (string or null). Fetch failure degrades to today’s behavior (local rows) with synced: false and the error text; network disabled skips the fetch with synced: false, error: null. Client types are regenerated in the same change.

  1. Add a module-level helper syncLeaseMirror(root, authority) in server.ts: runs the fetch with both refspecs and --prune, enforces the timeout so an unreachable authority cannot hang /api/state, and returns the lease_mirror status object.
  2. Call it from buildState before the two readLocalLeases calls, gated on networkEnabled. Resolve the authority remote name via the lease_authority: loader in apps/sdlc/lib/config/sdlc_yaml.ts, defaulting to origin when unset.
  3. Add lease_mirror to the contract in contract.ts; regenerate packages/ts/dashboard-client/src/types.ts by bun-running the generator apps/sdlc/lib/services/dashboard/gen-client.ts.
  4. Tests with the recorded-git seam used by the existing dashboard server tests: fetch precedes ref enumeration; --prune and both refspecs present; fetch failure returns rows plus synced: false without throwing; networkEnabled: false issues no fetch.
  5. No UI change here — rendering the flag belongs to the desktop dashboard.
LocationKindChange
apps/sdlc/lib/services/dashboard/server.ts#buildStatemodifySync the mirror before lease enumeration; populate lease_mirror
apps/sdlc/lib/services/dashboard/contract.tsmodifyAdd the lease_mirror field to the state payload
packages/ts/dashboard-client/src/types.tsmodifyRegenerated via gen-client
apps/sdlc/lib/services/dashboard/tests/modifySeam-recorded tests for sync ordering, degradation, and the network-disabled path
  • AC-1: with network enabled, buildState issues the authority fetch with both refspecs and --prune before enumerating lease refs (seam-recorded test).
  • AC-2: a failed or timed-out fetch yields the local-mirror rows plus lease_mirror.synced false with the error message, and buildState does not throw (test).
  • AC-3: with network disabled, no fetch is issued and lease_mirror reads synced: false, error: null (test).
  • AC-4: the contract carries lease_mirror and bun run apps/sdlc/lib/services/dashboard/gen-client.ts --check passes — the regenerated types ship in the same PR.
  • AC-5: manual smoke recorded in the PR body — with the dashboard running, archive or acquire a lease via the lease CLI from the primary checkout, refresh, and the panel reflects the authority state without any manual git fetch.
  • Rendering the staleness flag in the desktop dashboard UI — the payload field is the seam; the UI thread owns the display.
  • Sweeping dead leases (T-Z698-lease-task-sweep-archive-closed).
  • Caching or interval strategies beyond the per-buildState fetch.
  • none
  • 2026-07-18 dashboard session: the T-2KK8 working lease was absent from the panel while 15 orphaned leases showed as active; root cause traced to readLocalLeases reading only the local mirror.

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

  • AC-1: auto — bun test apps/sdlc/lib/services/dashboard (lease-mirror.test.ts: the authority fetch with both refspecs + --prune precedes for-each-ref enumeration).
  • AC-2: auto — same suite (a failed/timed-out fetch yields local rows + synced:false with the error, and buildState does not throw).
  • AC-3: auto — same suite (network disabled → no fetch, lease_mirror = {synced:false, error:null}).
  • AC-4: auto — the contract carries lease_mirror; bun run apps/sdlc/lib/services/dashboard/gen-client.ts --check passes (exit 0) with the regenerated packages/ts/dashboard-client/src/types.ts committed.
  • AC-5: deferred-user — the live dashboard smoke (acquire/archive a lease, refresh, panel reflects the authority without a manual git fetch) is left for a human spot-check; the payload field is correct and unit-covered.
  • The recorded-git seam (ctx.git) made the fetch-before-enumeration ordering directly assertable in one test, without standing up a real git repo.
  • gen-client.ts --check caught contract/type drift deterministically — the regenerated client types are gated, not trusted.
  • The ~5s fetch timeout can only be enforced at the spawn layer (spawnSync blocks), so the shared CommandRunner seam (registry.ts) gained an optional timeoutMs — a change beyond the task’s listed ## Files to touch. It is backward-compatible, but a task needing a timeout on an injected runner should scope the seam extension up front.
  • readLocalLeases used an un-injectable module-level spawnSync git() helper (why the old test built a real repo); it was refactored onto ctx.git to unify the server’s git seam. The seam being non-uniform was latent tech debt this task had to pay down.
  • Concurrency hazard: overlapping tsc --noEmit runs corrupt the shared incremental cache and produce spurious FAILs. Under the parallel-task workflow, quality gates MUST be serialized — the runner should either isolate each run’s tsbuildinfo or lock tsc to one run at a time.

← Back to Tasks