Skip to content

The agent proposes, the human decides: grapharc mcp and the Claude Code skill - #99

Open
Shashankss1205 wants to merge 5 commits into
mainfrom
supervision-mcp
Open

The agent proposes, the human decides: grapharc mcp and the Claude Code skill#99
Shashankss1205 wants to merge 5 commits into
mainfrom
supervision-mcp

Conversation

@Shashankss1205

Copy link
Copy Markdown
Collaborator

Phase 2 of the supervision plan, scoped to the MCP server and the Claude Code skill. Three commits, each with failure-mode gate tests; the full suite is green after each (2,009 passed at HEAD). Builds on main independently of #98.

What lands

1. The approval gate becomes real where it was promised (9d115b4)

  • GovernedLoop used to return PLANNED before the approval block could fire, so plan --approve — the form the Slack gate injects — was inert by ordering. The approval now precedes the plan-only return: an approved plan is still PLANNED, and a denial saves no plan.json for a later go to pick up.
  • go <dir> --approve was accepted and silently dropped (_cmd_go passed neither flag on; execute_plan had no parameter for them). Both commands now share one gate builder, so they cannot drift on how a parked run asks its question.
  • plan --json carries the admitted shape as data: run_dir, fingerprint, the proposal's nodes/edges/rationale, and a mutating verdict computed against the registry module's own MUTATING_KINDS — with a module that declared nothing read as "assume mutating", never as "declared safe".

2. grapharc mcp (164d14f) — the supervision surface, behind the mcp extra that had waited unimported since it was declared. Exactly three stdio tools:

  • plan(goal, scripted?, max_rounds?) — no registry, policy or model parameters: the operator's grapharc.toml in the server's root decides those, never the requester's call.
  • show_graph(run_dir) — read-only; rendered summaries, never raw state_delta; run directories confined to the server's root.
  • execute(run_dir, approval_timeout?) — re-admits the saved plan; parks on the file handshake exactly when the plan's own record says it can change files, so a read-only plan runs on the host's prompt while a mutating one waits for grapharc approve <run_dir> from a human, out of band.
  • There is no approve, deny, or decide tool, and a gate test holds the tool list to that sentence. The server drives the CLI as subprocesses (stdout belongs to the protocol; the --json contract is the tested one), and plan.json without a mutating field reads as mutating — an old plan is never assumed safe.

3. grapharc init --claude-code (8df4a4f) — writes .mcp.json and .claude/skills/grapharc/SKILL.md: the contract that routes multi-step work through plan → show the user the graph → execute, quotes the approve command when parked, and carries the never-clauses (never run grapharc approve, never touch the handshake files, a timeout means ask). Templates are string constants, byte-asserted by tests, never overwritten. Cookbook 09 walks the adoption; the deep dive and README state the trust boundary: the MCP gate binds the MCP surface, not a co-located agent's own file tools.

Verified

  • 18 new gate tests across test_approval.py, test_mcp_gate.py, test_adopt.py — including: a denied plan --approve leaves no plan file; an unanswered mutating execute leaves the plan unexecuted and reissuable; the tool list contains no approval verb; a run_dir outside the root is refused; the server writes nothing to stdout; the skill's never-self-approve clause is present in the exact bytes shipped.
  • End-to-end scripted through the real machinery: plan → show_graph → out-of-band grapharc approve from a thread → execute, plus the read-only path executing without a park.
  • init --claude-code smoke-tested through the real CLI, including the exit-2 refusal on rerun.

🤖 Generated with Claude Code

Shashankss1205 and others added 5 commits August 7, 2026 02:27
…are real now

Two flags that promised a gate and delivered none. GovernedLoop returned
PLANNED before the approval block could fire, so `plan --approve` — the
form the Slack gate injects — was inert by ordering; and `_cmd_go` accepted
--approve/--approval-timeout for the saved-plan path and passed neither on,
so `go <dir> --approve` was accepted and silently ignored. The approval
block now precedes the plan_only return (an approved plan is still PLANNED,
a denial saves nothing for a later go to pick up), execute_plan wires the
same file handshake `plan` uses — one shared gate builder, so the two
commands cannot drift on how a parked run asks — and a denied or timed-out
go leaves plan.json without an executed_run_id.

