Plan Engine — the dev-workflow graph (live)
canonical· tech-lead · updated 2026-09-05 · source
This describes what IS. The Plan Engine is live: a Postgres graph that is the daily-driver source of truth for plans + tasks, editable and actionable from both Claude (MCP) and the admin UI, kept bidirectionally in sync with GitHub issues/PRs and Vercel deploys. Decision records:
decisions/adrs/ADR-004-plan-engine.md(plans → graph),decisions/adrs/ADR-005-tasks-on-graph.md(tasks → graph, GitHub mirrored),decisions/adrs/ADR-006-effort-workflow-trace.md(effort schema = training-trace schema). Built as effort #560.
1. What it is
One graph, three views, actionable from two clients. The graph is the canonical end-state SoT;
GitHub stays a synced mirror (elimination deferred — see ADR-005). It is the dev-workflow
graph and must never mix with tenant/client data (see §7 and .claude/rules/workflow-vs-product.md).
- Package:
@tedos/db(packages/db) — the Drizzle/Postgres schema + migrations. - API: the Plan API in
@tedos/api(apps/api), prefix/plan/*. - MCP: 9
plan_*tools inapps/api/src/mcpwrapping the same service (one engine, two clients). - Admin:
/plan(editable tree),/roadmap(timeline), and/work(repointed to the graph).
2. Data model — 5 tables (Postgres plan schema)
@tedos/db lives in a dedicated plan Postgres schema (isolating it from @tedos/api's own
mission_snapshot migration journal on the same DATABASE_URL).
| Table | Purpose | Key columns |
|---|---|---|
| plan_node | the tree | id, parent_id, kind(effort·sub·milestone·roadmap·note), title, body, status(draft·todo·in_progress·blocked·done·abandoned), role, priority, timeline_start/due, position, outcome_label(positive·negative·gold), created_at/updated_at |
| plan_dependency | the DAG | source_id, target_id, relation(depends_on·blocks), unique(source,target,relation); cycle-guard on insert |
| plan_version | append-only history | node_id, changed_at, actor, op(create·update·status·link·move·delete), before/after jsonb — every write appends a row |
| plan_link | external work | node_id, kind(issue·pr·deploy·branch), provider(github·vercel), external_id, url, state, synced_at, unique(node_id,kind,external_id) — the issue#↔node map |
| sync_event | idempotency / loop-prevention | source(github·vercel·graph), external_id, payload_hash, processed_at, unique(source,payload_hash) |
The schema is also the training-trace dataset schema (ADR-006): plan_version is the
trajectory store, outcome_label carries the auto/gold reward signal.
3. API + MCP surface
- Plan API (
apps/api, Fastify v5, validated with@fastify/type-provider-zod): CRUD for nodes / dependencies / links, plustree,roadmap, andhistoryprojections. Operator-gated. Every write appends aplan_versionrow by construction. - MCP server (
apps/api/src/mcp): 9plan_*tools that wrap the same service layer — Claude and the admin hit one engine, two clients. No second implementation (.claude/rules/kit-engine-boundary.md).
4. Sync bridge + loop-prevention
Bidirectional, so the graph (SoT) and GitHub (mirror) never drift:
- graph → GitHub (mirror): pg-boss jobs propagate node create/update/status to GitHub issues + Projects v2 status.
- GitHub → graph + Vercel → graph: ingress webhooks (HMAC-verified) update the graph;
Vercel deploys land as
plan_link(deploy). - Reconciliation: last-write-wins (LWW) with a drift sentinel;
sync_event(unique(source,payload_hash)) dedupes; loop-prevention via origin tags + echo-skip (a change the bridge itself made is not re-emitted).
5. Admin views (3)
/plan— editable plan tree + node-detail editor (create/link issue, start, status)./roadmap— thetimelineprojection of the same graph (aligned by construction; not a doc)./work— repointed from live GitHub GraphQL to the graph (the "where we stand" surface).
All render the 4 mandatory states (loading/empty/error/success). These are builder-platform pages; the Paper design pass is deferred (flagged for Designer review).
6. Deploy topology
Deployed to Fly app tedos-api (the same app as Comprender E1 — see
comprender-mvp-e1.md), with two process groups:
- web — the Fastify API (
/plan/*, webhooks, MCP). - worker — the pg-boss queue draining the graph→GitHub mirror jobs.
7. DB-separation decision + upgrade path
Decided (recorded here as the durable home): the plan training graph and the product
system-of-record currently share one Fly MPG (managed Postgres) cluster, isolated by Postgres
schema (plan vs product).
- Why it's acceptable now: it honors ADR-006's intent — the training corpus is kept apart from tenant PII — and no real tenant data exists yet.
- It is a documented compromise, not the end state. ADR-006 wants the training corpus physically apart from tenant data.
- Upgrade path (tracked): move the plan graph to a dedicated cluster before any real
tenant-PII volume lands, or before any external training-data export. Until then: schema
isolation + the hard rule that no
plan_*table ever holds tenant/client data. - Host migration in flight (effort #1604): a Fly→Render Postgres migration + rollback design
(design-only, not yet executed) is a candidate moment to also resolve this upgrade path — see
render-postgres-migration-plan.md.
8. Related
decisions/adrs/ADR-004-plan-engine.md— plans → graph.decisions/adrs/ADR-005-tasks-on-graph.md— tasks → graph, GitHub mirrored.decisions/adrs/ADR-006-effort-workflow-trace.md— effort schema = training-trace schema.comprender-mvp-e1.md— tenant #1; shares the Flytedos-apiapp..claude/rules/kit-engine-boundary.md— one canonical home per workflow op..claude/rules/effort-model.md— the effort = unit of work + of training trace.