You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
plan plans, go goes — and the first hour finally has a guide
One day's worth of making the product mean what it says:
- The live view is a designed SVG dashboard (no CDN, packaged frontend):
per-node status/tokens/cost, goal in the header, violet proposed nodes,
approval banner, replay scrubber; goal and approve-command ride the trace.
- plan/go split: `plan` proposes + admits + saves plan.json and STOPS;
`go` executes the newest saved plan (re-admitted on the way in);
`plan --go` is the one-shot. Registry chain: flag/config > ./registry.py
> built-ins, with `--default` forcing the built-ins.
- No silent scripts in real commands: a model is required; `--scripted`
is the explicit, labeled rehearsal.
- Local-model planning made reliable: <think>-block-safe JSON extraction,
slim proposal schema for Ollama's grammar decoder, retry notes carrying
the offending reply + a diamond worked example (so 8B models fan out
independent work instead of chaining it), --model-arg/--workspace/
--max-planning-failures, and a registry-keyed generated-policy cache
(a stale policy could silently unlock the mutating kind).
- Onboarding: bare `grapharc` orients, `help` works, `start` explains
every term in plain words, `init` scaffolds a registry whose first free
run demonstrates refuse-then-admit and whose gather does real work.
- Traces default under .grapharc/runs; serve writes a discovery marker;
plan/go always print a probed `watch :` URL. Public site under docs/site
with a Pages workflow; README repositioned around the admission gate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+6Lines changed: 6 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -17,6 +17,12 @@ Entries are newest-last within a release, matching the order they were written.
17
17
- the `/live` **token was accepted in the query string on every route**, and a URL is the one place a secret cannot be taken back from: the uvicorn request line, the nginx access log, browser history, and the referrer of anything the page opens. The index made it worse by writing the token into every link it rendered, so clicking a trace filed the secret in history a second time. It is refused off `/live/api/stream` now — that route keeps it because a browser `EventSource` cannot set a header and has no other way in — with a 401 whose reason says *where* to put the token rather than that it is wrong. A browser gets a sign-in page instead of a bare 401 and trades the token for a cookie: a SHA-256 digest of it rather than the token itself, `HttpOnly`, `SameSite=Strict`, scoped to `/live`, and always ASCII, so a non-ASCII secret survives the latin-1 header encoding that a `Bearer` header cannot. Links carry no token at all. The residual exposure — the SSE request line — is now named in the cookbook next to `--live-token`, with what to scrub. Every confinement the reader already enforced is untouched: `../`, `%2e%2e%2f`, absolute paths, NUL bytes and symlinked traces are the same 404s, and a hostile token is still a 401 rather than a crash. (#41)
18
18
- the live page was **blind for the whole planning phase**, which is where a governed run spends its budget and does its refusing. `plan`, `admission` and `round` events were on disk — 2,081 tokens spent before any node ran, in the report — and the page rendered none of them, because it keys the graph off the `topology` event that only lands once a round is admitted and materialised. A run refused on every round produces no topology at all, so the most governance-relevant run there is showed nothing from start to "finished". The snapshot now carries a `planning` block folded from those same events (no new trace events): per round, the proposal size, the admission status, the checks that failed and the rejection codes, the planner tokens, and whether it executed; plus the loop's stop reason and detail when it stopped without a graph. The page renders it as a panel, and a round that has begun and not closed reads as *active* rather than idle — a planner mid-inference writes nothing for a minute at a time, which is exactly the "is it thinking or is it wedged?" the report describes. A run that never planned has no `planning` field and renders exactly as before. (#47)
19
19
- a finished trace **rendered as a done deal**: instantly all-green, with the amber `running` styling unreachable for every run that is already over — and for any live run whose nodes finish between two SSE polls. `?replay=1` on the stream walks the recorded events in timestamp order and emits the snapshots the run would have sent, so a node is amber for its recorded window and green after; `&speed=N` divides the wall clock and the whole replay is capped at 40 seconds, so a 40-minute incident trace is watchable. Frames are rebuilt by the same snapshot code a live stream uses, pointed at a prefix of the file, and depend on no clock: a trace replayed twice renders identically. Without the parameter nothing changed. (#48)
20
+
- a qwen3-class model's **`<think>` block could beat its own answer**: JSON extraction ranked object spans longest-first, so a longer draft inside the reasoning block outranked the real reply outside it, and a fenced draft inside the block won outright (only the first fence was ever tried). The visible text — reasoning tags stripped — is scanned first now, the original text is a fallback tier (a reply that is *entirely* think-block still parses, and a `<think>` inside a JSON string is data, because a reply that already parses whole is never rewritten), every fence is tried in order, and a trailing comma is repaired only on candidates that already failed to parse — a trailing comma is never valid JSON, so no valid document can be rewritten.
21
+
- the planner pushed **`Subgraph`'s own JSON schema at local grammar-constrained decoders** — recursive (`ProposedNode.subgraph → Subgraph`), every field required under strict mode including the `proposal_id`/`origin` it discards on arrival, ~3.5 KB of embedded docstrings — and small models reliably choked on it. Backends now declare `reliable_structured_output`; Ollama says no and gets the text path: a three-key slim shape (`nodes`, `edges` — pair, object and from/to forms all accepted — `rationale`) with a worked example in the prompt, re-validated through the real constructors so admission judges exactly what it always judged. A parse failure's retry note now shows the model a truncated snippet of its own reply plus the example, instead of a bare error string; `--max-planning-failures` makes the allowance operator-settable.
22
+
- the generated-policy cache **was not keyed by registry**, so a `.grapharc/generated-policy.toml` written for the incident demo (`deny *->deploy`) silently governed a later stdlib run — overriding stdlib's own `deny *->apply_change` and making the mutating kind reachable with no operator decision anywhere. Generated policies are keyed by registry target now (`generated-policy.<slug>.toml`); a legacy un-keyed file is never honoured implicitly when the run can say which registry it is — the run falls through to generation or the registry default and *says so* — and the file survives untouched for an explicit `--policy`.
23
+
-**the CLI never joined the runs it starts to the live view that draws them.**`plan`'s default trace went to a tempdir no server serves, and no command printed a URL. Defaults compose now: traces land under `.grapharc/runs/<stamp>/`, `serve --live-root` writes a discovery marker (`.grapharc/live-server.json` — URL, root, pid, never the token; removed on clean shutdown), and `plan`/`go` end with a `watch :` line — the exact page URL when a marker names a server that answers one loopback connect, the command that would start one otherwise. The goal now rides the loop's topology and approval events (operator-supplied text, deliberately shown — the second state field after `termination_reason`), so the page can say what a graph is *for*; a parked run shows its proposed nodes in violet with a copy-ready `grapharc approve <dir>` banner.
24
+
-**`plan` planned nothing and executed everything** — the name lied. The verbs are split now: `grapharc plan` proposes, the gate admits, and the run STOPS with the admitted plan saved to `plan.json` next to its trace (exit 0, `stopped: planned`); `grapharc go` executes the newest saved plan (`go <run-dir>` for a specific one), replaying the stored proposal through the full governed loop so admission judges it again on the way in — a hand-edited plan.json is a new proposal, not a pre-approved one; `plan --go` (and `go "a goal"`) does both in one run. Looking at a plan and then typing `go`*is* the approval; `--approve` remains for parking one-shot runs mid-flight. Registry resolution is now one visible chain shared by both commands: flag/config first, else a `registry.py` in the directory (yours wins), else the built-in general-purpose kinds — with `--default` forcing the built-ins past everything.
25
+
-**new commands for the first hour**: bare `grapharc` orients instead of erroring (exit 0); `grapharc start` is the guided tour; `grapharc init` scaffolds a commented `registry.py` (whose first free run reproduces refuse-then-admit), a `grapharc.toml`, and `.grapharc/runs/` — refusing to overwrite either authored file, with no `--force`; `grapharc go` is plan with doing-defaults (the stdlib tool-using registry, `--model` required); `--registry path/to/file.py:attr` loads a registry file directly; `--workspace` confines the stdlib kinds' tools to a directory (refused when a registry cannot take one — never silently un-confined); `--model-arg KEY=VALUE` reaches the backend constructor. The stdlib planner is now *told* the deterministic completion rule (end with `summarize`) instead of discovering it by burning rounds.
**A governed agent runtime built on [LangGraph](https://github.com/langchain-ai/langgraph).**
16
+
**The admission gate for agent graphs** — a governed agent runtime built on [LangGraph](https://github.com/langchain-ai/langgraph).
17
17
18
-
Build production-grade multi-agent systems with built-in safety, auditability, and control. GraphARC adds a governance layer on top of LangGraph: a planner *proposes* a subgraph, a deterministic checker *admits* it, and only then does anything execute. Every transition is permitted, every loop is bounded, and afterwards you can prove what happened and why it stopped.
18
+
A planner *proposes* a subgraph, a deterministic checker *admits* it — or refuses with reasons — and only then does anything execute. Every transition is permitted, every loop is bounded, and afterwards you can prove what happened and why it stopped. Three things this package does that you will not find together anywhere else:
19
19
20
-
**Status:** early days (`0.1.3`) — the API is not stable yet. Known limits are listed in [Status and limits](#status-and-limits); closed ones are in [CHANGELOG.md](CHANGELOG.md).
20
+
-**No step runs unless a deterministic gate admitted it — and it tells you why when it refuses.** Five checks on every proposal, every failure a structured rejection with a code and a remedy, work discovered mid-run re-entering the same gate. `grapharc run --check-only` is the gate as a linter: it can refuse without executing anything.
21
+
-**The worst-case cost is known before a graph runs, and the exact per-node bill after — even when it fails.** Admission prices the worst case against what is *left* of the budget; at runtime each node's spend lands on its own trace event, error and cancellation included. `recorded_cost_usd` is never an estimate.
22
+
-**One append-only JSONL trace file is the whole truth.**`replay`, `diff`, `metrics`, `viz`, cost attribution, OTel export and the live browser view all read the same file — the dashboard cannot disagree with the audit trail, because they are the same record.
23
+
24
+
**Website:**[codegraphcontext.github.io/GraphARC](https://codegraphcontext.github.io/GraphARC/) · **Status:** early days (`0.1.3`) — the API is not stable yet. Known limits are listed in [Status and limits](#status-and-limits); closed ones are in [CHANGELOG.md](CHANGELOG.md).
21
25
22
26

23
27
24
28
*One question in, a governed graph out: a local model proposes the topology, the admission gate and a human approval decide, and the live view shows every node run — amber while executing, green when done. ([full-quality mp4](docs/media/grapharc-decompose.mp4))*
25
29
26
30
> *Graph engineering*: when one agent loop stops being enough, coordination becomes the engineering. Nodes do work (agent loops, model calls, deterministic functions, humans approving things), edges decide what runs next, and a typed shared state flows between them. GraphARC implements the discipline that makes such graphs production-grade rather than demos — the ideas emerging from the July 2026 loops-vs-graphs debate (Steinberger, Ng, et al.), the "Two Graphs, Two Jobs" split, and twenty years of pre-AI graph systems where every edge means something and every path can be explained.
27
31
32
+
## Where it sits
33
+
34
+
None of these is a competitor to be beaten — they do different jobs, and GraphARC's [design lineage](#design-lineage) borrows from two of them. This is the row-by-row difference:
35
+
36
+
|| GraphARC | Claude Code | OpenClaw | raw LangGraph |
| Who authorizes work | A deterministic admission gate, pre-execution, with reasons | A human, live, per action | Configuration and allowlists | Nobody — convention |
40
+
| Cost control | Worst-case admission + per-node attribution, fail-closed | Usage visibility | Spend settings | None built in |
41
+
| Audit | One replayable JSONL trace; replay/diff/cost read it | Session transcripts | Logs | Checkpoints (state, not *why*) |
42
+
43
+
Claude Code is a great agent — GraphARC's default backend drives the Claude CLI. OpenClaw is a great gateway to agents — GraphARC borrowed its policy-before-schema tool gating and put it behind enforcement. GraphARC is the layer that decides what an agent system is *allowed* to do: before it does it, with receipts after.
grapharc plan "look into the outage" --model ollama/qwen3:8b # plan ONLY: propose -> admit -> save
97
+
grapharc go # execute the newest saved plan (go <run-dir> for a specific one)
98
+
grapharc go "fix the flaky import" --model ollama/qwen3:8b # plan AND execute, one shot
99
+
grapharc plan "..." --scripted # free rehearsal: stand-in planner, no AI
100
+
grapharc plan "..." --go --approve # one-shot, parked mid-run until a human answers
78
101
grapharc approve <trace># answer a parked run (--deny to refuse)
79
102
grapharc run graph.json # a topology you wrote, through the same gate
80
103
grapharc run graph.json --check-only # admission as a linter; executes nothing
@@ -91,7 +114,7 @@ grapharc replay <path> <run-id> # reconstruct a run from its trace
91
114
grapharc diff <path><a><b># what changed between two runs
92
115
```
93
116
94
-
Twelve commands, and every one of them takes `--json` — in JSON mode the failure is the document rather than a line on stderr. Exit codes are part of the interface: `0` did the job, `1` ran and the answer was negative (an agent stopped short, a run id had no events, two runs differed), `2` could not run at all.
117
+
Fifteen commands, and every one of them takes `--json` — in JSON mode the failure is the document rather than a line on stderr. Exit codes are part of the interface: `0` did the job, `1` ran and the answer was negative (an agent stopped short, a run id had no events, two runs differed), `2` could not run at all.
95
118
96
119
The Slack bot puts most of these commands one `/grapharc …` away from a phone, behind an allowlisting gate that keeps the default spend at zero — setup in [docs/cookbook/07-slack.md](docs/cookbook/07-slack.md), and a command-by-command session, refusals included, in [docs/cookbook/08-slack-walkthrough.md](docs/cookbook/08-slack-walkthrough.md). A tracing command run from Slack is narrated live — one status message edited in place as nodes run, with a refreshed diagram link — and `grapharc serve --live-root` adds a browser page that redraws the orchestration graph in real time over SSE.
97
120
@@ -130,12 +153,12 @@ The part with no prior art to copy, and the reason the rest exists. You cannot p
130
153
Watch it happen first. This costs nothing and needs no key — the shipped planner is scripted, and its first proposal names the policy-denied `deploy` kind:
131
154
132
155
```bash
133
-
grapharc plan "investigate the checkout outage"
156
+
grapharc plan "investigate the checkout outage" --scripted --go
@@ -151,6 +174,8 @@ state : goal='investigate the checkout outage' notes=['triage ran', 'patch r
151
174
152
175
Round 1 wanted to deploy and **never executed**. Round 2 went through the *same* checker and ran.
153
176
177
+
The output ends with a `trace :` path — under `.grapharc/runs/` by default — and a `watch :` line. With `grapharc serve --live-root .grapharc/runs` running in another terminal, that line is the exact URL of this run's live page (proposed graph in violet awaiting approval, amber while nodes run, green when done, replay scrubber after); without one, it is the command that starts it.
178
+
154
179
The `policy` line ends in `[registry-default]` — that is the **provenance**, and it is on the JSON payload too as `policy_source`. It matters because a policy can now come from four places: a `--policy` flag, a `grapharc.toml`, one an LLM generated on a first run, or the registry's own default. A generated run and an authored one look identical on the command line, so the source is the only thing that tells them apart afterwards.
155
180
156
181
`--model SPEC` swaps in a real backend and changes none of the enforcement; `--policy policy.toml` moves the rules into a document.
0 commit comments