For the external drivers this exists to serve, `plan --json` now carries
the admitted shape as data: run_dir, fingerprint, the proposal's nodes,
edges and rationale, and a `mutating` verdict computed against the registry
module's own MUTATING_KINDS — with a module that declared nothing read as
"assume mutating", never as "declared safe" (RegistryBundle.mutating now
distinguishes silence from an empty declaration).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The supervision surface external agents plug into, behind the mcp extra
that has waited in pyproject since it was declared. Three stdio tools and
deliberately not a fourth: plan proposes through the governed loop and
returns the admitted shape as data; show_graph reads a run directory back
(rendered summaries only, never raw state); execute re-admits and runs the
saved plan — parking on the file handshake exactly when the plan's own
record says it can change files, so an all-read-only plan runs on the
host's prompt while anything mutating waits for a human at a terminal.
There is no approve, deny, or decide tool, and a gate test holds the tool
list to that sentence: a client that could call approve() would be
approving its own proposal, which is not approval.

The server drives the CLI as subprocesses — stdout belongs to the
protocol, the --json payloads and exit codes are the tested interface, and
a parked execute cannot wedge show_graph. Argv is built, never parsed: no
tool accepts a registry, policy or model, because the operator's
grapharc.toml in the server's root decides those, and a run_dir a client
names is confined to that root. plan.json now records the mutating verdict
beside the fingerprint, and a record without the field reads as mutating —
an old plan is never assumed safe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…boundary

grapharc init --claude-code writes the two files that adopt the MCP
supervision surface: .mcp.json registering `grapharc mcp` as a project
server, and .claude/skills/grapharc/SKILL.md — the contract that routes
multi-step and state-changing work through plan -> show the user the graph
-> execute, quotes the approve command when a mutating plan parks, and
states the never-clauses for the hands the server cannot see: never run
grapharc approve, never touch the request or decision files, a timeout
means ask rather than retry. The templates are string constants in init's
convention, asserted byte-for-byte by the gate tests, and neither file is
ever overwritten — a refusal names what exists and half-writes nothing.

Cookbook 09 walks the adoption; the deep dive and README state the trust
boundary out loud rather than implying it: the MCP gate binds the MCP
surface, not a co-located agent's own file tools — the boundary is the
working directory, as it is for the Slack workspace — the host's permission
prompt is UX in front of the gate, not the gate, and a parked execute lives
inside one tool call. ROADMAP gains 9.7, distinct from the still-open MCP
*client* in 3.5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…st count it

Three defects that all followed from the same stale assumption — that a
failed node leaves no terminal event worth reading.

`observe.cost` skipped every failed execution, so a run stopped for
overspending reported a cost report that disagreed with its own audit trail
by precisely the spend the stop was about. The kernel stamps `error`
terminals with the node's tokens exactly as it stamps `end`, and
`metrics.summarize` already counted them; cost now does too, prices them,
and keeps `tokens_before_error` for the narrower case it was really about —
a trace whose `error` event carries no count of its own, where the
AgentNode sub-steps are the only record.

The live view double-counted: sub-step tokens accumulated into `live_tokens`
were never cleared at a terminal, so a node that ran, finished and ran again
showed its closed execution's spend twice.

And `go` never resolved `policy` from `grapharc.toml` the way `plan` does,
so a saved plan re-admitted through the gate was judged by the built-in
default rather than the operator's document.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…put is ignored

uv.lock records the `mcp` dev dependency added with `grapharc mcp`, so its
gate tests run in CI rather than silently skipping. The `.mcp.json` and
skill that `grapharc init --claude-code` writes when this repo dogfoods its
own supervision join the generated-not-authored list, alongside the runtime
residue ignored above.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant