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).
| Location | Role today |
|---|---|
| apps/sdlc/lib/services/dashboard/server.ts#readLocalLeases | for-each-ref over the local mirror; never contacts the authority |
| apps/sdlc/lib/services/dashboard/server.ts#buildState | Calls readLocalLeases for the tasks and archive namespaces; already carries a networkEnabled flag |
| apps/sdlc/lib/services/dashboard/contract.ts | Zod contract for the /api/state payload |
| apps/sdlc/lib/services/dashboard/gen-client.ts | Emits packages/ts/dashboard-client/src/types.ts from the contract; --check is the drift gate |
| packages/ts/dashboard-client/src/types.ts | Generated client types consumed by the desktop dashboard |
| apps/sdlc/lib/config/sdlc_yaml.ts | lease_authority: loader (the authority remote name; origin in this repo) |
Proposed
Section titled “Proposed”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.
Approach
Section titled “Approach”- Add a module-level helper
syncLeaseMirror(root, authority)inserver.ts: runs the fetch with both refspecs and--prune, enforces the timeout so an unreachable authority cannot hang/api/state, and returns thelease_mirrorstatus object. - Call it from
buildStatebefore the tworeadLocalLeasescalls, gated onnetworkEnabled. Resolve the authority remote name via thelease_authority:loader inapps/sdlc/lib/config/sdlc_yaml.ts, defaulting tooriginwhen unset. - Add
lease_mirrorto the contract incontract.ts; regeneratepackages/ts/dashboard-client/src/types.tsby bun-running the generatorapps/sdlc/lib/services/dashboard/gen-client.ts. - Tests with the recorded-git seam used by the existing dashboard
server tests: fetch precedes ref enumeration;
--pruneand both refspecs present; fetch failure returns rows plussynced: falsewithout throwing;networkEnabled: falseissues no fetch. - No UI change here — rendering the flag belongs to the desktop dashboard.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
| apps/sdlc/lib/services/dashboard/server.ts#buildState | modify | Sync the mirror before lease enumeration; populate lease_mirror |
| apps/sdlc/lib/services/dashboard/contract.ts | modify | Add the lease_mirror field to the state payload |
| packages/ts/dashboard-client/src/types.ts | modify | Regenerated via gen-client |
| apps/sdlc/lib/services/dashboard/tests/ | modify | Seam-recorded tests for sync ordering, degradation, and the network-disabled path |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: with network enabled,
buildStateissues the authority fetch with both refspecs and--prunebefore enumerating lease refs (seam-recorded test). - AC-2: a failed or timed-out fetch yields the local-mirror rows
plus
lease_mirror.syncedfalse with the error message, andbuildStatedoes not throw (test). - AC-3: with network disabled, no fetch is issued and
lease_mirrorreadssynced: false,error: null(test). - AC-4: the contract carries
lease_mirrorandbun run apps/sdlc/lib/services/dashboard/gen-client.ts --checkpasses — 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.
Out of scope
Section titled “Out of scope”- 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-
buildStatefetch.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”- 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
readLocalLeasesreading only the local mirror.
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/dashboard(lease-mirror.test.ts: the authority fetch with both refspecs +--pruneprecedesfor-each-refenumeration). - AC-2: auto — same suite (a failed/timed-out fetch yields local rows +
synced:falsewith the error, andbuildStatedoes 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 --checkpasses (exit 0) with the regeneratedpackages/ts/dashboard-client/src/types.tscommitted. - 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.
What worked
Section titled “What worked”- 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 --checkcaught contract/type drift deterministically — the regenerated client types are gated, not trusted.
Friction and automation gaps
Section titled “Friction and automation gaps”- The ~5s fetch timeout can only be enforced at the spawn layer (
spawnSyncblocks), so the sharedCommandRunnerseam (registry.ts) gained an optionaltimeoutMs— 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. readLocalLeasesused an un-injectable module-levelspawnSyncgit()helper (why the old test built a real repo); it was refactored ontoctx.gitto 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 --noEmitruns 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’stsbuildinfoor locktscto one run at a time.