feat(dashboard): Epic-centric dashboard — browse, progress, force-dispatch#152
Conversation
Adds the `epics` SQLite table (migration 005) and `epics-cache.ts` with `refreshEpics`/`readEpics`. Epics removed from the open set are marked closed rather than deleted to prevent mid-refresh flicker. Updates db.test.ts version expectations from 4→5 and includes `epics` in the expected-tables list.
listEpics now falls back to a blocked decision callout when an Epic has a Blocked entry but no Needs-human-input entry in the state issue. Adds a refresh button next to the repo selector in the Epics view that calls api.refreshEpics then re-fetches the list via the guard funnel.
📝 WalkthroughWalkthroughThis PR implements a complete Epic-centric dashboard feature spanning dispatcher and dashboard packages, enabling operators to browse GitHub Epics with live progress tracking, in-flight runner info, and manual force-dispatch capability. It includes SQLite caching, daemon refresh scheduling, API routes, React UI, and comprehensive tests and documentation. ChangesEpic-centric Dashboard Feature
Sequence Diagram(s)sequenceDiagram
participant Operator
participant Dashboard as Dashboard UI
participant API as Dashboard API
participant DB as SQLite DB
participant Daemon as Dispatcher Daemon
participant GitHub as GitHub API
Note over Daemon,GitHub: Background refresh every 60s
Daemon->>GitHub: listOpenEpics(repo)
GitHub-->>Daemon: EpicListItem[]
Daemon->>DB: refreshEpics (upsert open, mark closed)
DB-->>Daemon: void
Operator->>Dashboard: View Epics tab
Dashboard->>API: GET /api/epics/:repo
API->>DB: listEpics(repo)
DB->>API: EpicCard[]
API-->>Dashboard: EpicCard[]
Dashboard->>Operator: Render epic cards (progress, runner, decision)
Operator->>Dashboard: Click dispatch button (epic, adapter)
Dashboard->>API: POST /api/epics/:repo/:n/dispatch
API->>Daemon: dispatch(repo, n, adapter)
Daemon->>DB: Validate slots
Daemon->>DB: Enqueue workflow
Daemon->>DB: Schedule auto-dispatch
Daemon->>GitHub: Trigger cache refresh
API-->>Dashboard: Workflow result
Dashboard->>Dashboard: Refresh epics
Dashboard->>Operator: Show updated dispatch state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (3)
packages/cli/test/daemon-entry.test.ts (1)
69-100: ⚡ Quick winAdd a symmetric route-level passthrough test for refresh wiring.
This file verifies dispatch callback passthrough well; adding the same check for
POST /api/epics/:repo/refreshwould close the regression gap for the second newly-threaded callback.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/cli/test/daemon-entry.test.ts` around lines 69 - 100, Add a symmetric test to verify the /api/epics/:repo/refresh POST routes through the host-context refresh callback: create a test similar to "a dispatch POST reaches the host-context dispatch callback" that sets up a DaemonHostContext with a spy on refreshEpics (e.g., set a variable when refreshEpics is called), start the HookServer with dashboardHostExtras(ctx), POST to `${base}/api/epics/${encodeURIComponent("o/r")}/refresh` with appropriate headers/body, assert a 200 response and expected body, and assert the spy captured the repo argument (e.g., ["o/r"]) to ensure refreshEpics was invoked; reuse the existing db, dispose, server setup/teardown patterns from the dispatch test and reference refreshEpics, dashboardHostExtras, HookServer, and the route path to locate where to add the test.packages/dispatcher/src/db/migrations/005_epics.sql (1)
10-13: ⚡ Quick winAdd DB constraints for Epic state/progress invariants.
state,sub_total, andsub_closedare unconstrained right now. Adding CHECK constraints here prevents invalid cache rows from silently entering the table.Suggested migration tweak
CREATE TABLE epics ( repo TEXT NOT NULL, number INTEGER NOT NULL, title TEXT NOT NULL, - state TEXT NOT NULL, -- 'open' | 'closed' + state TEXT NOT NULL CHECK (state IN ('open', 'closed')), labels_json TEXT NOT NULL DEFAULT '[]', - sub_total INTEGER NOT NULL DEFAULT 0, - sub_closed INTEGER NOT NULL DEFAULT 0, + sub_total INTEGER NOT NULL DEFAULT 0 CHECK (sub_total >= 0), + sub_closed INTEGER NOT NULL DEFAULT 0 CHECK (sub_closed >= 0 AND sub_closed <= sub_total), gh_updated_at TEXT, -- reserved; EpicListItem carries no updated_at yet, so refreshEpics does not populate this last_refreshed INTEGER NOT NULL, -- epoch ms of our last write PRIMARY KEY (repo, number) );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/dispatcher/src/db/migrations/005_epics.sql` around lines 10 - 13, Add CHECK constraints to the epics table to enforce valid values and invariants: constrain state to only 'open' or 'closed' (e.g., CHECK (state IN ('open','closed'))), ensure sub_total is non-negative, ensure sub_closed is between 0 and sub_total (e.g., CHECK (sub_total >= 0) and CHECK (sub_closed >= 0 AND sub_closed <= sub_total)). Name the constraints clearly (e.g., chk_epics_state, chk_epics_sub_total_nonneg, chk_epics_sub_closed_range) and add them alongside the column definitions for state, sub_total, and sub_closed in the 005_epics.sql migration so invalid rows cannot be inserted.packages/dashboard/src/app/components/Epics.tsx (1)
68-78: ⚡ Quick winAdd a TSDoc/JSDoc block directly on the exported
Epicscomponent.The module has a file header, but this exported symbol still needs its own API contract doc per repo rules.
📝 Suggested fix
+/** + * Renders Epic cards for the selected repository, including runner status, + * decision callouts, and force-dispatch controls. + * Assumes `epics` all belong to the currently selected repo in `App`. + */ export function Epics({As per coding guidelines: “Every public export in a module must carry a TSDoc/JSDoc comment. Comments must describe behavior and contracts.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/dashboard/src/app/components/Epics.tsx` around lines 68 - 78, Add a TSDoc/JSDoc comment immediately above the exported Epics function that documents the component's purpose and the props contract: describe the Epics component behavior and list each prop (epics: EpicCard[] - what each EpicCard represents, adapters: string[] - expected adapter identifiers, onDispatch(repo: string, epicNumber: number, adapter: string) => void - when and how it is called, and optional onOpenInspector(session?: string) => void) including nullable/optional behavior and any side effects; ensure the comment is in standard TSDoc/JSDoc format and sits directly above the `export function Epics(...)` declaration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.gitignore:
- Line 13: The .middle/ ignore pattern prevents Git from honoring the allowlist
entry !.middle/verify.toml because ignoring a directory hides its contents;
update the .gitignore so the directory itself is not globally ignored (for
example replace the broad ".middle/" entry with a content-only pattern like
".middle/*" and/or add an explicit whitelist entry "!.middle/" before the file
allowlist) so that the existing allowlist rule !.middle/verify.toml takes
effect; references: ".middle/" and "!.middle/verify.toml".
In `@docs/superpowers/specs/2026-05-25-epic-centric-dashboard-design.md`:
- Line 137: Update the docs so the endpoint contract matches the implemented,
path-based route: replace references to `GET /api/epics?repo=<slug>` with `GET
/api/epics/:repo` (and update any example URLs, parameters, and usage notes
accordingly), ensure the response type `EpicCard[]` and description of what each
card contains remains intact, and remove or convert any guidance that references
the query parameter `repo=` to the path parameter `:repo` so clients use the
shipped route shape.
- Line 86: Update the stale migration filename reference in the spec: replace
the string "packages/dispatcher/src/db/migrations/004_epics.sql" with the
correct migration name "packages/dispatcher/src/db/migrations/005_epics.sql"
wherever it appears in this spec document so readers are pointed to the
implemented migration; ensure the as-built section and any other occurrences now
match "005_epics.sql".
In `@packages/dashboard/src/api.ts`:
- Around line 155-160: The handler currently validates body.adapter with trim()
but passes the original value downstream; normalize the adapter by assigning a
trimmed string (e.g., const adapter = body.adapter.trim()) after the validation
and use that normalized variable when calling deps.dispatchEpic(repo,
epicNumber, adapter) and anywhere else the adapter value is forwarded (refer to
body.adapter and deps.dispatchEpic in this block) so that inputs like " claude "
are dispatched as "claude".
In `@packages/dashboard/src/app/App.tsx`:
- Around line 233-236: refreshEpics (and the other similar refresh functions)
can set stale epics because they unconditionally call setEpics with whatever API
response arrives; modify refreshEpics so it captures the repo arg, performs the
async api.epics(repo) call inside guard, then before calling setEpics verify
that the repo argument still matches the currently selected repo (e.g. compare
to your selected repo state or a currentRepoRef) and only call setEpics when
they match; if you use a ref (currentRepoRef) update it on repo change and
compare to avoid race conditions when multiple fetches overlap.
In `@packages/dashboard/src/app/components/Epics.tsx`:
- Around line 86-87: The Epic card key currently uses only card.number which can
collide across repositories and preserve stale local state (e.g., in
DispatchControl); update the key in Epics.tsx where epics.map is rendering the
<li> to include a repo-scoped identifier (for example combine the repo
prop/identifier with card.number, or use a unique card.id if available) so each
key is globally unique per-repo and per-issue; ensure the same repo-scoped value
is used for data-epic if that data is relied on elsewhere (references:
Epics.tsx, epics.map, card.number, DispatchControl).
In `@packages/dashboard/src/db-deps.ts`:
- Around line 315-321: The code sets decision.link by passing need.link through
extractUrl without checking the URL scheme; update extractUrl (or add a wrapper)
to parse the input (e.g., using URL) and only return the href when the protocol
is "http:" or "https:", otherwise return undefined/null so the spread
...(need.link ? { link: extractUrl(need.link) } : {}) yields no href; apply the
same validation to the other call sites that populate EpicCard["decision"].link
from need.link so invalid schemes are omitted before rendering.
In `@packages/dashboard/test/app.test.tsx`:
- Around line 49-51: The test is using `http://localhost:${server.port}` which
can be flaky because the server binds to 127.0.0.1; update the fetch URL in
packages/dashboard/test/app.test.tsx (the call that constructs
`http://localhost:${server.port}/api/epics/${encodeURIComponent("o/r")}`) to use
`http://127.0.0.1:${server.port}` so the test consistently targets the IPv4
loopback the server is bound to.
In `@packages/dispatcher/src/epics-cache.ts`:
- Line 72: The current per-row mapping in readEpics uses
JSON.parse(r.labelsJson) as string[] which will throw on malformed labels_json
and abort the entire read; change the per-row parsing to a guarded parse inside
readEpics (or the mapping where r is used) by wrapping JSON.parse(r.labelsJson)
in a try/catch (or a small safeParseLabels helper) and return [] on error,
optionally logging a warning including the offending r.id or r.labelsJson;
ensure the resulting property remains typed as string[] (e.g., labels =
safeParseLabels(r.labelsJson) as string[]).
In `@packages/dispatcher/src/main.ts`:
- Line 373: The current call to refreshEpics(db, normalizedRepo,
ghGitHub).catch(() => {}) swallows all errors; change the empty catch to log the
error and context instead so failures are visible but still treated as
best-effort. Replace the catch with something like .catch(err =>
processLogger.error({ err, repo: normalizedRepo }, "refreshEpics failed after
dispatch")) (or console.error if processLogger is not available) so the error
and identifying context from normalizedRepo and the refreshEpics call are
recorded without breaking post-dispatch flow.
---
Nitpick comments:
In `@packages/cli/test/daemon-entry.test.ts`:
- Around line 69-100: Add a symmetric test to verify the
/api/epics/:repo/refresh POST routes through the host-context refresh callback:
create a test similar to "a dispatch POST reaches the host-context dispatch
callback" that sets up a DaemonHostContext with a spy on refreshEpics (e.g., set
a variable when refreshEpics is called), start the HookServer with
dashboardHostExtras(ctx), POST to
`${base}/api/epics/${encodeURIComponent("o/r")}/refresh` with appropriate
headers/body, assert a 200 response and expected body, and assert the spy
captured the repo argument (e.g., ["o/r"]) to ensure refreshEpics was invoked;
reuse the existing db, dispose, server setup/teardown patterns from the dispatch
test and reference refreshEpics, dashboardHostExtras, HookServer, and the route
path to locate where to add the test.
In `@packages/dashboard/src/app/components/Epics.tsx`:
- Around line 68-78: Add a TSDoc/JSDoc comment immediately above the exported
Epics function that documents the component's purpose and the props contract:
describe the Epics component behavior and list each prop (epics: EpicCard[] -
what each EpicCard represents, adapters: string[] - expected adapter
identifiers, onDispatch(repo: string, epicNumber: number, adapter: string) =>
void - when and how it is called, and optional onOpenInspector(session?: string)
=> void) including nullable/optional behavior and any side effects; ensure the
comment is in standard TSDoc/JSDoc format and sits directly above the `export
function Epics(...)` declaration.
In `@packages/dispatcher/src/db/migrations/005_epics.sql`:
- Around line 10-13: Add CHECK constraints to the epics table to enforce valid
values and invariants: constrain state to only 'open' or 'closed' (e.g., CHECK
(state IN ('open','closed'))), ensure sub_total is non-negative, ensure
sub_closed is between 0 and sub_total (e.g., CHECK (sub_total >= 0) and CHECK
(sub_closed >= 0 AND sub_closed <= sub_total)). Name the constraints clearly
(e.g., chk_epics_state, chk_epics_sub_total_nonneg, chk_epics_sub_closed_range)
and add them alongside the column definitions for state, sub_total, and
sub_closed in the 005_epics.sql migration so invalid rows cannot be inserted.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: bdc91156-30b0-4efd-a14c-3834c1a40c3e
📒 Files selected for processing (37)
.claude/skills/documenting-the-repo/SKILL.md.claude/skills/recommending-github-issues/SKILL.md.codex/skills/documenting-the-repo/SKILL.md.codex/skills/recommending-github-issues/SKILL.md.gitignoredocs/superpowers/plans/2026-05-25-epic-centric-dashboard.mddocs/superpowers/specs/2026-05-25-epic-centric-dashboard-design.mdpackages/cli/src/daemon-entry.tspackages/cli/test/daemon-entry.test.tspackages/dashboard/src/api.tspackages/dashboard/src/app/App.tsxpackages/dashboard/src/app/api-client.tspackages/dashboard/src/app/components/Epics.tsxpackages/dashboard/src/app/components/Queue.tsxpackages/dashboard/src/app/styles.csspackages/dashboard/src/bridge.tspackages/dashboard/src/db-deps.tspackages/dashboard/src/deps.tspackages/dashboard/src/index.tspackages/dashboard/src/wire.tspackages/dashboard/test/app.test.tsxpackages/dashboard/test/control-client.test.tspackages/dashboard/test/epics-api.test.tspackages/dashboard/test/epics-deps.test.tspackages/dashboard/test/epics.test.tsxpackages/dashboard/test/queue.test.tsxpackages/dashboard/test/sse.test.tspackages/dispatcher/CLAUDE.mdpackages/dispatcher/src/db/migrations/005_epics.sqlpackages/dispatcher/src/epics-cache.tspackages/dispatcher/src/github.tspackages/dispatcher/src/index.tspackages/dispatcher/src/main.tspackages/dispatcher/test/db.test.tspackages/dispatcher/test/epics-cache.test.tspackages/dispatcher/test/github-epics.test.tspackages/dispatcher/test/host-context.test.ts
…oped fetch/key, trim adapter, log refresh; docs+test fixes
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
docs/superpowers/specs/2026-05-25-epic-centric-dashboard-design.md (3)
113-113:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAlign refresh trigger description with as-built constant.
Line 113 describes "config
epics.refresh_interval_seconds(default 60)", but the as-built deltas (lines 16-17) document that the refresh cadence is a constantEPICS_REFRESH_INTERVAL_MS = 60_000inmain.ts, not a config setting (config-ification was deferred). For an "implemented" spec, this should reflect what shipped.Suggested fix
-- **Interval** — config `epics.refresh_interval_seconds` (default **60**). +- **Interval** — constant `EPICS_REFRESH_INTERVAL_MS = 60_000` in `main.ts`.Based on learnings: the as-built deltas section states "Refresh cadence is a constant
EPICS_REFRESH_INTERVAL_MS = 60_000inmain.ts(config-ification deferred), matching the existingPOLLER/WATCHDOGinterval style."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/superpowers/specs/2026-05-25-epic-centric-dashboard-design.md` at line 113, Update the spec text to match the implemented behavior: replace the config-based description for the refresh interval with the actual constant used in code by referencing EPICS_REFRESH_INTERVAL_MS in main.ts (current value 60_000 ms) and note that config-ification was deferred; remove mention of config key `epics.refresh_interval_seconds` and instead state the refresh cadence is driven by the EPICS_REFRESH_INTERVAL_MS constant consistent with POLLER/WATCHDOG style.
105-108:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate spec body to reflect as-built N=1 GitHub call optimization.
Lines 105-108 still describe the original 1+N design ("fetches each one's sub-issue progress" and "one
listOpenEpics+ NsubIssueProgresscalls"), but the as-built deltas (lines 8-10) document that this was eliminated:listOpenEpicsreads each issue'ssub_issues_summarydirectly, making per-EpicsubIssueProgresscalls unnecessary. For a spec marked "implemented", the body should match what shipped.Suggested fix to align body with as-built reality
- fetches each one's sub-issue progress, and **upserts** rows; Epics that vanish + and **upserts** rows with progress from each issue's `sub_issues_summary`; Epics that vanish from the open set are marked `closed` (not deleted, so a just-closed Epic doesn't - flicker out mid-view). One pass = one `listOpenEpics` + N `subIssueProgress` calls. + flicker out mid-view). One pass = one `listOpenEpics` call (progress comes inline).Based on learnings: the as-built deltas section explicitly states "One GitHub call, not 1+N" and "
listOpenEpicsreads each issue'ssub_issues_summaryfrom the single paginated issues call, so per-EpicsubIssueProgresscalls were dropped."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/superpowers/specs/2026-05-25-epic-centric-dashboard-design.md` around lines 105 - 108, The spec body incorrectly describes the implemented design for refreshEpics(db, repo, github) as a 1+N call pattern; update the prose to match the as-built behavior: state that listOpenEpics performs a single paginated GitHub issues API call which reads each issue's sub_issues_summary so per-Epic subIssueProgress calls were removed (N=1 optimization), and clarify that refreshEpics now upserts based on that single call and marks vanished Epics closed.
128-130:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winClarify GitHub methods after per-Epic progress call elimination.
Lines 128-130 list two methods including
subIssueProgress(repo, n), but the as-built deltas (lines 8-10) state that "per-EpicsubIssueProgresscalls were dropped" because progress comes inline fromsub_issues_summary. IfsubIssueProgresswas never implemented, remove it from this section; if it exists but is unused, note that it's not called during refresh.Suggested fix if the method was eliminated
-Two methods on the existing `GithubClient`, both via `gh api`, mirroring the -recommender's `sub_issues` technique: +New method on the existing `GithubClient` via `gh api`, mirroring the +recommender's `sub_issues` technique: -- `listOpenEpics(repo): Promise<{ number; title; state; labels: string[] }[]>` — - open issues that have ≥1 sub-issue. -- `subIssueProgress(repo, n): Promise<{ total: number; closed: number }>`. +- `listOpenEpics(repo): Promise<{ number; title; state; labels: string[]; subTotal: number; subClosed: number }[]>` — + open issues that have ≥1 sub-issue, with inline progress from `sub_issues_summary`.Based on learnings: the as-built deltas section documents "One GitHub call, not 1+N" and "per-Epic
subIssueProgresscalls were dropped — progress comes free with the list."🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/superpowers/specs/2026-05-25-epic-centric-dashboard-design.md` around lines 128 - 130, The docs currently list a GitHub helper `subIssueProgress(repo, n)` which contradicts the as-built change that per-Epic progress is provided inline via `sub_issues_summary` and "One GitHub call, not 1+N"; inspect the codebase for the symbol `subIssueProgress` and either remove it from this methods list if it was never implemented, or explicitly mark it as deprecated/unused (e.g., "exists but not called during refresh") if it remains in code; also update the methods bullet to reflect that `listOpenEpics(repo): Promise<...>` now returns per-epic progress via the `sub_issues_summary` field so that readers see progress is included without extra per-epic calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@docs/superpowers/specs/2026-05-25-epic-centric-dashboard-design.md`:
- Line 113: Update the spec text to match the implemented behavior: replace the
config-based description for the refresh interval with the actual constant used
in code by referencing EPICS_REFRESH_INTERVAL_MS in main.ts (current value
60_000 ms) and note that config-ification was deferred; remove mention of config
key `epics.refresh_interval_seconds` and instead state the refresh cadence is
driven by the EPICS_REFRESH_INTERVAL_MS constant consistent with POLLER/WATCHDOG
style.
- Around line 105-108: The spec body incorrectly describes the implemented
design for refreshEpics(db, repo, github) as a 1+N call pattern; update the
prose to match the as-built behavior: state that listOpenEpics performs a single
paginated GitHub issues API call which reads each issue's sub_issues_summary so
per-Epic subIssueProgress calls were removed (N=1 optimization), and clarify
that refreshEpics now upserts based on that single call and marks vanished Epics
closed.
- Around line 128-130: The docs currently list a GitHub helper
`subIssueProgress(repo, n)` which contradicts the as-built change that per-Epic
progress is provided inline via `sub_issues_summary` and "One GitHub call, not
1+N"; inspect the codebase for the symbol `subIssueProgress` and either remove
it from this methods list if it was never implemented, or explicitly mark it as
deprecated/unused (e.g., "exists but not called during refresh") if it remains
in code; also update the methods bullet to reflect that `listOpenEpics(repo):
Promise<...>` now returns per-epic progress via the `sub_issues_summary` field
so that readers see progress is included without extra per-epic calls.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 5c7e09ad-9134-4da9-8be8-88d433b4340b
📒 Files selected for processing (9)
docs/superpowers/specs/2026-05-25-epic-centric-dashboard-design.mdpackages/dashboard/src/api.tspackages/dashboard/src/app/App.tsxpackages/dashboard/src/app/components/Epics.tsxpackages/dashboard/src/db-deps.tspackages/dashboard/test/app.test.tsxpackages/dashboard/test/epics-deps.test.tspackages/dispatcher/src/epics-cache.tspackages/dispatcher/src/main.ts
Summary
Adds an Epic-centric dashboard — the interface around GitHub issue content the build spec's Dashboard section always implied but never modeled. It browses every open Epic in a repo with live sub-issue progress, shows which agent is on each, surfaces the state issue's decision callouts, and force-dispatches a free slot with a chosen agent. Epics is now the default landing view (Dashboard/Queue/Settings remain, demoted).
Why: the prior dashboard mirrored only what
middlemodeled — runner state + the recommender's ranked output. Nothing enumerated the Epic backlog for a human. This branch adds that model end-to-end.What changed (by layer)
epicstable (migration005_epics.sql) +epics-cache.ts(refreshEpics/readEpics).github.tsgainslistOpenEpics(one paginated call; progress comes from each issue'ssub_issues_summary— no per-Epic call). A 60s refresh sweep + post-dispatch refresh inmain.ts; vanished Epics are markedclosed, never deleted (non-flicker).DaemonHostContextgainsdispatch(repo, n, adapter)andrefreshEpics(repo).dispatchEpicManualreuses the samestartDispatchImpl+ slot/collision gates asPOST /control/dispatch(byte-identical 400/429/409 parity — the dashboard can't bypass a guard).EpicCardwire type;db-deps.listEpicsjoins the cache (progress) +workflows(which agent, liveinFlight) + the state issue (decision callout fromneedsHumanInput, falling back toblocked; recommended adapter fromreadyToDispatch) + per-adapterhasFreeSlot. NewGET /api/epics/:repo,POST /api/epics/:repo/refresh,POST /api/epics/:repo/:n/dispatch. Dispatch/refresh deps are optional → standalone (non-daemon) dashboard 404s them cleanly and still serves the cache.daemon-entry.tsthreadsdispatch+refreshEpicsintocreateDbDeps.Epics.tsx(progress bars, agent badge → Inspector, decision callout, force-dispatch control with an adapter picker defaulting to the recommender's pick, disabled when in-flight / no free slot). Repo filter + manual refresh button inApp.tsx; live refresh via the repo SSE channel.How to run
bun install mm start # daemon serves the dashboard on http://127.0.0.1:4120/Open
http://127.0.0.1:4120/— Epics is the default tab.What to verify
closed/totalprogress bars; repo filter switches repos (shown only with >1 tracked repo).adapter · state); clicking it opens the Inspector. The dispatch button is disabled while in-flight.mm dispatch.How to review
dispatchEpicManual(packages/dispatcher/src/main.ts) against#handleControlDispatch(packages/dispatcher/src/hook-server.ts) — both funnel throughstartDispatchImpl.inFlightis computed live fromworkflowson everylistEpics, so it doesn't depend on cache staleness.listEpicsalways works (reads cache);dispatchEpic/refreshEpicsare optional seams.Fragile / extra eyes
parseEpicsListrelies on GitHub'ssub_issues_summaryon the issues API.epics.gh_updated_atis reserved/unpopulated (noted inline).EPICS_REFRESH_INTERVAL_MS = 60_000(config-ification deferred).Known-minor (deferred, non-blocking)
codex) even when onlyclaudeis dispatchable today; selecting a non-dispatchable adapter disables the button (safe), but the option is shown.epicRepoholds a stale slug and the view shows the empty state (no crash); no auto-reselect.Follow-up (separate spec, agreed)
A sibling Runs/Activity view for recommender + documentation (docs-auditor/fixup) workflow visibility — currently every dashboard view filters to
kind = 'implementation'. To be brainstormed/spec'd next.Test plan
bun test— 700 pass, 0 failbun run typecheck— cleanbun run lint— cleanmain(post recommendation PR); cleanly mergeableSpec:
docs/superpowers/specs/2026-05-25-epic-centric-dashboard-design.md· Plan:docs/superpowers/plans/2026-05-25-epic-centric-dashboard.mdSummary by CodeRabbit