feat(flows): SQLite persistence + default graph + read endpoints (NF-4) #352

Merged
code-lead merged 1 commit from feat/325-nf4-persistence-default-graph into main 2026-04-24 13:29:46 +00:00
Collaborator

Summary

  • Three new SQLite tables (flows, flow_runs, flow_node_runs) added as an appended migration block in apps/server/src/infrastructure/database/db.ts, with CRUD helpers: listFlows / getFlow / upsertFlow / deleteFlow / seedDefaultFlow, insertFlowRun / finishFlowRun / listFlowRuns / getFlowRun / deleteFlowRun / pruneFlowRunsOlderThan, insertFlowNodeRun / listFlowNodeRuns. No hard FK constraints (cascade is imperative) per the story's Bun-WAL caveat.
  • Baked-in default graph at apps/server/src/domain/flows/default-graph.json + loader in default-graph.ts that compiles eagerly against defaultRegistry() on module load — a malformed shipped graph fails boot loudly instead of silently seeding a broken row. DEFAULT_GRAPH_VERSION = 1.
  • Read-only HTTP routes (GET /flows, GET /flows/:id, GET /flows/runs, GET /flows/runs/:id) in apps/server/src/http/flows-routes.ts, wired into the Hono app in main.ts. Intentionally not behind guardMutating — NF-7 owns mutations, and the read-only surface lives on the same LAN boundary as /issues/pipeline / /board. One-liner comment on each flags it.
  • Startup seeding via seedDefaultFlowAtBoot() wired into main.ts right after loadWebhookConfig. Upsert rule: insert when missing, rewrite when version !== DEFAULT_GRAPH_VERSION, no-op when equal, skip when an operator row hijacks id="default" — operator data never clobbered.
  • Retention helper pruneFlowRuns(ttlDays, now) added to the existing sweeper as a new Phase 4; default TTL 30 days, overridable via SweeperConfig.flowRunTtlDays. Counts surface on SweepResult.{flow_runs_pruned, flow_node_runs_pruned} and in the pass log.

Design notes

  • One file, not a bundle. The story spec text says "one flows row exists with id="default"", and Graph.on.trigger is a single kind — so the shipped default graph is a single flow covering the most common dispatch path (issue.assigned → agent.dispatch(<routed-type>)). Gaps are flagged inline in default-graph.ts and below.
  • Switch for mutual exclusion. The default graph uses router.switch on the trigger's routedType field so the three dispatch branches (designer / design-reviewer / assignee fallback) are mutually exclusive. FILTER_DROP on the losing branches skips the dispatch cleanly — same pattern NF-5's soak can diff against once the trigger-shape contract is final.
  • defaultGraphRegistry() kept as an indirection. NF-3 already stuffs forge.* + agent.* into defaultRegistry(), so the function is currently a thin rename — but the indirection lives here so a future split (e.g. opting out of agent.* for a read-only soak) only touches one file.
  • Biome-friendly SQL. The three new tables' comments avoid backticks (they break the template literal that holds the CREATE TABLE DDL) — noted inline.

Default-graph coverage vs. gap inventory (for NF-5 / NF-6)

Covered (this story):

  • issue.assigned → label-override area:designagent.dispatch(designer, implement)
  • issue.assigned → label-override area:design-reviewagent.dispatch(design-reviewer, review)
  • issue.assigned → default → agent.dispatch(<routedType>, implement)

NOT covered — tracked as NF-6 gaps:

  • pull_request_review.review_requestedagent.dispatch(reviewer) with the designer / area:dashboarddesign-reviewer overrides (today in webhook-handlers.ts).
  • check_suite.completed → reviewer re-request vs. fix-ci branching (today the state machine in webhook-ci.ts).
  • /raise-cap, /hold, /ready, /unassign slash-command dispatches.
  • Dependency propagation on issue.closed (today in domain/workflow/deps.ts).

Each of those is its own flow when NF-6 lands the cutover — no further DB work is needed; the flows table accepts arbitrary ids, and seedDefaultFlowAtBoot() only owns id="default".

