T-UUMK-port-dashboard-four-source-view
Status: closed/done · Impact: high · Complexity: large
Per D-0013-dashboard-app, v1 of the dashboard is the “active work, locally and on Git/GitHub” view over four data sources. Two already exist in the server (lease refs + task/milestone entities); two are net-new (this checkout’s local working state, and GitHub PR status). This task builds the four-source view in the Vite+Vue SPA and extends the dashboard JSON API with the two net-new sources, so an operator sees in-flight local work, leases, PRs, and tasks in one place.
| Location | Role today |
|---|---|
plugin/lib/services/dashboard/server.ts#buildState | Assembles state from two sources: lease refs (readLocalLeases over refs/sdlc/tasks/* + archive) and task/milestone entities (readTasks, readSimpleEntities). Returns project_root, generated_at, network_enabled, tasks, active_leases, archived_leases, milestones, summary. No local-working-state or PR-status source. |
plugin/lib/services/dashboard/server.ts#makeApp | Hono app: GET / serves the INDEX_HTML string; GET /api/state returns buildState; POST /api/refresh fetches lease refs from origin, gated on networkEnabled. |
plugin/lib/services/dashboard/server.ts#INDEX_HTML | ~180-line embedded HTML/CSS/JS string — the entire current UI, rendering leases/tasks/milestones. Retired by T-JDEV, not here. |
plugin/lib/util/git.ts#listWorktreeBasenames | Returns sorted basenames of .sdlc/worktrees/<name> dirs for a checkout. Reusable local-working-state primitive. |
plugin/lib/services/project/ops/_worktree-common.ts#probeDirty | Runs git status --porcelain; returns { dirty, porcelain[] }. Reusable git-status primitive. |
plugin/lib/model/entities/task/ops/inflight.ts | sdlc task inflight op: pairs .sdlc/worktrees/<basename> with task/<basename> branches, reads each task’s status, queries gh pr list for an open PR, and categorises implementing/awaiting-review/stale/other. --no-gh skips the gh queries. This is the in-flight task-work source. |
plugin/lib/model/entities/task/ops/_probe_core.ts | Shared probes: listLocalBranches, worktreePath, openPrForBranch (branch→open-PR number via gh pr list --search head:<branch>). |
plugin/lib/services/pr/ops/classify.ts | sdlc pr classify <n> op: classifies a PR into MERGED/CLOSED/CONFLICTS/CI-FAILED/NEEDS-RESPONSE/CLEAN/ERROR via gh pr view --json. Reusable PR-status source. |
plugin/lib/services/dashboard/dashboard-service.ts:115 | Parses --no-network into networkEnabled and threads it through detach/serve → buildState/makeApp. The network gate this task’s PR source must honor. |
Proposed
Section titled “Proposed”buildState returns the two existing sources plus two net-new keys:
local_work— this machine’s active work: one entry per.sdlc/worktrees/<basename>paired with atask/<basename>branch, each carryingbasename,worktree_path,branch,task_status, the in-flightcategory(implementing/awaiting-review/stale/other),open_pr_number, and the worktree’s git-status dirty flag + porcelain lines. The root checkout’s owngit statusdirty flag is included too.pull_requests— GitHub PR status for the in-flight tasks’ branches: per open PR apr_number,branch/task_id,verdict(thepr classifyenum), and one-linereason. Suppressed under--no-network: whennetworkEnabledis false the key is present but its rows carry no network-derived data (e.g.{ network_disabled: true, prs: [] }), exactly asPOST /api/refreshalready 409s offline.
summary gains counts for the two new sources (e.g. local_worktrees, dirty_worktrees,
open_prs). The Vite+Vue SPA renders all four sources as the unified “active work” view: a task
being worked locally surfaces in local_work, its lease in active_leases, its PR in
pull_requests, and its entity row in tasks, cross-linked by task_id/basename.
Approach
Section titled “Approach”- Extend
buildStatewithlocal_work. ReuselistWorktreeBasenames(plugin/lib/util/git.ts) and the in-flight categoriser logic frominflight.ts/_probe_core.ts(listLocalBranches,worktreePath,classify) to build the worktree↔branch↔task roster. For each worktree runprobeDirty(_worktree-common.ts) to attach the git-status dirty flag + porcelain. Do not reimplement worktree/branch/status scanning — call the existing helpers. Thegh-dependentopen_pr_numberlookup (viaopenPrForBranch) runs only whennetworkEnabled. - Extend
buildStatewithpull_requests. For each in-flight branch with an open PR, classify it by reusing thepr classifylogic (plugin/lib/services/pr/ops/classify.ts—fetchLiveState+classify) to attachverdict/reason. Gate the whole source onnetworkEnabled: when false, emit the suppressed shape and make zeroghcalls. - Thread the network gate.
buildStatealready receivesnetworkEnabled; both net-new sources read it so--no-networkfully suppresses everyghcall (matching the existingPOST /api/refresh409 behavior). Add the newsummarycounts. - Build the SPA view. In
apps/dashboard/src/(scaffolded byT-CW4K) add Vue components that render the four sources as the “active work” view, consumingGET /api/statethrough the generated typed client fromT-JZL4(no hand-written request plumbing). Cross-link rows bytask_id/basenameso one task’s local-work / lease / PR / entity rows associate. - Tests. Extend
plugin/lib/services/dashboard/tests/dashboard.test.tsto assert the new keys are present, that--no-networksuppresses the PR source, and that a worktree+branch pair surfaces inlocal_work.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/lib/services/dashboard/server.ts#buildState | modify | Add local_work + pull_requests sources and new summary counts; honor networkEnabled for both. |
plugin/lib/services/dashboard/tests/dashboard.test.ts | modify | Assert the four-source /api/state shape and --no-network PR suppression. |
apps/dashboard/src/components/ActiveWorkView.vue | new | Top-level “active work” view composing the four source panels, fed by the typed client. |
apps/dashboard/src/components/LocalWorkPanel.vue | new | Renders local_work (worktrees, dirty status, in-flight category). |
apps/dashboard/src/components/PullRequestsPanel.vue | new | Renders pull_requests (verdict + reason), with offline/suppressed state. |
apps/dashboard/src/api/state.ts | new | Thin call wrapper around the generated typed client’s /api/state accessor for the view to consume. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
GET /api/statereturns all four sources —active_leases+tasks/milestones(existing) andlocal_work+pull_requests(net-new) — as top-level keys. - AC-2: When the server runs with
--no-network,GET /api/state’spull_requestssource carries no network-derived PR data (suppressed shape) and noghprocess is spawned for it. - AC-3: A task with a live
.sdlc/worktrees/<basename>worktree and atask/<basename>branch appears inlocal_workwith itscategory,task_status, and git-status dirty flag. - AC-4: With network enabled, an in-flight branch that has an open PR appears in
pull_requestswith averdictfrom thepr classifyenum and a one-linereason. - AC-5: The Vite+Vue SPA renders the unified “active work” view showing local worktrees, active leases, PRs, and tasks, consuming the data through the generated typed client (no hand-written fetch plumbing).
- AC-6:
dashboard.test.tscovers the four-source state shape and the--no-networkPR suppression, and passes.
Out of scope
Section titled “Out of scope”- The build-into-plugin pipeline (committed drift-gated
web-dist/) and retiringINDEX_HTML/ repointingGET /—T-JDEV. - Generating the typed API client itself —
T-JZL4(this task consumes it). - Scaffolding
apps/dashboard(Vite + Vue 3) and the one-command dev startup —T-CW4K. - Re-keying or reshaping the existing lease / task / milestone sources beyond adding cross-links.
Dependencies
Section titled “Dependencies”T-CW4K— must close first: provides theapps/dashboardVite+Vue scaffold the view components live in.T-JZL4— must close first: provides the generated typed API client the SPA consumes.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-06-28. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto —
dashboard.test.ts“buildState four-source state” assertsactive_leases,tasks,milestones,local_work,pull_requestsare all top-level keys. - AC-2: auto —
dashboard.test.ts“—no-network” assertspull_requestsis the suppressed{ network_disabled: true, prs: [] }shape and the injectedghrunner records zero calls. - AC-3: auto —
dashboard.test.tsnetwork-enabled case asserts a worktree+branch pair surfaces inlocal_workwithcategory,task_status, and the git-status dirty flag/porcelain. - AC-4: auto —
dashboard.test.tsasserts an in-flight branch with an open PR appears inpull_requestswithverdictfrom thepr classifyenum and a non-emptyreason. - AC-5: agent-manual —
bunx vue-tsc --noEmitandbunx vite buildpass (the bundle includes the generated typed client); the SPA’s only data path issrc/api/state.ts→createDashboardClient().getState(), andApp.vue’s prior hand-written fetch was removed. Live in-browser render against a running backend is deferred-user. - AC-6: auto —
bun test plugin/lib/services/dashboard/tests/dashboard.test.ts→ 9 pass, 0 fail.
What worked
Section titled “What worked”- Reusing the existing probes (
listWorktreeBasenames, inflightclassify,probeDirty,openPrForBranch,fetchLiveState/classify) made both net-new sources thin assembly over proven primitives — no scanning logic re-implemented. - The
contract.ts→gen-client.tsseam kept server and typed client in lockstep: extend the zod contract, regenerate, andmoon run dashboard-client:checkstays green. - The baseline-gated quality run cleanly separated this branch’s drift from the ~538 pre-existing findings, so the only real new-drift surfaced was self-inflicted and fixable.
Friction and automation gaps
Section titled “Friction and automation gaps”- The
## Files to touchtable omittedcontract.tsand the client regeneration even though the T-JZL4 typed-client seam makes the contract the source of truth for any new/api/statekey — a task adding API surface to a contract-backed endpoint should require Files-to-touch rows for the contract schema + the generated client. → T-1Y4A-task-specs-cite-contract-and-client apps/dashboardis outside the rootworkspacesglob (packages/ts/*only), so the typed client had to be wired via a Viteresolve.alias+ tsconfigpaths+file:dep rather thanworkspace:*— anapps/*consumer of apackages/ts/*library hits this every time; the pattern should be documented (a standard) or the glob widened. → T-VY9S-workspaces-glob-covers-apps-consumers- The readiness gate’s path claim-resolver flags shorthand citations but offers no reflow, so
expanding
util/git.ts→plugin/lib/util/git.tspushed two Approach lines past MD013’s 100-char limit, caught only by the later quality gate — the citation-fix path should reflow. → T-WML6-citation-fix-reflows-overlength-lines - The project quality gate’s
bun testincludes a network-dependent test (task-auto-defineshellinggit fetch) that cannot pass in a no-network sandbox, soquality runnever reports a headline OK here; baseline-gating absorbs it but the top-line status is misleading. → T-0PQK-task-auto-define-test-no-network
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-1Y4A-task-specs-cite-contract-and-client (local; no-network sandbox) — spawned (Local): Files-to-touch for a contract-backed endpoint must cite the contract schema + generated client.
- T-VY9S-workspaces-glob-covers-apps-consumers (local; no-network sandbox) — spawned (Local):
let
apps/*consumepackages/ts/*viaworkspace:*(widen the glob or document a standard). - T-WML6-citation-fix-reflows-overlength-lines (local; no-network sandbox) — spawned
(Upstream-plugin,
sdlc-meta): readiness-gate citation fix reflows lines past MD013. - T-0PQK-task-auto-define-test-no-network (local; no-network sandbox) — spawned
(Upstream-plugin,
sdlc-meta): task-auto-define test mocksgit fetchso the gate passes offline.
Depends on
Section titled “Depends on”T-CW4K, T-JZL4