Skip to content

Commit f767b6f

Browse files
Complete governed planning CLI and documentation
1 parent 78dd36f commit f767b6f

27 files changed

Lines changed: 2016 additions & 159 deletions

ARCHITECTURE.md

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -366,31 +366,36 @@ already-approved path.
366366

367367
### The gaps that matter
368368

369-
The first four are seams: a subsystem that exists and works, sitting next to a
370-
subsystem that does not know it exists. They are ROADMAP §12.
371-
372-
1. **Nothing shipped drives the loop.** `grapharc.planner` is imported by no
373-
other module in the package: no CLI command, no example graph, no session
374-
graph. The cycle above is a library API proven by tests and by the run
375-
described above — not something a reader can invoke and watch. Until there
376-
is a `grapharc plan`-shaped entry point, ①→③ is a diagram, not a path.
377-
2. **Policy does not reach admission.** `AdmissionChecker` takes an `EdgePolicy`
378-
assembled in Python. `grapharc.policy` parses a TOML document that already
379-
understands `node`, `edge`, `tool` and `spend` rules — and there is no bridge
380-
between the two, nor any caller of either from the agent or the CLI. Box ④
381-
is enforced by code an operator writes in Python, not by the declarative
382-
document that was built for it.
369+
One seam is left: a subsystem that exists and works, sitting next to a
370+
subsystem that does not know it exists. It is ROADMAP §12.3. Two gaps that were
371+
here are closed, and are recorded below so the shape of the fix is not lost.
372+
373+
1. *Closed — the loop has a surface.* `grapharc plan <goal>` drives propose →
374+
admit → materialise → execute → replan and prints every round with its
375+
admission status and rejection codes. ①→③ is a path, not a diagram.
376+
`grapharc/examples/plan_incident.py` supplies the kinds; `--registry
377+
module:attr` replaces them, along with the `STATE_SCHEMA` and `WRITES` a
378+
registry needs to be usable.
379+
2. *Closed — policy reaches admission.* `PolicyEngine.edge_policy(tenant=…)`
380+
compiles the document's `edge` rules into the `EdgePolicy` the checker
381+
consults, mirroring `permission_policy()` on the tool side, and `grapharc
382+
plan --policy` calls it. Box ④ can now be enforced by the declarative
383+
document. What the compiled object still cannot carry is the approver role
384+
and the audit record, because `EdgePolicy.decide` returns a bare `Decision`;
385+
and nothing yet routes the *tool* plane through the document, so `grapharc
386+
agent` is still governed by Python objects.
383387
3. **The HTTP API and the session runtime are two different things.**
384388
`grapharc/session` gives durable, cross-process sessions: verified by running
385389
one interpreter to an approval hold and resuming it by id in a second, with
386390
each node appearing exactly once in the append-only log. `grapharc/server`
387391
does not use it — it has its own `InProcessRuntime` whose sessions die with
388392
the process, never evict, and record `message`/`approval` events without
389393
delivering them into a running graph. Stage ② holds; stage ① does not reach it.
390-
4. **The shipped graphs do not use durable memory.** `SQLiteMemoryStore` is
391-
verified durable across separate processes, and every `grapharc run` command
392-
still constructs the in-process `MemoryStore()`. Box ⑥'s "durable memory with
393-
provenance" is true of the library and not of anything you can run.
394+
4. *Closed — the shipped graphs can use durable memory.* `grapharc run
395+
--memory PATH` hands `stage6` and `capstone` a `SQLiteMemoryStore`, verified
396+
across a real process boundary; in-process remains the default so a plain run
397+
writes nothing nobody asked for. Box ⑥'s "durable memory with provenance" is
398+
now true of something you can run.
394399

395400
The fifth is not a seam but a boundary, and it is the one most likely to be
396401
over-read:

HANDOFF.md

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ is why the Claude-CLI gateway backend exists. Run tests with
6666
`.venv/bin/python -m pytest`. **Never run `pytest -m live` casually — those call
6767
paid APIs and cost real money.**
6868

69+
> **The subscription buys inference, not agency.** `ClaudeCodeCLIChatModel`
70+
> implements no `bind_tools`, so it **cannot drive `grapharc agent`** — the very
71+
> milestone this handoff leads with. That is deliberate, not unfinished: `claude
72+
> -p` is itself a full agent with its own tools and its own loop, and letting it
73+
> call tools would put the control plane inside a subprocess GraphARC cannot see
74+
> or veto, which is the architecture this project exists to replace. So the
75+
> adapter strips it to pure inference (`--disallowedTools`, `--setting-sources
76+
> ""`, empty cwd, no session). Consequence to plan around: any node that needs a
77+
> model to *think* runs free on `claude-cli`; any node that needs a model to
78+
> *drive tools* needs OpenRouter and real money. `grapharc agent` refuses with a
79+
> clear error rather than degrading.
80+
6981
> ### The `-qq` trap — read this before you believe a test run
7082
>
7183
> `addopts` in `pyproject.toml` is `-q -m 'not live' --strict-markers
@@ -284,23 +296,41 @@ choose which one that is.
284296

285297
`ARCHITECTURE.md` §7 is the current, re-derived gap analysis — **read it rather
286298
than trusting a copy here**, since a copy is exactly what went stale last time.
287-
In one line each, the five gaps it names:
288-
289-
1. **Nothing shipped drives the loop.** `grapharc.planner` is imported by no
290-
other module: no CLI command, no example, no session graph. The cycle is a
291-
library API proven by tests, not something a reader can invoke and watch.
292-
**This is the highest-value thing left.**
293-
2. **Policy does not reach admission.** `grapharc.policy` parses a TOML document
294-
that already understands `node`/`edge`/`tool`/`spend` rules;
295-
`AdmissionChecker` takes an `EdgePolicy` assembled in Python. No bridge.
296-
3. **The HTTP API and the session runtime are two different things.**
299+
Four of the five gaps that version named are now closed. What is left:
300+
301+
1. **The HTTP API and the session runtime are two different things.**
297302
`grapharc/server` has its own `InProcessRuntime` whose sessions die with the
298-
process. The durable, cross-process `grapharc/session` is not what it uses.
299-
4. **The shipped graphs do not use durable memory.** Every `grapharc run`
300-
constructs the in-process `MemoryStore()`, though `SQLiteMemoryStore` is
301-
verified durable across processes.
302-
5. **Admission authorises a kind, not its arguments** — a boundary, not a seam,
303+
process and whose approvals are recorded without being delivered. The
304+
durable, cross-process `grapharc/session` is not what it uses. **The last
305+
seam, and the highest-value thing left** (ROADMAP §12.3).
306+
2. **Admission authorises a kind, not its arguments** — a boundary, not a seam,
303307
and the one most likely to be over-read. See *Known limits*.
308+
3. **The source is not on the public remote.** 15 commits unpushed; the
309+
documented install fails at `uv sync`. `origin/main` is an ancestor of HEAD,
310+
so a plain `git push` fast-forwards.
311+
312+
Closed since, each with a shipped caller and tests:
313+
314+
- **`grapharc plan <goal>`** drives propose → admit → materialise → execute →
315+
replan, printing every round and its rejection codes. Scripted and free by
316+
default; `--model` for a real backend, `--registry module:attr` for your own
317+
kinds. `grapharc/examples/plan_incident.py` is the demo registry, built so the
318+
default run *shows* a refusal: `deploy` is registered and every edge into it
319+
denied, so round 1 is refused on `edge_denied` and round 2 replans without it.
320+
- **`PolicyEngine.edge_policy(tenant=…)`** compiles the TOML document's `edge`
321+
rules into the `EdgePolicy` `AdmissionChecker` consults, and `grapharc plan
322+
--policy` is the caller. The test that matters: with a `*->deploy` deny rule in
323+
the file round 1 is refused; delete the rule and the same run admits it. The
324+
document constrains the run rather than answering questions about one.
325+
- **`grapharc run --memory PATH`** hands `stage6` and `capstone` the durable
326+
`SQLiteMemoryStore`. In-process stays the default. Proved across a real
327+
process boundary, not just in one interpreter.
328+
- **`cost_usd` on trace events.** Both gateways publish the provider's price
329+
through the same `llm_output` envelope; the runtime's usage callback writes it
330+
onto the node's `end` event and an agent writes the per-call figure onto each
331+
`model` event. `recorded_cost_usd` is a measurement again, and a backend that
332+
reports no price still falls back to a `RateCard` estimate without the two
333+
mixing.
304334

305335
---
306336

0 commit comments

Comments
 (0)