Turn a sentence into a diagram, edit the diagram, then let Codex implement it one node at a time with tests that have to go green before the next node starts.
Not a single-shot code generator. A visible, editable, per-node iterative build loop you drive from a React Flow canvas.
Brief (a sentence) → Codex drafts a diagram
──────────────────
Diagram (edit on canvas) → nodes, edges, per-node intent/behavior/tests
──────────────────
Build Loop (Start) → topological order, note/group skipped
──────────────────
for each node:
codex writes src + tests ─────┐
↓ │
run vitest │ fail → feed failure into next attempt
↓ │ (up to 3 retries)
done or failed ←─────────┘
You own the diagram. The loop turns each node into code + tests, fails visibly, retries with the test failure in-prompt, and persists progress to .graphcoding/build-state.json so you can stop and resume.
- Brief → Diagram. One sentence, one button. Codex returns a structured diagram with nodes and edges; you edit it on a React Flow canvas with an Inspector panel.
- Per-node build loop. Topological order (ties broken by x-position),
note/groupshapes skipped. Each node goes implementing → done | failed. On failure, the test output is fed into the retry prompt — up to 3 retries. - Real tests. Every node writes vitest tests under
tests/<node-slug>/. The runner pins to the workspace's ownnode_modules/.bin/vitestso it never resolves to a sibling project's version. - Workspace isolation gate. After every Codex attempt, the server rejects external symlinks, host absolute path references, and invalid pnpm
node_modulespackage entries before treating the node as done. - Correct cross-node context. Each node's prompt includes the files produced by every prior completed node, so codex imports from them instead of re-implementing.
- Persistence. Diagram and build state both live on disk under
.graphcoding/(debounced), and auto-restore on workspace open. You can close the tab mid-build and come back. - Stop that actually stops. Clicking Stop aborts the in-flight fetch via
AbortControllerand invalidates a generation counter, so a late response can't overwrite the paused state.
Welcome screen is intentionally two interactive things: a path input with Open and a native-dialog link. Everything else (aux panel, bottom panel, editor tabs) stays hidden until a folder is loaded.
After opening a folder:
- Top: one workflow rail: Target → Generate → Edit → Build.
- Left: Project target, architecture blocks, active docs, and files.
- Center: React Flow canvas + a small bottom panel that starts with just
GRAPH JSON(SPEC / BUILD PROMPT / ITERATION tabs appear only once you've generated a spec). - Right: one detail panel for the currently selected workflow step. The top rail is the only place that switches Generate / Edit / Build.
Spec generation is deliberately collapsed into an optional <details> because the Build Loop does not read the spec — spec is for human review and export.
Requirements:
- Node.js 18+ and npm
codexCLI logged in (codex login statusshould return OK)- A workspace folder you're okay with codex writing into (the sandbox is
workspace-write)
Install and run:
npm install
npm run dev # concurrent server (8791) + vite client (5173)
# or separately:
npm run dev:server
npm run dev:client
# Need a different port? (e.g. 8791 is taken on your machine)
GRAPHCODING_PORT=9100 npm run devOpen http://localhost:5173, type a project folder path, click Open, run the Target wizard that auto-opens, then use Generate to draft the diagram. When the diagram is drafted and edited, switch to Build → Start Build Loop.
Folder /Users/you/Documents/my-messaging-app (empty is fine)
Harness preset SaaS Web App (or whichever fits)
Brief Local-first messaging app with thread list,
chat view, and a calculator popup that can
insert results into the chat.
Expected result ~8–10 nodes; loop builds each with tests.
First node takes the longest (scaffolding).
The first node bootstraps package.json, tsconfig.json, vitest.config.ts, and installs deps. Subsequent nodes only add their own files and tests.
src/
App.tsx main shell; build-loop driver; persistence gates
components/ ExplorerPanel, RunPanel, InspectorPanel,
BuildLoopPanel, BottomPanel, WorkspaceSetupModal
lib/ diagram, harness, workspace, types
styles/app.css VS Code-styled layout, 3-column workbench
server/
index.mjs express API on :8791 (override with GRAPHCODING_PORT)
/api/ai/diagram brief → structured diagram (gpt-5.4)
/api/ai/spec diagram → structured spec (gpt-5.4, optional)
/api/ai/build-order topological sort (note/group skipped)
/api/ai/build-node codex workspace-write + vitest + retry
/api/build-state/save persist .graphcoding/build-state.json
/api/build-state/load auto-restore on workspace open
/api/workspace/validate-isolation external-link/path guard
/api/workspace/* open-folder, read-file, write-artifacts
State lives in three layers:
| piece | react state | localStorage | disk (.graphcoding/) |
|---|---|---|---|
| diagram (nodes + edges) | ✓ | ✓ | diagram.graph.json (debounced 600ms) |
| build-state (per-node status) | ✓ | — | build-state.json (written on every transition) |
| brief text | ✓ | ✓ | — |
| harness config | ✓ | — | harness.json (via write-artifacts) |
On workspace open, disk wins for diagram (falls back to localStorage on read failure), and build-state is restored with running:false, paused:true so nothing resumes without an explicit user click.
Things that broke during real E2E testing and got fixed:
- Vitest resolution leaking to sibling projects.
npx vitestvia pnpm's shared store was picking up a neighbor repo's vitest.detectTestRunnernow invokes<cwd>/node_modules/.bin/vitestdirectly when present. - Stop not stopping. In-flight fetch kept going; late response overwrote paused state. Fixed with
AbortController+ abuildGenRefgeneration counter; every await re-checksisCurrent(). - Stale per-node context. The driver kept a local snapshot of
recordsthat never updated; node N always sawpreviouslyBuilt = []. Fixed by mirroring everyupdateNodeRecordinto the local snapshot atomically. - File diff missing modifications. The
fileslist only captured new files. Replaced with an mtime snapshot before/after so modified files (package.json, shared utils) are reported too. - Path-escape inconsistency.
/api/build-state/save|loadand/api/ai/build-nodebypassedensureWithinRoot; file read/write APIs also needed realpath checks so existing symlinks cannot escape the workspace. These now route through root + realpath guards. - External dependency leakage. Codex once linked
node_modulesto a sibling project. Build-node now runs a workspace isolation gate after every attempt and feeds violations back into the retry loop instead of silently passing. - Reload hang.
reloadNativeWorkspaceerrors inside the loop used to leaverunning:trueforever. Now wrapped in asafeReloadNativeWorkspacethat surfaces a notice and lets the driver continue. - Cross-workspace diagram overwrite. Opening workspace B right after A could write A's nodes into B's disk file. A
hydratedKeyRefnow gates the save effect: only writes after the load effect has confirmed the current key. - Silent 20×
spawnSync.codexStatus()forked a process on every request. Now cached with a 30s TTL;/api/auth/statusstill takes a fresh read.
- Diagram generation and each node build take time —
codexrunsgpt-5.4with reasoning effortmediumby default. Override withGRAPHCODING_CODEX_MODELorGRAPHCODING_CODEX_REASONING_EFFORT. "testing"and"fixing"statuses are defined but the client doesn't currently stream intermediate server events, so the UI jumpsimplementing → done | failed. Server-sent events for in-progress retries is the next UX upgrade.- Fallback diagrams/specs (when codex fails) return with
ok:true. The Build Loop already blocks Start when diagram source is fallback, but the spec path still ships a template silently if codex dies — treat unexpectedly fast spec results with suspicion. - Only tested on macOS with ChatGPT-backed codex login. Windows/Linux should work but the native folder dialog uses zenity/powershell fallbacks that haven't had the same mileage.
node_modules/ dist/ .tmp/ generated/ .graphcoding/ .env*
Before publishing, re-scan for secrets:
rg -n --hidden -S "(api[_-]?key|token|secret|password|sk-|ghp_|github_pat_)" . \
--glob '!node_modules' --glob '!.git' --glob '!dist' --glob '!.tmp' --glob '!generated'Unlicensed. Treat the repository as private until a LICENSE is added.