Skip to content

Latest commit

 

History

History
100 lines (69 loc) · 8.47 KB

File metadata and controls

100 lines (69 loc) · 8.47 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

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.mdthe 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.md in one change and flag it, because every slice depends on it verbatim.

Commands

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 + build are the verification gate. If you add tests, add the runner and wire a test script per workspace.
  • Per-workspace: pnpm --filter @forq/<server|web|cli|shared|api-client> <script> (dev, build, typecheck).
  • pnpm build order is load-bearing: @forq/shared and @forq/api-client are consumed from their built dist/. After editing either, rebuild it (or run its dev watcher) or downstream typecheck/build sees stale types.

Running without credentials (the default)

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_demo1

Architecture

pnpm-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

@forq/shared is the single source of truth

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.

Backend is contracts-first (parallel-build seams)

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 a create*() factory.
  • services.ts constructs the impls and injects them into each other (runs gets the other four).
  • index.ts puts the bag on Hono context under SERVICES_KEY ("services"); routes pull services via c.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() / the fail() route helper; codes in ApiErrorCode, status via ERROR_STATUS).

Capability-gated mock/live (the load-bearing operational pattern)

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_KEYBoxdService mock (status().mock === true, fabricated VM handles)
  • no GITHUB_APP_*GitHubService stubbed (mock repos/issues; ConfigService resolves to defaults)
  • no NEBIUS_API_KEYTriageService returns a deterministic act:true verdict so the pipeline still flows

Read config from the exported env / capabilities, never process.env directly elsewhere.

Runs engine = the state machine

apps/server/src/runs/ owns the RunStatus lifecycle (queued → triaging → provisioning → implementing → testing → {pr_opened | needs_human | aborted | failed}).

  • RunStore is the single mutator of a Run's status; the engine kicks off orchestrators and answers reads (idempotent on the Idempotency-Key).
  • runs/orchestrators/ — one per action (preview / implement / triage / github), driving the state machine on timers and emitting RunEvents.
  • runs/strategies/single-shot (default) vs speculative, behind one FixStrategy interface.
  • Currently stubbed (no real golden provisioned yet): runs/strategies/speculative.ts fabricates a believable best-first fork-tree for the viz (no real search), and boxd/platform.ts (the golden-sync / preview-handler / fix-handler on-VM bash reuse) short-circuits with a descriptive non-zero ExecResult until createGolden stages /opt/boxd-platform. These are the seams to make real — grep for TODO(golden).

SSE

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.

Web

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).

Conventions that bite

  • ESM .js import extensions. module: NodeNext — in server/shared/cli, import sibling source files with the .js extension (import { x } from "./foo.js") even though the file is .ts. Web uses bundler resolution (no extension needed).
  • TS strict + noUncheckedIndexedAccess are on. Index access is T | undefined — handle it.
  • Don't edit package.json dependency 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.
  • .env is gitignored; .env.example is the template. Secrets degrade to mock when absent — keep that invariant.