feat(dashboard): file-mode Epic display (epic_ref + file:// links)#189
feat(dashboard): file-mode Epic display (epic_ref + file:// links)#189thejustinwalsh wants to merge 15 commits into
Conversation
…hybrid)
Per-repo opt-in `epic_store = "file"` mode: one Markdown file per Epic
under `planning/epics/`, recommender state in `.middle/state.md`. PRs and
CI stay GitHub-native in both modes ("hybrid"). Workflow bodies, gates,
watchdog, hook server, poller — all unchanged: the three existing single-
seam interfaces (`GitHubGateway`, `StateIssueGateway`, `GitHubPollGateway`)
get renamed (`EpicGateway`, `StateGateway`, `PollGateway`) and gain
parallel file implementations behind the same contracts. Bootstrap picks
the implementation per-repo from `repo_config.epic_store`.
The agent's `blocked.json` flow plugs in at one DI seam (`postQuestion`).
Round-trip-pure parser/renderer absorbs #178's class (structurally
distinct `question`/`answer` markers) and #180's class (renderer is the
sole writer for strict-marker content). Phase split: file-Epic dispatch
ships first (~2 wk) with a `mm resume` escape hatch; file-watcher Q&A
resume (~1 wk) rides the existing 120s poller cron.
See: docs/superpowers/specs/2026-05-29-file-backed-epic-store-design.md
…se 2 Bite-sized TDD steps, exact files + commands + expected output for each. Phase 1 (~2 wk): rename gateways → schema migrations → parser/renderer → three file gateways → bootstrap selector → postQuestion wiring → mm init/dispatch/doctor/resume → skill refactor → parity test → smoke. Phase 2 (~1 wk): mtime poll helper → file-signal poll on poller cron → parity test for file-edit Q&A resume → smoke. Spec: docs/superpowers/specs/2026-05-29-file-backed-epic-store-design.md
…/Poll) Preparation for the file-backed Epic store: the three existing single- seam DI'd interfaces are renamed to reflect what they abstract (an Epic store, not GitHub specifically). Implementations (ghGitHub, ghStateIssueGateway, ghPollGateway) keep their gh* names — they're the GitHub implementation of the renamed interfaces. Behavior unchanged. - GitHubGateway → EpicGateway - StateIssueGateway → StateGateway - GitHubPollGateway → PollGateway Mechanical codemod across 23 files. 1068 tests pass, typecheck clean.
Additive migration: adds epic_store ('github' default), epics_dir, and
state_file columns. All existing rows default to github mode — zero
behavior change for existing repos. File-mode opt-in is a single config
edit (epic_store.mode = "file" in .middle/<slug>.toml).
The bootstrap selector reads this column to pick the gateway trio at
buildImplementationDeps time. epics_dir / state_file are nullable —
only populated when mode = 'file'.
… (008) Additive migration with backfill: epic_ref TEXT (nullable), populated from CAST(epic_number AS TEXT) for every existing row whose epic_number is non-null. Recommender / documentation workflows have null epic_number and stay null epic_ref. github mode writes both columns; file mode writes only epic_ref (slug). Application-level enforcement in createWorkflowRecord ensures every implementation workflow has it set. epic_number was already nullable — no table rebuild needed.
HTML-comment markers are the structural contract for the round-trip parser/renderer that follows. Type model describes the fully-parsed Epic shape: meta, acceptance, sub-issues, conversation. Every field a renderer needs is on the model so the parser preserves it — required for the byte-identical round-trip invariant. Marker convention mirrors state-issue v1 (<!-- AGENT-QUEUE-STATE v1 -->): the marker IS the structural contract; the renderer is the sole writer of strict attribute lines (closes #180's class of writer/parser drift).
…rsation)
Strict on markers + attributes (the structural contract), lenient on
prose. Throws with a named-marker error when a structural element is
malformed — operators diagnose without log-tailing.
Conversation parser distinguishes dispatch-event vs question, threads
an answer block under its question, and treats an answer's html-comment-
only body as 'placeholder empty' (no resume) vs human-written text
('replied' — the file-watcher trigger).
12 tests covering: empty epic, missing markers, every meta key, checked
+ unchecked sub-issues, provenance suffix, conversation with empty
answer, conversation with non-empty answer.
…erty test renderEpicFile(parseEpicFile(body)) === body for every fixture (empty- epic, codex-adapter, mid-question, all-closed) — byte-identical, first try. Round-trip purity replaces a lock: dispatcher and human can both edit the file (dispatcher patches conversation entries via the renderer; human edits between markers or inside their answer block) without corrupting each other's writes. The renderer is the sole writer of strict-marker attribute lines. Agents/humans write between markers but never inside the attributes — that single-writer rule closes #180's class for the file path.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
getWorkflow now reads the migration-008 epic_ref column into a new WorkflowRecord.epicRef field (slug in file mode, String(epic_number) or null in github mode). Read accessor only — the dispatch write path is unchanged. Unblocks the dashboard bridge emitting epicRef (#187).
…ridge db-deps selects epic_ref and projects it onto RunnerSummary and RunnerPanel (epicRef: string | null); the repo-channel workflow nudge in bridge.ts emits epicRef alongside epic. github-mode rows are unaffected (epic_number drives their rendering). (#187 AC1, AC3)
New EpicRef component: github mode → plain #N (unchanged, no anchor — AC4); file mode → the slug as a file://planning/epics/<slug>.md link (no GitHub link); both-null → caller fallback. Wired into the IN-FLIGHT runner row and the Inspector header, the db-deps.ts:83-fed surfaces the issue cites. Slug is URL-encoded into one safe path segment. (#187 AC2)
EpicRef component (all three render modes + slug encoding), RunnerRow / Inspector rendering, the bridge file-mode emit, and the integration path (real Bun.serve + migrated db): a file-mode workflow row surfaces epicRef over /api/repos/:repo and /api/sessions/:session. seedWorkflow gains an epicRef option. (#187)
897910d to
4396a08
Compare
…/ link A present-but-blank slug (empty or whitespace) took the file-mode branch and rendered <a href="file://planning/epics/.md"></a>. Guard on a trimmed-truthy slug so it falls through to the fallback, and trim surrounding whitespace in the label + href. Self-review hardening within the EpicRef blast radius. (#187)
thejustinwalsh
left a comment
There was a problem hiding this comment.
Decision log distilled inline (source: planning/issues/187/decisions.md). These explain the why behind the diff for the reviewer.
| /** What to render when there's no Epic at all (both ids null). */ | ||
| fallback?: string; | ||
| }) { | ||
| if (epicNumber !== null) return <>#{epicNumber}</>; |
There was a problem hiding this comment.
github mode renders plain #N, not an anchor — on purpose. The issue Context describes the current rendering as "a numeric link to GitHub", but the surfaces in fact render plain #N text today (no <a> exists). AC4 is the hard constraint — no behavior change for github-mode rows — so adding a GitHub anchor would itself be a behavior change. The literal reading wins: github mode keeps its exact pre-#187 plain-text output; file mode is the only new rendering. (toBe("#42") in the test asserts byte-identical markup.)
| * path segment, URL-encoded so a malformed value can't break out of the | ||
| * `planning/epics/` directory or inject markup into the `href`. | ||
| */ | ||
| export function epicFileHref(slug: string): string { |
There was a problem hiding this comment.
file:// href + slug encoding. AC2 names exactly file:// + planning/epics/<slug>.md; the repo's absolute root isn't known client-side, so the path stays repo-relative as specified (yes, file://planning/... parses planning as the URL authority — it's a display affordance, not a guaranteed click-to-open). encodeURIComponent collapses the slug to one safe path segment, so a malformed value (../, quotes, angle brackets) can't traverse out of planning/epics/ or inject markup into the href; a normal kebab-case slug encodes to itself.
| // null; an empty/whitespace value (no real writer produces one today, but a | ||
| // future one could) would otherwise render an empty-labelled link to | ||
| // `planning/epics/.md`, so treat it as "no Epic" and fall through. | ||
| const slug = epicRef?.trim(); |
There was a problem hiding this comment.
Blank-slug guard (self-review hardening). A present-but-blank epicRef ("" / whitespace) would otherwise take the file-mode branch and render <a href="file://planning/epics/.md"></a>. No writer produces a blank value today, but guarding on a trimmed-truthy slug encodes the real intent ("a present, non-blank slug is the file-mode signal") and survives a future writer. Trimming also keeps stray whitespace out of the label and href.
| kind: row.kind as WorkflowRecord["kind"], | ||
| repo: row.repo, | ||
| epicNumber: row.epic_number, | ||
| epicRef: row.epic_ref, |
There was a problem hiding this comment.
Read path only. getWorkflow reads the migration-008 epic_ref column verbatim so the dashboard bridge (AC3) can emit it. The dispatch write path (createWorkflowRecord) is deliberately untouched — populating epic_ref on insert is the file-backed Epic store rollout's job (PR #188 plan, Task 3 Step 4), out of scope here. github rendering keys on epic_number, so a null epic_ref on new github rows is harmless.
Reviewer's brief — PR #189 (closes #187)What this is. The dashboard's
How to run itgit fetch origin && git checkout middle-issue-187
bun install
bun run typecheck # clean
bun run lint # clean
bun test packages/dashboard packages/dispatcher/test/workflow-record.test.ts # 128 passFor the live read path specifically: bun test packages/dashboard/test/api.test.ts -t "file-mode"What to verify (and what "correct" looks like)
How to review
Fragile / extra eyes
|
8a25b90 to
2db1447
Compare
Summary
Closes #187
Plumbs the file-mode Epic identifier (
epic_ref, a slug) through the dashboard's/api/*read plane so file-mode workflows (epic_number IS NULL,epic_ref = '<slug>') render afile://planning/epics/<slug>.mdlink instead of a blank cell. github-mode rows render exactly as before (plain#N).Acceptance criteria
db-deps.tsselectsepic_refand the/api/*response shape gainsepicRef: string | null— proven end-to-end by a realBun.serveintegration test that boots the server against a migrated db and issues a GET to/api/repos/:repo+/api/sessions/:session, asserting a file-mode row surfacesepicRef:api.test.tsfile://planning/epics/<slug>.mdlink whenepicRefis set andepicNumberis null, with no GitHub link in file mode —EpicRefcomponent, used in the IN-FLIGHT runner row + Inspector header; verified byepic-ref.test.tsxepicRefalongsideepic—bridge.tsrepo-channel nudge; verified by a live-SSE test insse.test.ts#N, byte-identical) — verified byepic-ref.test.tsx(toBe("#42")) andapi.test.tsWhat changed
packages/dispatcher/src/workflow-record.ts—getWorkflowreads the migration-008epic_refcolumn into a newWorkflowRecord.epicRef(read path only; the dispatch write path is untouched — see feat(epic-store): foundation — spec + plan + parser/renderer/migrations #188 Task 3).packages/dashboard/src/db-deps.ts,wire.ts— selectepic_ref;RunnerSummary/RunnerPanelgainepicRef: string | null.packages/dashboard/src/app/components/EpicRef.tsx(new) — github → plain#N; file →file://slug link; blank/null → fallback. Used inRunnerRow.tsx+Inspector.tsx.packages/dashboard/src/bridge.ts— the repo-channel workflow nudge emitsepicRef.Why these changes
The issue cites
db-deps.ts:83— the/api/*read plane that reads theworkflows.epic_refcolumn. AC4 is the load-bearing constraint: github-mode rows render plain#Ntext today (no anchor exists), soEpicRefpreserves that byte-for-byte and adds thefile://link only for file mode — adding a GitHub anchor would itself be a behavior change. The slug isencodeURIComponent-collapsed into one safe path segment so a malformed value can't traverse out ofplanning/epics/or inject markup. Full reasoning inplanning/issues/187/decisions.md, distilled into inline review comments on this PR.Verification
All gates green locally:
bun run typecheck— clean.bun run lint/bun run format— clean.bun test— 1105+ pass, 0 fail (full suite). Affected:packages/dashboard/**+packages/dispatcher/test/workflow-record.test.ts→ 128 pass.Per-criterion evidence:
packages/dashboard/test/api.test.tsboots a realBun.serveover a migrated temp db; a seeded file-mode row (epic_numberNULL,epic_refslug) surfacesepicRefoverGET /api/repos/:repo(inFlight[].epicRef) andGET /api/sessions/:session(RunnerPanel.epicRef); a github-mode row carriesepic: 7, epicRef: null.packages/dashboard/test/epic-ref.test.tsx— all three render modes (github#Nno-anchor, filefile://slug link, both-null fallback), blank/whitespace slug → fallback, slug encoding (a/../b→a%2F..%2Fb,<script>neutralized), plus RunnerRow/Inspector rendering.packages/dashboard/test/sse.test.ts— a live SSE connection asserts theworkflownudge carriesepicRef(github row →null; file row → the slug).packages/dispatcher/test/workflow-record.test.ts—getWorkflowround-tripsepic_ref(slug / number-string / null).Self-review
Ran an adversarial clean-eyes review pass over the diff before marking ready. It confirmed the diff clean against the ACs and surfaced one in-blast-radius hardening — a blank (
""/whitespace)epicRefwould have rendered an empty-labelledfile://…/.mdlink — now guarded (trimmed-truthy slug → fallback) with tests. Adjacent surfaces it flagged are out of scope and tracked below.Stumbling points
main. The dispatched worktree's branch already carried the entirefeat/file-backed-epic-storefoundation (PR feat(epic-store): foundation — spec + plan + parser/renderer/migrations #188), so the initial PR-against-maindiff was ~5k lines of someone else's work. Rebased ontoorigin/feat/file-backed-epic-storeand retargeted this PR's base to it, leaving a focused 5-commit diff. (Suggested CLAUDE.md note below.)#N. AC4 ("no behavior change") is the tie-breaker — preserved plain text for github mode.createWorkflowRecorddoesn't writeepic_ref. Migration 008's comment claims it does; it doesn't yet. That's feat(epic-store): foundation — spec + plan + parser/renderer/migrations #188's Task 3 (write path), out of scope here — github rendering keys onepic_number, so a nullepic_refon new github rows is harmless.Suggested CLAUDE.md updates
Consider a note (root or
packages/dispatcher/CLAUDE.md) that issues which are phases of a multi-PR rollout are dispatched on a branch stacked on the foundation PR's branch, so the implementer should retarget the PR base to that branch rather thanmain(and let GitHub auto-retarget on the foundation's merge). This run rediscovered it by inspecting open PRs.Follow-ups (not filed — already tracked by the #188 rollout plan)
The remaining file-mode surfaces are owned by the file-backed Epic store rollout plan (
docs/superpowers/plans/2026-05-29-file-backed-epic-store.md, Tasks 3–4), so filing standalone issues would duplicate planned scope:/control/events(main.tsbroadcastWorkflow→ControlWorkflowFrame) still shows—for file-mode rows — a separate (control-plane) data path the issue doesn't cite.db-deps.ts:307, recommender state-issue ranking) collapses a file-mode slug to#0viaNumber(slug) || 0, andRepos.tsxkeys the list onn.epic— a latent React duplicate-key collision once two file-mode rows both map to#0. Whoever wires NEXT UP for file mode should carry the raw ref throughNextUpItemand key onrank.createWorkflowRecordepic_ref write path — feat(epic-store): foundation — spec + plan + parser/renderer/migrations #188 Task 3 Step 4.Out of scope
mm dispatch --epic <slug>is the path for now).