C9 — Plan-Mode hook, one graph default kind, a write-free savings panel (PLN-2027 PR 8, FR5) - #24
Merged
Merged
Conversation
…d graph has one default kind (PLN-2027 PR 8) PR 8 items 1 and 2. The Plan-Mode hook no longer auto-allows `search` or `context`, which the broker classifies as writers, and auto-allows `graph` only for the kinds the broker runs. The hook stays standard-library-only, so a repo-side drift test pins the direction (hook-allowed stays inside broker-read-only, web_fetch aside), not equality. graph's default kind was declared in broker_refusal, _op_graph and tool_graph. All three now read broker_policy.GRAPH_DEFAULT_KIND. Co-Authored-By: Claude <noreply@anthropic.com>
…ion_source names the rules that fired Operator-directed scope from the PRD-739 GA re-run, not a PLN-2027 PR 8 item. - FREE_TIER_MAX_FILES moves from engine._FREE_TIER_MAX_FILES to infra/code_intel/inclusion.py. The engine and coverage both read it there at call time, so infra no longer imports a private pro symbol and one monkeypatch still caps both. coverage's call-time iter_source_files reach stays. - code_coverage_check's exclusion_source was a fixed string that named git-ignore when no path used it and left out skipped-directory when a path did. It now joins the rules behind this report's excluded verdicts, in EXCLUSION_RULES order, and is empty when nothing was excluded. Co-Authored-By: Claude <noreply@anthropic.com>
…ker without a write (PLN-2027 PR 8) PR 8 item 3, the write-free path (the plan's preferred option). statusline_segment gains read_only. With it, segment returns the sidecar as it stands, and markdown/json build the report with fold=False: the savings aggregate is read as it stands, never folded, persisted or refreshed in the background, and the window cache is not written. fold threads through build_savings_report, compute_usage_meter (the second write the canary found), aggregate_window_savings and _read_historical_savings[_many]. The broker allow-lists statusline_segment and refuses any call without read_only=true. The four SKILL.md copies (source edited, hosts regenerated by scripts/sync_agent_context.py) name the broker route. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
C9 — Post-review fixes: Plan-Mode hook, graph default kind, savings panel, plus the GA re-run residue
Last chunk of the PRD-739 campaign. Lands PLN-2027 · PR 8 in full, plus the two operator-added findings from the GA re-run. No later chunk depends on this one.
What it lands
PLN-2027 · PR 8, item 1 — Plan Mode auto-allows nothing the broker calls a writer.
searchandcontextare out of the hook's_READ_ONLY_TOOLS(both write). The hook also stopped auto-allowing everygraphcall: it now readstool_inputand allowsgraphonly for the kinds the broker actually runs, and never withenable.PLN-2027 · PR 8, item 2 — one declaration of
graph's default kind.The plan says the default is declared twice. It was declared three times —
broker_refusal,_op_graph,tool_graph. All three now readbroker_policy.GRAPH_DEFAULT_KIND.mcp_server.py(upstream-owned) changes by one import line and two signature defaults.PLN-2027 · PR 8, item 3 — the savings panel is reachable without a write.
statusline_segmenttakesread_only, and the broker runs it only withread_only=true. This is the plan's preferred write-free path, not the pre-approved fallback of advertising the tool under both profiles.Operator-added (GA re-run) — (a) the free-tier file cap is public in
inclusionand no longer imported as a private symbol frompro; (b)code_coverage_check'sexclusion_sourcenow names the rules that actually fired.Acceptance ledger
12 criteria · 7 met · 5 met differently · 0 not met
Met differently, each with its reason in the outcome ledger:
web_fetchis hook-allowed and broker-denied for network reach, not writes; PRD-739 Open Question 2 leaves outbound network undecided). A set-equality test would fail on a correct difference.make typecheckclean from a fresh cache →mypy --stricton the 9 changed files;engine.py's 7 errors reproduced onorigin/mainin a scratch worktree.mainbaseline → not run locally (10-minute cap; CI runs the lane sharded).exclusion_sourceno longer returns the fixed string"git-ignore + unrecognised-file-type". It now lists the rules that fired, inEXCLUSION_RULESorder joined by" + ", and is""when nothing was excluded. The field could not both agree with the per-path rule fields (the operator's instruction) and keep its old value.This invalidates PLN-1677 N0 — any capture or assertion of that literal string. PLN-1677 is outside this campaign and is not touched here.
Review
/code-review:deep --base origin/main→ APPROVED (cr-95289), scope proven at3269d8b6. 5/5 reviewers ran. 2 verified MEDIUM, 1 rejected, 0 BLOCKING, 0 HIGH, no coverage gaps.Both MEDIUMs were real, and both are fixed:
read_only=truestill wrote. On thesegmentpath with no resolvable host session, the sidecar path was resolved before the guard and fell through to the workspace store dir, creating<workspace>/.lemoncrow/and its.gitignore— contradicting the contract this chunk adds. Resolve found a second instance the review did not name: the workspace-bridge session lookup had the same side effect, so a Codex/OpenCode host would still have written after the first guard was in place. Both fixed;paths.pygained a non-creatingworkspace_store_dir()thatresolve_workspace_store_dir()now delegates its layout to, keeping the self-ignore side effect attached to every writer.kind.tool_input.get("kind", …) in frozensethashes raw model input, so{"kind": []}raisedTypeErrorout ofmain()and exited 1 — breaking the hook's documented fail-open. It now mirrors the broker's ownisinstance(kind, str)check.Re-review gate fired (
RERUN: 3 prod files changed (> 2)) →/code-review:deep --since-last-review→ APPROVED, 0 findings at every severity, scope proven atf7b87366. Note for the reviewer:routeclassified the 156-LOC incremental diff as Small and took the fast path, so the Design Critic and Impact Analyzer did not spawn. The orchestrator checked thepaths.pyblast radius mechanically instead — exactly one call site moved to the non-creating form (a pure read insidetry/except); every writer ofsession_state.jsonstill uses the creating form.1 finding declined (not pushed back): the Design Critic's pass-through-variable observation on the
fold/read_onlythread. Its verifier rejected the anchor (evidence_not_found), and resolve declined it on the merits anyway — the recommended shape would duplicate the report/meter composition, and re-plumbing the exact flag that carries the write-free contract, for no behavior change, is the churn most likely to reintroduce the write. Full reasoning verbatim inC9.resolve.json.Verification
pytest -q tests/integrations/test_mcp_read_allow_hook.py tests/gateway/test_cap_tools_list_gate.py tests/core/test_savings_aggregate.pypytest -q tests/infra/code_intel/test_inclusion_layering.py tests/infra/code_intel/test_coverage.pyruff check --no-cache src benchmarks tests scripts integrationsblack --check src testsmypy --stricton the 9 changed source filesmain)paths.py(test_paths,test_host_session_bridge,test_statusline_script,test_project_knowledge_store,test_context_dedup)mainbaselineCounterfactuals were proven per fix, not in aggregate: with both production fixes reverted, the no-session segment case, the bridge case and both hook cases fail (4 failed / 94 passed).
Independent orchestrator probes (outside the chunk's own tests): through the broker with
read_only=true, nothing is written under either the LemonCrow root or the workspace root across 15 host/format combinations — no session id, Codex bridge host, Claude with a session, and OpenCode/Codex with a live stamped bridge file, insegment,markdownandjson. The hook exits 0 with no decision for{"kind": []}and{"kind": {"a": 1}}, stays silent forpr_risk, and allowsdead_codeand the default.workspace_store_dir()creates nothing;resolve_workspace_store_dir()still writes.lemoncrow/.gitignoreand returnsmain's path shape.Notes for the reviewer
read— writes per-call accounting (live_savings_events.jsonl,tool_token_ledger.json,outcomes.json, a throttled sidecar refresh). That is pre-existing and not attributable to this tool.segmentwithread_only=truereturns""until something else has written the sidecar.markdownandjsonalways return the panel, and their totals can trail the newest unfolded ledger rows — that is the freshness this route trades away to avoid the write.relations,code_query,code_coverage_checkandcode_changes, all already advertised.statusline_segmentis not in FR5's list — the fallback branch that would have extended the advertised surface was not taken, so nothing here extends it.paths.pyentered this chunk's file set during resolve and adds one public name (workspace_store_dir) to__all__. Additive: no signature, return value or side effect of the existing function changed.make lint's baseline is 0, not the 9 PLN-2027 §0 claims (recorded by C7). Every ruff finding here would have been this chunk's own; there were none.Artifacts
APPROVEDuntil this merges.closedloop-ai/campaigns/prd-739-20260915/What is still owed after this
Nothing in this campaign — C9 is the last chunk. Operator-owned items carried forward: PRD-739 Open Question 4 still gates FR15 on a probe the operator waived; PLN-2027 §1's FR12 row and §0's 9-finding lint baseline are both stale; PLN-2027 is still
DRAFT; and PLN-1677 N0 needs theexclusion_sourceupdate above.