This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
forq — "fork the queue." A self-serve dashboard + standalone TypeScript backend that turns a GitHub repo's issue/PR backlog into a queue you burn down on boxd cloud VMs: spin preview environments, triage cheaply, implement fixes with coding agents, open PRs. The headline mode (speculative) live-forks one running VM into many candidate fixes and lets the test suite collapse the tree onto the green patch.
Two docs are authoritative — read them before non-trivial work, and prefer updating them over re-deriving their contents here:
PLAN.md— product design, the four actions, the speculative strategy, milestones.CONTRACTS.md— the coordination contract. Directory-ownership map, the full API endpoint table, backend service interfaces, the UI component inventory, and design tokens. Treat it as frozen-by-default: to change a contract, edit@forq/shared+CONTRACTS.mdin one change and flag it, because every slice depends on it verbatim.
pnpm install
pnpm dev # server (:8787) + web (:5173) concurrently
pnpm dev:server # backend only (tsx watch)
pnpm dev:web # dashboard only (Vite)
pnpm dev:cli # CLI via tsx
pnpm typecheck # all workspaces, strict
pnpm build # ORDER MATTERS: shared -> api-client -> (server, web, cli)
pnpm clean # rm dist / tsbuildinfo / caches- No test harness is configured yet — no test runner, no
*.test.ts.typecheck+buildare the verification gate. If you add tests, add the runner and wire atestscript per workspace. - Per-workspace:
pnpm --filter @forq/<server|web|cli|shared|api-client> <script>(dev,build,typecheck). pnpm buildorder is load-bearing:@forq/sharedand@forq/api-clientare consumed from their builtdist/. After editing either, rebuild it (or run itsdevwatcher) or downstream typecheck/build sees stale types.
The whole stack comes up with an empty .env — every secret is optional at boot and its absence degrades that service to mock/stub. cp .env.example .env and fill in only what you need.
# web defaults to MOCK mode (renders the full UI with no server)
pnpm dev:web # set VITE_FORQ_MOCK=false to hit the backend
# CLI against mock data (no server)
node apps/cli/dist/index.js --mock --json repos list
node apps/cli/dist/index.js --mock runs tree run_demo1pnpm-workspaces monorepo, pnpm 10 / Node ≥24 / TypeScript 5, ESM everywhere.
packages/shared @forq/shared domain types + zod schemas + API contracts + RunStatus + ForqConfig (single source of truth)
packages/api-client @forq/api-client typed client (one method/endpoint) + SSE subscribe() + mock mode (consumed by web + cli)
apps/server @forq/server Hono + zod-openapi + boxd SDK + octokit + Nebius triage. REST/JSON + SSE under /api/v1
apps/web @forq/web React 19 + Vite + Tailwind v4 SPA dashboard (dark-default, monospace-forward)
apps/cli @forq/cli commander tree mirroring the API
All domain types, zod schemas, request/response shapes, RunStatus, and ForqConfig (the forq.yml schema) live in packages/shared/src/. Never redefine a shape locally — import it. Each schema is exported as a named PascalCase value and its inferred type under the same name (export type X = z.infer<typeof X>), so import { Run } from "@forq/shared" gives you both the schema and the type. The server validates against these, the client/web/cli consume them, and the OpenAPI doc is generated from them.
apps/server/src/contracts.ts defines the five service interfaces (BoxdService, GitHubService, ConfigService, TriageService, RunsEngine) and the Services bag. The design lets the slices be built independently:
- Each slice owns one directory (see the ownership map in
CONTRACTS.md) and exposes acreate*()factory. services.tsconstructs the impls and injects them into each other (runsgets the other four).index.tsputs the bag on Hono context underSERVICES_KEY("services"); routes pull services viac.get("services")and never import concrete impl classes. Route files validate input with a shared zod schema → call the service →ok()/fail().- The error envelope is exactly
{ error: { code, message, details? } }everywhere (apiError()/ thefail()route helper; codes inApiErrorCode, status viaERROR_STATUS).
apps/server/src/env.ts validates env and derives a capabilities flag set from which credentials are present (boxd, github, githubWebhooks, triage). Each create*() factory picks live vs mock off those flags — so the orchestrator and every route run end-to-end with zero credentials:
- no
BOXD_API_KEY→BoxdServicemock (status().mock === true, fabricated VM handles) - no
GITHUB_APP_*→GitHubServicestubbed (mock repos/issues;ConfigServiceresolves to defaults) - no
NEBIUS_API_KEY→TriageServicereturns a deterministicact:trueverdict so the pipeline still flows
Read config from the exported env / capabilities, never process.env directly elsewhere.
apps/server/src/runs/ owns the RunStatus lifecycle (queued → triaging → provisioning → implementing → testing → {pr_opened | needs_human | aborted | failed}).
RunStoreis the single mutator of a Run'sstatus; the engine kicks off orchestrators and answers reads (idempotent on theIdempotency-Key).runs/orchestrators/— one per action (preview / implement / triage / github), driving the state machine on timers and emittingRunEvents.runs/strategies/—single-shot(default) vsspeculative, behind oneFixStrategyinterface.- Currently stubbed (no real golden provisioned yet):
runs/strategies/speculative.tsfabricates a believable best-first fork-tree for the viz (no real search), andboxd/platform.ts(thegolden-sync/preview-handler/fix-handleron-VM bash reuse) short-circuits with a descriptive non-zeroExecResultuntilcreateGoldenstages/opt/boxd-platform. These are the seams to make real — grep forTODO(golden).
Live run telemetry is RunEvent (discriminated by type: status | log | tree | result | error | heartbeat). RunsEngine.subscribe(runId, onEvent) is the source; the SSE route bridges it to text/event-stream (event: run-event, data: <JSON>, id: <seq>). seq is monotonic per run — clients resume with Last-Event-ID.
React 19 SPA, statically served, no SSR. All server state via @tanstack/react-query over a single @forq/api-client instance — never fetch the API directly past the client. Mock mode is the default (VITE_FORQ_MOCK unset/true → client returns shared-shaped fixtures and animates SSE with no server); VITE_FORQ_MOCK=false hits the backend and Vite proxies /api + /openapi.json → :8787. UI is monospace-forward, dark-default, one accent color, hairline borders — use the Tailwind token utilities from styles/theme.css (bg-surface, text-strong, text-accent, …); never hardcode colors. Component inventory + design tokens are specified in CONTRACTS.md (d) and (e).
- ESM
.jsimport extensions.module: NodeNext— in server/shared/cli, import sibling source files with the.jsextension (import { x } from "./foo.js") even though the file is.ts. Web uses bundler resolution (no extension needed). - TS strict +
noUncheckedIndexedAccessare on. Index access isT | undefined— handle it. - Don't edit
package.jsondependency lists. Every dep each workspace needs is already declared; if something's genuinely missing, flag it rather than adding it ad hoc. - Stay in your slice's directory when doing parallel build work (ownership map in
CONTRACTS.md(a)). Foundation-owned files (root config,tsconfig.base.json,packages/**, the server bootstrap{index,env,contracts,services,openapi}.ts, the web shell{main,App}.tsx+ theme) change only with coordination. .envis gitignored;.env.exampleis the template. Secrets degrade to mock when absent — keep that invariant.