ADR-097: Project management lives in the factory; warp is the system of record
Status: Superseded by ADR-103 (2026-04-25) Date: 2026-04-25
Superseded note (2026-04-25, same day, ~6 hours later). ADR-103 reverses the operator-UI cut below: the operator-facing PM kanban stays at
warp.gyrum.aiinstead of migrating togyrum.ai/pm, the factory's/pmroute retires (or shrinks to a thin/pm-summarywidget), and warp's web UI is no longer deprecated. Tonight ~30 PRs landed againstgyrum-labs/warp/web/(epic UI, sort, filter persistence, audit timeline, markdown editor, bulk ops, mobile responsive) while the factory's/pmshipped only SWARM-AH's read-only dashboard plus SWARM-AV's epic UI — and the latter was unreachable for the operator due to no GHCR image, no/pmroute, no localStorage token cross-origin. The decision was already de facto reversed by where the work flowed; ADR-103 makes it explicit with ADR-100's audience-driven split rule as structural backing. The system-of-record cut below (warp owns the database and API; the factory consumes vialocalStorage['warp.token']+ cross-origin calls) survives unchanged into ADR-103. Read ADR-103 for the reversed decision; the rest of this ADR is preserved as the historical record of the direction we tried first.
Related: ADR-077 (warp as session-independent agent coordination layer — defines warp's role as the AI-worker inbox), ADR-096 (warp as fleet PM via one-way GitHub-issue mirror — adds the second source of items so warp's view is the fleet's view), ADR-081 (UI consolidation grammar — names the factory as the operator's "everything" surface)
Context
Tonight project management at the fleet became first-class. SWARM-AA shipped epics, parent_id, size, and target_date columns on warp_items; ADR-096 wired the GitHub-issue mirror so warp's queue covers issue-tracked work as well as AI-worker work; SWARM-AH (PR #235) shipped a /pm dashboard route inside the SaaS factory at gyrum-labs/ai-frontend that fetches https://warp.gyrum.ai/api/v1/items browser-direct using the bearer token persisted at localStorage['warp.token'], groups by epic / repo / kind / priority / status / size / target_date, and deep-links each row back to either warp.gyrum.ai/items/<id> or the upstream GitHub URL depending on source. The dashboard is read-only today, but it already proves the shape: the operator gets one filterable, schema-aware view of every open work item across the fleet without leaving the factory chrome.
That proof creates a fork in the road. The operator-facing PM surface now exists in two places: warp.gyrum.ai (the original kanban shipped with the warp service, which knows how to claim, edit, and key-manage) and gyrum.ai/pm (the new dashboard, which lives next to every other operator surface — /operate, /projects, /agents, the command palette, the design-system shell, the persistent auth). Two PM URLs is the same merge-cost ADR-096 just paid down for the GitHub/warp split: every status check costs a context switch, every new operator (or agent) has to learn which URL owns which affordance, every chrome change has to be made twice. The user's framing on this — "we're starting to move to project management and epics and stuff, to fill in coverage of the system, that's why I'm thinking about the factory" — names the resolution: as PM matures, the factory is the right pane. The kanban UI shipped inside warp was the right shape when warp was a single-purpose service; it is the wrong shape now that PM is the operator's primary daily surface.
The cut is between the operator-facing UI (which surface the operator opens and interacts with) and the system of record (which database holds the canonical work-item data and what HTTP API serves it). The two questions look fused but are independent. Tonight we move the UI to the factory while leaving the system of record exactly where it is.
Decision
The operator-facing project-management UI lives in the factory at gyrum.ai/pm/* (routes under gyrum-labs/ai-frontend/src/routes/pm/). The warp service keeps its Postgres database, its HTTP API at warp.gyrum.ai/api/v1/*, its cron jobs, and its operational ownership; what it loses is the standalone web kanban at warp.gyrum.ai/, which becomes deprecated in favour of the factory routes. Warp's role contracts to API + system of record + AI-worker inbox (per ADR-077); the factory takes over operator-facing presentation.
The shape of the cut:
System of record stays on warp-01. The Postgres instance behind warp keeps owning
warp_items,warp_keys, theexternal_id/sourcecolumns from ADR-096, and theparent_id/size/target_datecolumns from SWARM-AA. No data move, no second database, no replication. The factory talks to warp the same way every other client does — the/api/v1/*HTTP surface with a bearer token. Moving data was never the right answer; moving the view is.Browser-to-warp auth continues to ride
localStorage['warp.token']. The token is minted athttps://warp.gyrum.ai/keys(or via the manifest-drivenmint-tokentool from ADR-093 athttps://gyrum.ai/platform/warp/tools), persisted tolocalStorage['warp.token']on the browser's first visit to either surface, and read by both the legacy warp UI and the new/pmdashboard from the same key. The pattern is already shipped — SWARM-AH proved it works — and crucially keeps the factory free of any warp-specific server-side proxy. The browser talks towarp.gyrum.aidirectly; CORS allows it; the factory's SvelteKit pages run client-only (ssr = false) for any route that needs the token. No new auth model gets introduced.The factory's
/pm/*route tree owns every operator PM affordance./pm(read-only dashboard, shipped) →/pm/board(kanban with claim flow + ItemDrawer, port fromwarp/web/) →/pm/keys(token management, port fromwarp/web/) → eventually any future PM affordance (timeline, burndown, calendar) lands here, not inwarp/web/.
The migration is staged across five phases so each step ships independently, each step is reviewable, and the legacy UI keeps working at every checkpoint until its replacement is proven:
- Phase 1 (DONE).
/pmread-only dashboard atgyrum.ai/pm— PR ai-frontend #235. Group by epic / repo / kind / priority / status / size / target_date; client-side filter / sort / search; source-aware deep-links. No write paths yet; warp's web kanban remains the place to claim or edit. - Phase 2.
/pm/board— port the kanban columns (ready/in_progress/done/blocked/cancelled), theItemDrawerdetail panel, the claim/release/heartbeat verbs, and the create-item form fromwarp/web/intoai-frontend/src/routes/pm/board/. Reuses$lib/api-warp.tsfor HTTP, the factory's design tokens for surfaces, the factory's command palette for navigation. After this lands the operator's daily PM workflow runs entirely in the factory; the warp web kanban becomes a fallback for the operator who arrives via awarp.gyrum.ai/items/<id>deep-link. - Phase 3.
/pm/keys— port the key management surface (mint, list, revoke). After this lands no operator workflow requireswarp.gyrum.ai/at all; only the/api/v1/*surface and the deep-link landing pages remain. - Phase 4.
warp.gyrum.ai/(the root web UI, not the API) becomes a redirect-and-shrink. The HTML root and the static assets behind it are deleted from the warp project repo's UI image;warp.gyrum.ai/302s tohttps://gyrum.ai/pm,warp.gyrum.ai/items/<id>302s tohttps://gyrum.ai/pm/board?item=<id>,warp.gyrum.ai/keys302s tohttps://gyrum.ai/pm/keys. The warp project's two-image contract from ADR-092 holds:ghcr.io/gyrum-labs/warp(backend) keeps shipping,ghcr.io/gyrum-labs/warp-uishrinks to annginx:alpineserving redirect rules and nothing else (per ADR-092's "ship a placeholder UI image even if there's no UI" rule). - Phase 5 (deferred). Consider folding
warp.gyrum.ai/api/v1under a unifiedapi.gyrum.ai/warp/*gateway alongside other fleet APIs. Out of scope for this ADR — there is no second API to consolidate with today, and consolidating one API is just renaming. Re-open when there's a second.
Consequences
What becomes easier:
- One operator surface for PM. Open
gyrum.ai/pmand see every work item across the fleet — warp-native + GitHub-mirrored, grouped by whatever axis is relevant today, with the same chrome (sidebar, command palette, mode pill, design tokens) as every other factory page. No second tab, no second login, no second mental model. Status checks, epic browsing, and item triage all happen in the place the operator already is. - The factory's existing infra carries PM's UX for free. Command palette → "Go to Project management" already lands in the right place. The Operate-mode side rail already lights up on
/pm. The persistent auth (cookie-based, set on first visit to anygyrum.ai/*page) sits alongside the warp token without conflict — the factory authenticates the operator's identity, the warp token authorises calls to warp's API. New PM affordances inherit the design system; new factory chrome (a re-themed sidebar, a new command palette entry shape) automatically applies to PM with no warp-side change. - Warp gets to be a smaller service. The web kanban + key UI inside the warp repo is a non-trivial maintenance surface (Svelte components, drag-and-drop kanban, modal forms, key-list pagination). After Phase 4 the warp project owns one Go backend and one nginx-alpine redirect-stub UI; everything operator-facing is one repo away in
ai-frontend. The warp team (which is currently the same team as the factory team, but won't always be) gets a tighter scope. - Future PM features land in one place. Burndown charts, epic-level dependency graphs, target-date timelines, capacity views — none of these have to be designed twice or reasoned about twice. They land in
ai-frontend/src/routes/pm/next to the dashboard and the board, share the same$lib/api-warp.tsclient, and ship under the factory's release cadence.
What becomes harder:
- Two repos to coordinate for any PM-shape change. A new column on
warp_itemsrequires (a) a schema migration ingyrum-labs/warp, (b) the API change in the same PR, (c) a$lib/api-warp.tstype update + UI rendering ingyrum-labs/ai-frontend. Today (Phase 1) the dashboard already feature-detects schema-optional fields (visibleGroupKeys()runs a one-pass detect so older warp deployments don't show toggles that all collapse), so the two-repo dance is low-friction at the dashboard level. Phase 2's kanban will need to be more careful — claim/release verbs and the item-drawer form fields are tighter contracts. Mitigated by the OpenAPI spec gate (warp/contract/) from ADR-092, which already enforces forward-compatible API changes, and by the per-feature pattern of "ship the API change first, then the UI consumer in a second PR." - CORS becomes a contract.
warp.gyrum.ai/api/v1/*must allow cross-originAuthorization-bearing requests fromhttps://gyrum.ai. That's already true (SWARM-AH exercised it) but it's now load-bearing — a misconfigured CORS header on a future warp release breaks the factory's/pmpage silently. Mitigated by adding a contract test inwarp/contract/that verifies theAccess-Control-Allow-Origin: https://gyrum.ai+Access-Control-Allow-Headers: Authorizationheaders appear on a preflight toOPTIONS /api/v1/items. - The deprecation window is real toil. Between Phase 1 (now) and Phase 4 (warp UI gone), both surfaces ship together. The legacy warp kanban stays available — agents and operators with deep-linked bookmarks (
warp.gyrum.ai/items/<id>) keep working — but every new PM feature has to choose one place to land. The rule is simple and one-way: new PM UI ships atgyrum.ai/pm/*; the warp web kanban gets bug-fixes only, no new affordances. Documented at the top ofwarp/web/README.mdso a contributor doesn't add features in the wrong place. - Operator's first-time-on-a-new-machine flow gains one step. First visit: open
gyrum.ai/pm→ see the "Connect to warp first" empty-state CTA (already shipped by SWARM-AH) → click through tohttps://warp.gyrum.ai/keys→ mint a token → paste back into the factory. That's two surfaces for one onboarding, which is a small tax. ADR-093's manifest-driven tools atgyrum.ai/platform/warp/toolsalready smooth this — the operator can mint a token from inside the factory without leaving — but it requires the operator to know to look there. Mitigated by linkinggyrum.ai/platform/warp/toolsfrom the empty-state CTA in a follow-up Phase 2 PR. warp.gyrum.aibrand identity dilutes. The URL the operator types when they think "where do I track work?" was, until tonight,warp.gyrum.ai. After Phase 4 it'sgyrum.ai/pm. The redirect chain handles bookmarks; the muscle-memory cost is a few weeks of mistypes. We accept this — the alternative (keep warp.gyrum.ai as the front door, ship a redirect fromgyrum.ai/pmto it) loses the factory consolidation that motivates the ADR.
What we have signed up to operate:
- A
gyrum.ai/pm/*route tree ingyrum-labs/ai-frontendthat owns every operator PM affordance. Phase 1 shipped (/pm); Phases 2 and 3 are queued in the warp work-item queue. - A long-lived
$lib/api-warp.tsinai-frontendthat wrapshttps://warp.gyrum.ai/api/v1/*with the bearer token, the 60-second TTL cache, the 401-handling, and theitemExternalUrl()source-aware URL routing. This module becomes a maintained client, not a one-shot helper. - A CORS contract on
warp.gyrum.ai/api/v1/*allowinghttps://gyrum.aiorigins withAuthorizationheaders, gated by a contract test inwarp/contract/. - A redirect contract on
warp.gyrum.ai/(ships in Phase 4):/→/pm,/items/<id>→/pm/board?item=<id>,/keys→/pm/keys, all 302s. The redirect targets are stable URL conventions for the factory's PM surface, owned bygyrum-labs/ai-frontend's router. - A documented "new PM UI lands in
ai-frontend, not inwarp/web/" rule at the top ofwarp/web/README.mdand indark-factory/docs/ARCHITECTURE.mdnext to the existing warp paragraph.
What we revisit:
- If Phase 2 reveals that the kanban + ItemDrawer's claim flow needs WebSocket-style real-time updates that are awkward over CORS to
warp.gyrum.ai, the right answer is a thin SvelteKit server endpoint in the factory that proxies to warp (server-to-server, no CORS) — not moving the whole API. Re-open the "factory has no warp-specific server-side proxy" rule above when this happens. - If
localStorage['warp.token']proves brittle for cross-device operators (signal: operators losing their token after a browser data-clear, or wanting the same auth on phone + laptop without re-minting), revisit whether the token should be issued via OIDC against the factory's auth and propagated to warp via a JWKS verification — a real cross-system auth change, not the lightweight one we have today. - Phase 5 (
api.gyrum.aiconsolidation) re-opens the moment a second fleet API exists. Until then it's not a real question.
Alternatives considered
Keep the operator UI in warp; the factory's
/pmis just a status snapshot. Status quo plus SWARM-AH's dashboard. Loses on the "where does new PM UI land?" question — every new affordance forces the operator to context-switch back towarp.gyrum.ai/, and the factory's investment in command palette, design system, and chrome stops compounding for PM. Rejected because the user's framing was explicit: PM is becoming first-class, and first-class surfaces belong in the everything-pane.Move the system of record into the factory's own database. Stand up a new Postgres in the factory's deploy, migrate
warp_itemsacross, retirewarp.gyrum.ai/api/v1/*. Loses on the AI-worker inbox role — the warp protocol (claim/heartbeat/complete withSELECT … FOR UPDATE SKIP LOCKED) is a real concurrency contract that lives at the API layer, and moving it would require either re-implementing it inside the factory's backend or running both backends against one database (which is the worst of both worlds). Also loses on the migration cost: ADR-077's whole point was that warp is the cross-session inbox, and moving its database is a multi-day operation with a real downtime window for every agent on every machine in the fleet. Rejected — the data is fine where it is; only the view needs to move.Keep both UIs forever; let the operator pick. No deprecation, both surfaces shipped indefinitely. Loses on the maintenance bill and on the first-time-operator question ("which URL is canonical?"). Every PM feature has to ship twice or land in only one place and confuse anyone who opens the other. Rejected — two-of-the-same-thing is exactly what ADR-096 spent 200 lines on, and the same logic applies inside the fleet boundary.
Move only the read view; keep all writes in warp's UI. Phase 1 + permanent. Loses on the consequence-side argument: as soon as the operator wants to claim an item or edit a target_date from the factory dashboard, they have to context-switch to the warp UI to do it, which is the exact friction the ADR is trying to remove. Rejected — read-only is the right starting point, not the right ending point.
Build the new PM UI inside warp's repo at
warp.gyrum.ai/v2and redirectgyrum.ai/pmto it. Keeps the UI co-located with the API, no cross-origin concerns, no two-repo dance for shape changes. Loses on every reason this ADR exists: PM is the operator's everything surface, the operator's everything surface is the factory, and shipping the new UI underwarp.gyrum.ailoses the factory consolidation. Also loses on the design-system question — the warp repo doesn't have the factory's components, tokens, or chrome, so we'd either rebuild them (duplication) or vendor them in (drift). Rejected — the URL is the point; the factory is the URL.
Supersedes: none. Amends ADR-077 (warp as session-independent agent coordination layer) by narrowing warp's scope to API + system of record + AI-worker inbox; deprecates the operator-facing kanban shipped at warp.gyrum.ai/. Extends ADR-081 (UI consolidation grammar) by naming gyrum.ai/pm/* as the operator's PM route tree. Composes with ADR-096 (warp as fleet PM via one-way GitHub-issue mirror) — the mirror provides the data; this ADR provides the surface.
Superseded by: ADR-103 (PM stays in warp; the factory is the operations cockpit) — reverses the operator-UI cut while preserving the system-of-record cut.