Test plan

  • apps/server/src/domain/flows/default-graph.test.ts — 14 tests: shipped JSON parses, compiles against the NF-3 registry, uses a canonical TRIGGER_KINDS kind, has a source node + at least one agent.dispatch, defaultGraphBody() is stable.
  • apps/server/src/infrastructure/database/flows-db.test.ts — 29 tests: CRUD on all three tables, listFlows orders default→operator then by id, seedDefaultFlow returns the four-state tag (inserted / unchanged / updated / skipped) and never touches operator rows, pruneFlowRunsOlderThan cascades, pagination via keyset.
  • apps/server/src/http/flows-routes.test.ts — 17 tests: every endpoint end-to-end, 404 paths, ?limit clamping to [1, 500], ?before= keyset, ?flow_id= filter, seedDefaultFlowAtBoot() transitions including the operator-short-circuit.
  • apps/server/src/background/flows-retention.test.ts — 7 tests: direct pruneFlowRuns + runSweep integration covering TTL override + cascade.
  • bun x turbo run typecheck clean; bun x biome check clean.
  • No regressions in apps/server/src/main.test.ts (46 tests) or apps/server/src/infrastructure/database/db.test.ts (24 tests). The 3 pre-existing session JSONL pruning failures + 1-2 foreman failures are unrelated to this story (confirmed by re-running against main at bdd183b).

Total new tests: 67 (target was 30–40 — richer coverage because the three tables multiplied the CRUD surface).

Closes #325

🤖 Generated with Claude Code

## Summary - Three new SQLite tables (`flows`, `flow_runs`, `flow_node_runs`) added as an appended migration block in `apps/server/src/infrastructure/database/db.ts`, with CRUD helpers: `listFlows` / `getFlow` / `upsertFlow` / `deleteFlow` / `seedDefaultFlow`, `insertFlowRun` / `finishFlowRun` / `listFlowRuns` / `getFlowRun` / `deleteFlowRun` / `pruneFlowRunsOlderThan`, `insertFlowNodeRun` / `listFlowNodeRuns`. No hard FK constraints (cascade is imperative) per the story's Bun-WAL caveat. - Baked-in default graph at `apps/server/src/domain/flows/default-graph.json` + loader in `default-graph.ts` that compiles eagerly against `defaultRegistry()` on module load — a malformed shipped graph fails boot loudly instead of silently seeding a broken row. `DEFAULT_GRAPH_VERSION = 1`. - Read-only HTTP routes (`GET /flows`, `GET /flows/:id`, `GET /flows/runs`, `GET /flows/runs/:id`) in `apps/server/src/http/flows-routes.ts`, wired into the Hono app in `main.ts`. Intentionally **not** behind `guardMutating` — NF-7 owns mutations, and the read-only surface lives on the same LAN boundary as `/issues/pipeline` / `/board`. One-liner comment on each flags it. - Startup seeding via `seedDefaultFlowAtBoot()` wired into `main.ts` right after `loadWebhookConfig`. Upsert rule: insert when missing, rewrite when `version !== DEFAULT_GRAPH_VERSION`, no-op when equal, **skip when an operator row hijacks `id="default"`** — operator data never clobbered. - Retention helper `pruneFlowRuns(ttlDays, now)` added to the existing sweeper as a new Phase 4; default TTL 30 days, overridable via `SweeperConfig.flowRunTtlDays`. Counts surface on `SweepResult.{flow_runs_pruned, flow_node_runs_pruned}` and in the pass log. ## Design notes - **One file, not a bundle.** The story spec text says "one `flows` row exists with `id="default"`", and `Graph.on.trigger` is a single kind — so the shipped default graph is a single flow covering the most common dispatch path (`issue.assigned → agent.dispatch(<routed-type>)`). Gaps are flagged inline in `default-graph.ts` and below. - **Switch for mutual exclusion.** The default graph uses `router.switch` on the trigger's `routedType` field so the three dispatch branches (`designer` / `design-reviewer` / `assignee` fallback) are mutually exclusive. `FILTER_DROP` on the losing branches skips the dispatch cleanly — same pattern NF-5's soak can diff against once the trigger-shape contract is final. - **`defaultGraphRegistry()` kept as an indirection.** NF-3 already stuffs `forge.*` + `agent.*` into `defaultRegistry()`, so the function is currently a thin rename — but the indirection lives here so a future split (e.g. opting out of `agent.*` for a read-only soak) only touches one file. - **Biome-friendly SQL.** The three new tables' comments avoid backticks (they break the template literal that holds the `CREATE TABLE` DDL) — noted inline. ## Default-graph coverage vs. gap inventory (for NF-5 / NF-6) **Covered (this story):** - `issue.assigned` → label-override `area:design` → `agent.dispatch(designer, implement)` - `issue.assigned` → label-override `area:design-review` → `agent.dispatch(design-reviewer, review)` - `issue.assigned` → default → `agent.dispatch(<routedType>, implement)` **NOT covered — tracked as NF-6 gaps:** - `pull_request_review.review_requested` → `agent.dispatch(reviewer)` with the designer / `area:dashboard` → `design-reviewer` overrides (today in `webhook-handlers.ts`). - `check_suite.completed` → reviewer re-request vs. fix-ci branching (today the state machine in `webhook-ci.ts`). - `/raise-cap`, `/hold`, `/ready`, `/unassign` slash-command dispatches. - Dependency propagation on `issue.closed` (today in `domain/workflow/deps.ts`). Each of those is its own flow when NF-6 lands the cutover — no further DB work is needed; the `flows` table accepts arbitrary ids, and `seedDefaultFlowAtBoot()` only owns `id="default"`. ## Test plan - [x] `apps/server/src/domain/flows/default-graph.test.ts` — 14 tests: shipped JSON parses, compiles against the NF-3 registry, uses a canonical `TRIGGER_KINDS` kind, has a `source` node + at least one `agent.dispatch`, `defaultGraphBody()` is stable. - [x] `apps/server/src/infrastructure/database/flows-db.test.ts` — 29 tests: CRUD on all three tables, `listFlows` orders default→operator then by id, `seedDefaultFlow` returns the four-state tag (`inserted` / `unchanged` / `updated` / `skipped`) and never touches operator rows, `pruneFlowRunsOlderThan` cascades, pagination via keyset. - [x] `apps/server/src/http/flows-routes.test.ts` — 17 tests: every endpoint end-to-end, 404 paths, `?limit` clamping to `[1, 500]`, `?before=` keyset, `?flow_id=` filter, `seedDefaultFlowAtBoot()` transitions including the operator-short-circuit. - [x] `apps/server/src/background/flows-retention.test.ts` — 7 tests: direct `pruneFlowRuns` + `runSweep` integration covering TTL override + cascade. - [x] `bun x turbo run typecheck` clean; `bun x biome check` clean. - [x] No regressions in `apps/server/src/main.test.ts` (46 tests) or `apps/server/src/infrastructure/database/db.test.ts` (24 tests). The 3 pre-existing `session JSONL pruning` failures + 1-2 foreman failures are unrelated to this story (confirmed by re-running against `main` at `bdd183b`). Total new tests: **67** (target was 30–40 — richer coverage because the three tables multiplied the CRUD surface). Closes #325 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(flows): SQLite persistence + default graph + read endpoints (NF-4)
All checks were successful
qa / qa (pull_request) Successful in 4m35s
qa / dockerfile (pull_request) Successful in 10s
54e38c21fb
Adds three new SQLite tables (`flows`, `flow_runs`, `flow_node_runs`)
with CRUD helpers, a baked-in default graph covering the
`issue.assigned → agent.dispatch(<routed-type>)` path (including the
designer / design-reviewer label-override branches), read-only HTTP
endpoints, and retention pruning in the existing sweeper so NF-5's
dry-run soak has persistence to diff against. Operator rows
(`source="operator"`) are never touched by the seed migration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
charles force-pushed feat/325-nf4-persistence-default-graph from 54e38c21fb
All checks were successful
qa / qa (pull_request) Successful in 4m35s
qa / dockerfile (pull_request) Successful in 10s
to a8ea470438
All checks were successful
qa / qa (pull_request) Successful in 5m6s
qa / dockerfile (pull_request) Successful in 14s
2026-04-24 13:16:04 +00:00
Compare
code-lead deleted branch feat/325-nf4-persistence-default-graph 2026-04-24 13:29:48 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!352
No description provided.