Everything between today and a full-fledged general-purpose agent runtime, as Status is measured, not aspirational.
Legend: [x] done · [~] partial · [ ] not started · B blocks other
work · ! known-false claim shipping today
Overall: ~70% of the list below (72 of 103 enumerated items). Re-derived on 2026-07-28 by executing every claim against the tree rather than reading the commit log — the percentage is the fraction of enumerated items verified done, section by section, which is this project's own definition of scope and not the industry's.
At that point: pytest → 1,381 passed, 10 deselected (the live ones);
ruff check . clean; the wheel builds and imports all 94 submodules in a clean
virtualenv. Treat the test count as a snapshot rather than a fact about the
project — pytest re-derives it in one command, which is the only reason it is
quoted at all.
The number used to be generous in one specific way, and mostly is not any
more. Four subsystems were built, tested and reachable by nothing: planner/
had no command driving the governed loop, policy/ had no caller and no bridge
from its TOML document to the EdgePolicy admission consults, the CLI handed
graphs the in-process memory store instead of the SQLite one, and no trace event
carried the provider's cost. All four are closed — grapharc plan,
PolicyEngine.edge_policy(), grapharc demo --memory, and cost_usd on the
end and model events. One seam is left: server/ still ships its own
in-process session runtime instead of the durable session/ one (§12.3). Code
that works and is unreachable scores as built here and is worth less than that
to a reader, which is what §12 exists to count.
In order.
- Put the HTTP API on the real session layer (§12.3) — the last seam.
session/is durable and resumes across processes;server/uses its ownInProcessRuntimethat does neither, and records approvals without delivering them. Two session layers, one seam. - Let admission constrain arguments (§5.6) — !. The gap most likely to
be over-read: a rule reaches a node's kind and never its
args, soargs={"path": "/etc/passwd"}is admitted on the strength of the kind.Materializerdrops args by default, which makes the default safe and the opt-in sharp. - Route the tool plane through the document (§7.5 remainder) — the edge
side now compiles to the admission gate, but nothing calls
permission_policy(), sographarc agentis still governed by Python objects rather than by the TOML file. Publish to PyPI(§11.1) — done,0.1.0is live. Next is0.1.1, to correct the__version__the published wheel carries. Build only from a clean tree, and let the tag-drivenrelease.ymldo it rather than a localuv build— that is exactly how the drift got in.Decide the version.Decided:0.1.0. A1.0would imply API stability that several days-old subsystems do not have.
Ship-blockers. Every item verified by running code. Section clear.
- 0.1 —
ctypesescape closed, along with three more found during the audit:sqlite3.connect(opens files in C, raising noopenevent),_posixsubprocess.fork_exec(the C entry point undersubprocess), and compiled-extension imports from outside the runtime paths. - 0.2 — Child environment scrubbed to an allowlist, so a secret nobody thought to name cannot leak just by being new.
- 0.1b — Runtime paths are read-only. Re-verified by running it: a
sandboxed tool writing
evil.pthintosite-packagesraisesSandboxViolation, no file is created, and stdlib reads still succeed so imports keep working. Reads and mutations use separate grants. - 0.3 —
max_secondsinterrupts a running node (SIGALRM on the main thread, async-exception injection elsewhere) and re-arms, so a node that swallows one interrupt does not run free. Residual limits documented. - 0.4 — Tokens charge automatically, via a usage callback that meters
every model call inside a node, deduplicated by call identity rather than
by token count.
max_tokensis enforced aton_llm_end, so overspend is bounded by the one call that crosses the line rather than discovered a node later. - 0.5 — Types validated at write time. Remaining gap stated precisely
rather than papered over: annotation-carried constraints bite, but a state
model's own
@field_validatordoes not run at write time. - 0.6 — README claims corrected, each disproof re-run against the tree.
- 0.7 — Neo4j fiction removed; a real
ClaimStoreprotocol now exists. - 0.8 —
pytestdefaults fixed.addoptscarries-m 'not live'; live tests are opt-in viapytest -m live. Verified: a plain run deselects 10. - 0.9 —
LICENSEcopyright and README clone URL corrected. The URL now resolves; what it resolves to is §11.7's problem, not this one. - 0.10 — Gateway tempfile leak fixed. The scratch directory is an
ExitStack-managedTemporaryDirectorythat outlives every retry attempt and dies with the call, withignore_cleanup_errors=Trueso an undeletable directory cannot fail a call that already cost money.
No longer amputates LangGraph. Async, Command returns and state access all
came through, which is what unblocked the HTTP API and the session layer.
- Typed state, per-node declared writes, deep-copy isolation
- Budgets (iterations/tokens/seconds/concurrency), convergence guards, DAG mode, cycle detection
- JSONL traces, checkpoint resume, fail-closed run context
- Bounded fan-out with worker isolation and dedup
- 1.1 — Async:
ainvoke/astream/astream_events. All three run,async defnodes execute, and the sync entry points now refuse a graph containing them withAsyncNodeErrorbefore anything runs — rather than the misleadingWritePermissionErrorthis line used to describe.astream_eventsoffersv1/v2;v3is refused because LangGraph returns a stream object there, a different shape than the method's contract. - 1.2 —
Commandreturns accepted. A node may returnCommand(goto=…, update=…); theupdategoes through the same write allowlist as a dict, and thegotois validated against the compiled graph at the node boundary. ACommandpassed as input toinvoke()/stream()remains unsupported, deliberately: itsupdatewould reach state unchecked. - 1.3 —
get_state/update_state/get_state_historypassthrough, plus the threea*twins.update_stateis not a bare passthrough: it rejects unknown state fields, type-checks the values, and applies the node's declared write allowlist when called withas_node=. The residual gap is stated in the docstring — withas_node=NoneLangGraph attributes the update to whichever node last ran, and GraphARC does not reproduce that inference, so such an update is type-checked but not allowlisted. - 1.7 —
TraceRecorder.thread_summaryis incremental. It folds only the bytes appended since the last call and keys its index on file length, so a long-lived thread costs O(events) over its life rather than O(events²), and lines written by another process are picked up the same way. - [~] 1.5 —
interrupt()half-works. LangGraph'sinterrupt()suspends the graph and theInterruptappears onget_state(...), so the suspend side is real. There is no supported way to resume:invoke()takes a dict, a state model orNone, and.inner.invoke(Command(resume=…))fails closed withMissingRunContextErrorby design. It also still writes a spuriousphase="error"trace line carrying theGraphInterrupt. Use the session layer's approval gate (§6.5) for human-in-the-loop today. - 1.4 — Passthrough
retry_policy,cache_policy,durability, subgraphs.add_nodetakeswritesandinput_schema;compiletakescheckpointer. Nothing else reaches LangGraph. - 1.6 — Offer a decorator form so discipline composes with LangGraph instead of replacing it.
- 1.8 — Make deep-copy opt-out-able for large states. It is currently unconditional.
- Claude Code CLI adapter (tools disabled, argv array, stdin prompt)
- Correct cache-token accounting
- 2.1 —
bind_tools— works on OpenRouter. StillNotImplementedErroron the Claude-CLI backend, which is inherent toclaude -p. - 2.2 —
with_structured_output— works on OpenRouter. - 2.3 —
_streamand_agenerate— both work on OpenRouter. - 2.5 — Retries, backoff, rate-limit handling.
RetryPolicy(3 attempts, 0.5s initial, ×2, capped at 20s, 25% shrinking jitter) with an explicit transient/deterministic split: a 429 or 5xx or connection reset is retried, a 400/401/402/403 is raised on the first attempt, and anything unrecognised is treated as deterministic.Retry-Afterraises the delay but never lowers it and is itself capped. Streaming is not retried — once a chunk reaches the caller the request cannot be re-issued. - 2.6 — Routing rules — provider
order/sort/max_price/require_parameters, plus model-levelfallback_modelschains. - 2.7 — Enforced cost ceilings.
SpendMeterrefuses before a call once the ceiling is reached (ensure_headroom) and raises after charging the call that crossed it, so overspend is bounded by one call. Where it stops: a call the provider does not price cannot be charged, and those land inunpriced_callsrather than being guessed at —unpriced_calls > 0means the ceiling saw less than the whole bill. - 2.9 — Backend registry —
claude-cli,openrouter,openai,ollama,mock; a mistyped backend is rejected rather than folded into a model name.openaiis both a backend and an OpenRouter author slug, and the backend wins. - [~] 2.4 — Provider adapters: OpenRouter, Claude CLI, the OpenAI API and a
local Ollama server, plus a
mockbackend for tests. The three OpenAI-wire backends share one base class, so tool-calling, streaming, retries and the usage envelope are identical across them. Direct Anthropic API and vLLM pending; theapiextra exists with nothing importinganthropic. What the new backends do not bring: OpenAI returns no per-call price, so a dollar ceiling there counts calls unless aprice_per_million=card is supplied, and Ollama's probe reports configuration rather than a running daemon. No model count is quoted here any more. This line used to say "~340 models" while the CLI help said "~400" — nobody re-checks a number like that and it rots into a contradiction. The CLI settled on wording with no count in it and has a test enforcing that; these docs now match.grapharc models --checkreports what this machine can actually reach. - 2.8 — Prompt caching support and per-run model pinning.
- Registry, deny→ask→allow permissions, hooks, approval gates
- Audit-hook executor: path confinement, network gating, spawn refusal, SIGKILL escalation
- 3.1 — Wired to an agent.
AgentNodedrives the registry, permissions and executor.grapharc/examples/agent_fixit.pyis a shipped graph that calls tools, andgrapharc agent <task>drives the core toolset. - 3.2 — Container executor.
grapharc/harness/container.pyruns each tool call in a throwaway container: one bind mount (the workspace),--network noneunless the tool declaredneeds_network, all capabilities dropped,no-new-privileges, non-root, read-only rootfs, memory and pid limits, and no host environment forwarded. Its constraints are real and enforced rather than documented away — the tool must be importable inside the image (a lambda, apartialor a bound method is refused before a container starts), and arguments and results must be JSON. Tests against a live runtime skip themselves when no runtime or image is present and never pull one; they were exercised against Docker withpython:3.12-slimduring this pass. - 3.3 — Core tools:
read_file,write_file,edit_file,list_dir,glob,grep,run_command. Every path argument is resolved and confined by the tool, independently of the executor — verified that both../../../etc/passwdand/etc/passwdraiseWorkspaceEscape.run_commandis the deliberate exception and is documented as one: it takes an argv list and never a shell string, but the child it spawns is an ordinary process with the caller's privileges and can read the whole filesystem. It also cannot run underSandboxedExecutorat all. - 3.4 — Browser tool and HTTP/network tool.
- 3.5 — MCP client — the ecosystem standard for third-party tools. The
mcpextra exists with nothing importing it. - 3.6 — Progressive disclosure / tool search for large tool sets.
ToolRegistry.visible()filters by permission, which is a different thing. - 3.7 — Idempotency keys for side-effecting tools.
- 3.8 — Large-output offloading (write to file, return a preview).
ToolLimitstruncates at 20,000 chars; nothing is offloaded.
- 4.1 —
AgentNodebuilt: observe → model → tool request → permission check → sandboxed execute → repeat, budgeted and traced. A denied tool is fed back to the model rather than killing the run; malformed tool JSON is reported back instead of silently reading as success; stall detection keys on the tool result, so re-running a test suite is not mistaken for a loop. Onlytarget_metfills the answer field — a run that stopped for any other reason keeps its last utterance inpartial_outputwhere nothing can mistake it for an answer. - 4.2 — Context management (compaction, just-in-time retrieval).
- 4.3 — Subagent spawning with context isolation and summary-only return.
- 4.4 — Skills / instruction packs loaded on demand.
- 4.5 — Per-node model and effort tiering.
The component with no prior art to copy. It exists, and the cycle runs.
- 5.1 — Planner node that proposes rather than acts.
PlannerNode.propose()returns a typedSubgraphand executes nothing.SubgraphandProposedNodeforbid extra fields, so a proposal has no channel for a callable at all — verified, abody=orfn=key is aValidationError. - B 5.2 — Admission checker. Five checks — registry, policy, budget,
depth, acyclicity — all of which run on every proposal so a planner gets
the complete list rather than the first complaint. Nothing executes during
a check:
NodeSpec.factoryis never called and the budget meter is read, not written. Every decision keys on the registrykind, never on the instancename, so renaming a denied kind does not launder it — verified by proposingProposedNode(name="harmless_helper", kind="deploy")against a rule denyingdeployand watching it refused. Worst-case cost is summed from the registry's own numbers, so a planner cannot buy admission by claiming to be cheap. - 5.3 — Rejections are first-class traced events. Every decision,
admitted and rejected alike, writes a
phase="admission"line carrying the status, the proposal fingerprint, the checks run and the failed codes. The phase is deliberately not"end", so admission decisions cannot inflate the node-execution countsobserve.metricsreports. - 5.4 — Replanning with loop protection.
GovernedLoop.run()carries a goal from first proposal to recorded stop.AdmissionResult.feedback()— the per-check list with codes and remedies — becomes the planner's next feedback, and the loop never trims an over-large proposal, drops a denied edge, or retries an identical one. Nine stop reasons, includingno_progress,max_rounds,admission_refusedandplanning_failed; the loop cannot fall out of the bottom. Observed end to end: a scripted planner whose first proposal named a policy-denieddeploynode → round 1 rejectedpolicy/edge_denied, round 2 admitted and executed, round 3 admitted and executed, stopgoal_met, with 3 admission + 3 round + 2 node + 1 stop events under onerun_id. - 5.4b — Materialisation binds to the authorisation.
Materializer.materialize(admitted, proposal)takes theAdmissionResultfirst and matches it to the proposal by fingerprint: a result that authorised something else raisesNotAdmitted, and so does a rejected one. There is no overload taking a bareSubgraph. The graph is built throughGraphARC.add_node/add_edge/compile, so declared writes, typed state, budgets and traces all apply unchanged, and a body returningCommand(goto=…)is confined to the admitted edge set withUnadmittedTransition. - 5.5 — Decomposition strategies (map-reduce, specialist fan-out) as reusable planner presets.
- ! 5.6 — Admission cannot constrain arguments. Stated plainly because
it is the gap most likely to be over-read: no rule reaches
ProposedNode.args, so a proposal carryingargs={"path": "/etc/passwd"}is admitted on the strength of its kind.Materializerdrops args by default;forward_args=Truehands the raw dict to a factory with nothing having checked it. Admission authorises the verb, not the object.
- 6.1 — Long-lived sessions with a status lifecycle in a
SessionStore. - 6.2 — Resume across process restart. Verified by running it: one
interpreter created a session, ran
ingestandplan, and stoppedawaiting_approvalholdingapply; a second interpreter resumed by id, saw the hold, approved it, and ranapplyandreport. The append-only log shows each node exactly once — nothing repeated, nothing skipped. The resuming process must register the graph in its ownGraphRegistry, or it getsUnknownGraphErrorrather than a guess. - 6.3 — Interrupt and steering at superstep boundaries.
- 6.4 — Event queue for multi-turn input, durable in the store.
- 6.5 — Human approval as a suspending graph node. Gated nodes are
passed to LangGraph as
interrupt_before, so the graph stops before the gated node runs. Every gated node on a superstep boundary is held separately with its own request id, and the graph does not move while any is unanswered — a signature onsend_emailis not a signature ondelete_records. - 6.6 — Concurrent sessions with isolation, each on its own thread.
- 6.7 — Async turns.
run()is synchronous and occupies its caller until the session stops. The kernel grewastreamwhile this was being written; an async turn is buildable and simply not built. - 6.8 — A real runner lease.
SessionStore.transitionstops a second runner from claiming a session, and nothing reclaims one whose runner died holding it. That is a claim, not a lease.
Everything here works and nothing calls it.
- 7.1 — Declarative policy config over nodes, edges, tools and spend.
TOML in,
PolicyEngineout; a commented example ships atgrapharc/policy/example.toml. Evaluation is tiered rather than positional — everydenybefore everyaskbefore everyallow— so a broad deny beats a narrow allow, including one scoped to a single tenant. Verified:delete_*denied for a tenant thatwrite_*is allowed for, and an undeclared tenant denied outright. - 7.2 — Approval routing.
askrules carry a requiredapprover_role;engine.approval_router(handlers, tenant=…)produces the callback aHarnessalready obeys, andengine.permission_policy(tenant=…)produces a realPermissionPolicy. - 7.3 — Policy versioning and decision audit. Every decision lands in a JSONL record naming the resource, subject, tenant, effect, the rule id and reason that produced it, the policy version, and a digest of the document — so a decision can be tied to the exact policy text that made it.
- 7.4 — Multi-tenant scoping. A declared tenant list makes a rule scoped to an unknown tenant a load error and a request naming one a recorded denial.
- 7.5 — The document reaches the gate.
edge_policy(tenant=…)compilesedgerules into theEdgePolicyAdmissionCheckerconsults andnode_policy(tenant=…)compilesnoderules into theNodePolicybeside it — the node half reached nothing at all until issue #66, so adenyrule over a kind was text and the kind still ran — andgrapharc plan --policyis a shipped caller, so this package is no longer imported by nothing. What the compiled object still cannot carry is whatpermission_policy()cannot either: the approver role and the audit record, becauseEdgePolicy.decidereturns a bareDecision. Admission treatsaskas not-yet-permitted. Still open: no call fromAgentNodeorgrapharc agenttopermission_policy(), so the tool plane is still governed by Python objects rather than by the document.
- Claims with provenance; supersession instead of overwrite
- Unicode-safe entity normalization
- 8.1 —
SQLiteMemoryStore, sameClaimStoreprotocol, verified durable across genuinely separate processes. - 8.2 — Artifact storage.
SQLiteArtifactStorepoints at the same file as the claim store. Append-only with versions rather than overwrites, mandatory provenance, and content-addressed blobs written before the row that references them — so a crash leaves an unreferenced blob (garbage) and never a row pointing at content that does not exist (a lie).nameis metadata and never used to build a path. - 8.3 — Real retrieval. Okapi BM25F over subject+predicate+object with the subject weighted highest, an optional injected vector channel that stays silent below a similarity floor, and graph traversal that reads a claim's object as an entity so a question about A reaches facts about B, each hop decaying the inherited score. Every result list is sorted by a total order so both backends rank identically. Nothing here is sublinear and nothing here pretends to be.
- 8.4 — Automatic contradiction detection.
detect_contradictionsandadd_and_detectflag a new claim that shares a normalized (subject, predicate) with a stored one and differs in object. Structural, not semantic: it will not relate "is fast" to "is slow", will not match a rephrased object, and will flag a legitimately multi-valued predicate. It therefore reports and never resolves — auto-superseding would delete half a multi-valued fact inside the one subsystem whose promise is that facts are never destroyed. - 8.5 — Token-budgeted context rendering.
render_contexttakesmax_tokens,max_dead_endsand acount_tokenscallable; the dead-end section is no longer uncapped. - 8.6 — Per-tenant/user memory scoping.
- 8.7 — Hand a durable store to the shipped graphs.
grapharc run --memory PATHgivesstage6andcapstonetheSQLiteMemoryStore; the in-process one remains the default so a plain run stays hermetic. Thememoryextra that named Neo4j with nothing importing it was removed; a Neo4j-backed store would bring its own extra with it.
- CLI (
run/trace/metrics/viz) - 9.1 — HTTP API. Seven routes on FastAPI: create, list, get, post an
event, SSE stream, NDJSON trace, healthz. Verified end to end — create →
poll →
succeeded,text/event-streamframes intrace/status/doneorder, the NDJSON trace being the same record the stream carried, 404 on an unknown session or graph and 422 on input that fails the graph's state schema. Behind theserverextra; importing the rest of GraphARC does not import FastAPI. - 9.2 — Real CLI: ten commands,
run/plan/agent/serve/models/replay/diff/trace/metrics/viz, every one of them with--json. In JSON mode the failure is the document rather than a line on stderr, and exit codes are part of the interface:0did the job,1ran and the answer was negative,2could not run at all. - 9.6 — Streaming output to clients via SSE, with a
last-event-idcursor so a reconnect skips what it already saw. - 9.3 — Cron schedules and 9.4 — webhook triggers.
- 9.5 — Chat channels (Slack / Discord).
- JSONL traces, metrics summaries, Mermaid path rendering
- 10.1 — Replay a run from its trace. A reconstruction, not a
re-execution:
replay()rebuilds the node sequence, the folded state, the timing and the failures off the JSONL, and calls no model, tool or node. Two limits inherited from the recording side and stated in the signature — strings past 2,000 chars were truncated at write time, and the trace does not record which fields have reducers, so a reduced field replays last-write-wins unless the caller supplies the reducer.diff_runs/diff_tracealign two runs and report where they diverged. - 10.3 — OpenTelemetry export. One root span per run, one child per node
execution,
AgentNodesub-steps parented by inference (and to the run span rather than to a guess when the parent cannot be identified). The dependency is confined toOTelSpanExporterbehind a Protocol, so importing the module needs no OTel. Previously documented as unverified against the real SDK — verified during this pass againstopentelemetry-sdk1.44.0, with spans reaching anInMemorySpanExporter. - [~] 10.4 — Cost attribution per run, thread (session) and node. The
price is now recorded, not guessed: every gateway publishes a
cost_usdthrough the samellm_outputenvelope, the runtime's usage callback accumulates it per node and writes it onto theendevent, and anAgentNodewrites the per-call figure onto eachmodelevent. A backend that reports no price still falls back to aRateCardestimate, andrecorded_cost_usdandestimated_cost_usdnever mix — a recorded figure wins outright rather than being averaged with a guess. Tokens are counted from the same eventsmetrics.summarizeuses — nodeendevents plus work outside any node span, which is what agrapharc agentrun consists of entirely — and the suite asserts the two agree. One gap left: there is no tenant on a trace event, so tenant attribution is not offered rather than being approximated. - 10.2 — Rollback and versioned graph/prompt configs.
- 10.5 — Alerting on budget, failure, and verifier-drift.
-
Builds a clean wheel; 1,534 tests; CI; ruff clean. Verified in a fresh virtualenv: a bare wheel install imports most of the 103 submodules and runs
grapharc demo stage0—gateway.openrouterand the wholeserverpackage need their extras — and installing[all]imports all 93. -
11.6 — Classifiers,
[project.urls], contribution guide. Every URL names a file that exists, and theallextra is self-referential so it cannot drift out of sync with the others. -
[~] 11.3 — Live-model examples behind the
livemarker: 10 tests, deselected by default. CI wiring still pending (it needs a key in secrets). -
[~] 11.2 — Docs site. A cookbook is landing under
docs/cookbook/; no published site. -
11.7 — The source is on the public remote.
git clone https://github.com/CodeGraphContext/GraphARC && uv sync --group devworks; verified by cloning into a scratch directory and findingpyproject.tomland thegrapharc/package. This was the single most consequential false claim in the tree for most of the project's life, because it was the first one a reader hit. -
11.1 — Publish to PyPI.
0.1.0is live;pip install grapharcverified in a clean virtualenv through tographarc demo stage0. One defect shipped with it: the wheel's module carries__version__ = "0.1.0a0"while its metadata says0.1.0. PyPI is immutable, so this is corrected by0.1.1, not by a re-upload. -
11.2 — Ship
0.1.1to correct the__version__above. Build only from a clean tree:git statusempty, thenuv build. -
11.4 — Benchmarks, including published losses.
-
11.5 — External security review (the audit-hook sandbox is defense in depth; §3.2's container executor is the boundary to review).
Each item is two working subsystems that do not know about each other; none of them is research, and all of them are worth more than another feature. Four of the five are closed.
- 12.1 — A surface for the governed loop.
grapharc plan <goal>drives propose → admit → materialise → execute → replan and prints every round, its admission status and its rejection codes. Scripted by default, so it costs nothing and is deterministic;--model SPECswaps in a real backend,--registry module:attrswaps in your own kinds (and, viaSTATE_SCHEMAandWRITESon the same module, the schema they write to). The shipped demo registersdeployand denies every edge into it, so the default run shows round 1 refused onedge_deniedand round 2 replanning without it.grapharc/examples/plan_incident.pyis the registry;tests/test_cli.pypins the rounds, the stop reason and the exit codes. - 12.2 —
PolicyDocument→EdgePolicy.PolicyEngine.edge_policy( tenant=…)compiles the document'sedgerules into the objectAdmissionCheckerconsults, mirroringpermission_policy()for the planner side; a test pins it tocheck_edgeacross an edge × tenant matrix, and an undeclared tenant compiles to a policy that permits nothing.grapharc plan --policy PATH --tenant NAMEis the shipped caller, and the end-to-end test is the one that matters: with a*->deploydeny rule in the file round 1 is refused, and with the rule removed the same run admits it. The document constrains the run. - 12.3 — The HTTP API on the real session layer.
create_app(runtime=…)already takes anySessionRuntime; what is missing is the implementation backed bygrapharc.session, which would give the API durable sessions, cross-process resume, and approval events that are delivered rather than merely recorded. - 12.4 — A durable store for the shipped graphs.
grapharc run --memory PATHhandsstage6andcapstoneaSQLiteMemoryStore. In-process stays the default, so a run still writes nothing nobody asked for. Proved across a real process boundary: two interpreters, one file, and the second run recalls what the first persisted. - 12.5 —
cost_usdonto trace events. See §10.4.
| Scope | Gate: a real task against a real model | Status | |
|---|---|---|---|
| V0 | §0 + §2.1 + §4.1 | An agent edits a file and runs tests, permission-gated and budgeted | passed |
| V1 | §5 + §1.1–1.2 | "Refactor this repo and run tests" plans its own fan-out; an over-budget plan is rejected with a recorded reason | mechanism done, gate not run — the loop, the rejection and the recorded reason all work on scripted planners; no live-model run of the real task has been recorded |
| V2 | §6 + §8.1 | A session survives restart; a human approves a destructive action mid-run | mechanism done, gate not run — verified across two processes with a scripted model |
| V3 | §7 + §9 | Incident response runs from a webhook, remediation gated on approval | blocked on §12.2 and §9.4 — policy and the API exist, the webhook and the wiring do not |
| V4 | §10 + §11 | Replay any production run; a stranger pip installs it |
replay works; nobody can install it — see §11.7 |
Read the V1 and V2 rows carefully. Both say mechanism done, gate not run, and that distinction is the entire point of defining gates this way. The code does the thing; nobody has yet pointed a real model at the real task and recorded the result. Marking them passed on the strength of a green suite is precisely the failure mode below.
The failure mode to avoid — this repo already hit it once — is writing the essay before the code and marking milestones done because tests pass rather than because a real task ran. It has a second form, which this page is now guarding against: marking a subsystem done because it is built, when nothing calls it. §12 exists so that gap has somewhere to be counted.