Decisions

ADR-097: Project management lives in the factory; warp is the system of record

> **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.ai` instead of migrating to `gyrum.ai/pm`, the > factory's…

#097

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.ai instead of migrating to gyrum.ai/pm, the factory's /pm route retires (or shrinks to a thin /pm-summary widget), and warp's web UI is no longer deprecated. Tonight ~30 PRs landed against gyrum-labs/warp/web/ (epic UI, sort, filter persistence, audit timeline, markdown editor, bulk ops, mobile responsive) while the factory's /pm shipped 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 /pm route, 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 via localStorage['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, the external_id/source columns from ADR-096, and the parent_id/size/target_date columns 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 at https://warp.gyrum.ai/keys (or via the manifest-driven mint-token tool from ADR-093 at https://gyrum.ai/platform/warp/tools), persisted to localStorage['warp.token'] on the browser's first visit to either surface, and read by both the legacy warp UI and the new /pm dashboard 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 to warp.gyrum.ai directly; 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 from warp/web/) → /pm/keys (token management, port from warp/web/) → eventually any future PM affordance (timeline, burndown, calendar) lands here, not in warp/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). /pm read-only dashboard at gyrum.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), the ItemDrawer detail panel, the claim/release/heartbeat verbs, and the create-item form from warp/web/ into ai-frontend/src/routes/pm/board/. Reuses $lib/api-warp.ts for 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 a warp.gyrum.ai/items/<id> deep-link.
  • Phase 3. /pm/keys — port the key management surface (mint, list, revoke). After this lands no operator workflow requires warp.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 to https://gyrum.ai/pm, warp.gyrum.ai/items/<id> 302s to https://gyrum.ai/pm/board?item=<id>, warp.gyrum.ai/keys 302s to https://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-ui shrinks to an nginx:alpine serving 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/v1 under a unified api.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/pm and 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 any gyrum.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.ts client, 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_items requires (a) a schema migration in gyrum-labs/warp, (b) the API change in the same PR, (c) a $lib/api-warp.ts type update + UI rendering in gyrum-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-origin Authorization-bearing requests from https://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 /pm page silently. Mitigated by adding a contract test in warp/contract/ that verifies the Access-Control-Allow-Origin: https://gyrum.ai + Access-Control-Allow-Headers: Authorization headers appear on a preflight to OPTIONS /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 at gyrum.ai/pm/*; the warp web kanban gets bug-fixes only, no new affordances. Documented at the top of warp/web/README.md so 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 to https://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 at gyrum.ai/platform/warp/tools already 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 linking gyrum.ai/platform/warp/tools from the empty-state CTA in a follow-up Phase 2 PR.
  • warp.gyrum.ai brand 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's gyrum.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 from gyrum.ai/pm to it) loses the factory consolidation that motivates the ADR.

What we have signed up to operate:

  • A gyrum.ai/pm/* route tree in gyrum-labs/ai-frontend that 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.ts in ai-frontend that wraps https://warp.gyrum.ai/api/v1/* with the bearer token, the 60-second TTL cache, the 401-handling, and the itemExternalUrl() source-aware URL routing. This module becomes a maintained client, not a one-shot helper.
  • A CORS contract on warp.gyrum.ai/api/v1/* allowing https://gyrum.ai origins with Authorization headers, gated by a contract test in warp/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 by gyrum-labs/ai-frontend's router.
  • A documented "new PM UI lands in ai-frontend, not in warp/web/" rule at the top of warp/web/README.md and in dark-factory/docs/ARCHITECTURE.md next 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.ai consolidation) 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 /pm is 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 to warp.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_items across, retire warp.gyrum.ai/api/v1/*. Loses on the AI-worker inbox role — the warp protocol (claim/heartbeat/complete with SELECT … 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/v2 and redirect gyrum.ai/pm to 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 under warp.gyrum.ai loses 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.