From a003aa3e88000073ad0bbfabea628e496afa232e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 04:34:42 +0000 Subject: [PATCH 1/2] docs(rules): a fifth scanning row for the live-behaviour question, and row four's write direction `.claude/rules/scanning.md` routed four question classes to four instruments and left two failures unnamed, both measured on one session (CLOUD-1533). Row five: a pointer confirmed in code is not a behaviour confirmed in the store. A row was filed on a correctly cited mechanism and stated the store behaviour it implied; a census over the store found zero entries of that origin in 9,674. The instrument is a count over the live instance, taken before the sentence is written, and the row names a capability rather than a product for row one's reason. Row four's write direction: a finding leaves the session in the turn it is found, filed or fixed, before it reaches a sentence to a human. Six defects and a root-cause analysis were carried in chat until a human asked what would survive the archive. `scanner_taxonomy.rs` pins the new row and iterates it as a capability row; the declared mutation rewrites the question so the prose assertion goes red. Refs: CLOUD-1533 --- .claude/rules/scanning.md | 44 +++++++++++++++++++--- crates/batten/tests/it/scanner_taxonomy.rs | 29 +++++++++++--- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/.claude/rules/scanning.md b/.claude/rules/scanning.md index 74b92c5b7..cd09d8a42 100644 --- a/.claude/rules/scanning.md +++ b/.claude/rules/scanning.md @@ -13,12 +13,13 @@ These load when you are about to ask something about the whole tree rather than about the file in front of you. The question decides the tool, and the three questions are not interchangeable. -| the question | instrument | -| ----------------------------------------------------------------------- | ----------------------------- | -| does this file contain this literal string | a structured text search | -| is this token in command position, inside a comment, or inside a string | a tree-sitter matcher | -| which type does this name resolve to | clippy, rust-analyzer, Serena | -| has this already been filed, decided, or measured | the board, before the tree | +| the question | instrument | +| ----------------------------------------------------------------------- | ------------------------------------------------------- | +| does this file contain this literal string | a structured text search | +| is this token in command position, inside a comment, or inside a string | a tree-sitter matcher | +| which type does this name resolve to | clippy, rust-analyzer, Serena | +| has this already been filed, decided, or measured | the board, before the tree | +| is this pointer's behaviour live, or only present in the code | a census over the store or session, before the sentence | Row two is the one the tree kept reaching past. Rows one and three both have a habit behind them — `grep` is in every hand, and `.claude/rules/rust.md` already @@ -79,6 +80,37 @@ every time; it cannot reach a claim that something is _unfiled_, because no tool call is being made. That asymmetry is the reason this row is prose: the write has an object and an exit code, and the assertion has neither. +## Row four has a write direction, and row five is what a pointer cannot answer + +Two failures from one session (2026-09-06), on rows filed the same hour, and +each is the row above read in the direction its sentence did not point. + +**The write direction.** Row four says: before a claim about what is filed +reaches a durable artifact, search. Its converse is the failure measured here: a +root-cause analysis and six defects found mid-task were carried in chat — in a +closing paragraph headed "left for you" — until a human asked what would survive +the session's archive. AGENTS.md's output-posture paragraph already says a +finding's home is an issue or a memory; what it does not say is WHEN. **A finding +leaves the session in the turn it is found: filed, or fixed, before it reaches a +sentence to a human.** The tell is a summary paragraph carrying findings no row +owns. `filed-here` prices filing over fixing, `stop-posture` catches the hedge +phrasing, and nothing decides "found, not filed, summarised" — so this is prose, +for row four's own reason: no tool call is being made. CLOUD-1526 carries the +grooming-specific half. + +**Row five.** A pointer confirmed in code is not a behaviour confirmed in the +store. CLOUD-1523 was filed on `anchor()` returning `"."` and the drain keying +its shard on that constant — correct at the line cited, verified by reading — and +stated that the constant-keyed shard was the one growing. A census over the store +found zero entries of that origin in 9,674; the growth was another writer's. +CLOUD-1480 retracted six causes the same way, each naming a real symbol with a +false causal claim attached, until one control settled it. Row three answers what +a name resolves to; **row five asks whether the behaviour that code describes is +occurring, and the instrument is a count over the live instance** — a census, a +control, a `wc -l` — taken before the sentence is written. It names a capability +rather than a product for row one's reason: which store and which counting tool a +session has varies. + ## The row names a class, and CLOUD-310 names the components Row two is a tree-sitter matcher, not a product. CLOUD-310 evaluated the diff --git a/crates/batten/tests/it/scanner_taxonomy.rs b/crates/batten/tests/it/scanner_taxonomy.rs index 89b742124..bbfb7eab9 100644 --- a/crates/batten/tests/it/scanner_taxonomy.rs +++ b/crates/batten/tests/it/scanner_taxonomy.rs @@ -95,6 +95,23 @@ const INSTRUMENTS: &[(&str, &[&str])] = &[ "has this already been filed, decided, or measured", &["the board, before the tree"], ), + // ROW FIVE'S SUBJECT IS A COUNT OVER THE LIVE INSTANCE (CLOUD-1533). Row + // three resolves a name; nothing asked whether the behaviour that code + // describes is occurring. Measured: a row filed on a pointer confirmed in + // code stated the store behaviour that pointer implied, and a census over + // the store found zero entries of that origin. A capability row, for row + // one's reason — which store and which counting tool a session has varies — + // so it is the THIRD row `a_capability_row_names_no_bare_product` iterates. + // + // The mutation rewrites this question to one the file does not contain, so + // `the_rules_name_an_instrument_for_each_question_class` goes red: that is + // what proves the case reads the prose rather than its own table. + //MUTANT-SUITE crates/batten/tests/it/scanner_taxonomy.rs + //MUTANT row-five-question-dropped|s@"is this pointer's behaviour live, or only present in the code",@"is this pointer absent from the code",@|the_rules_name_an_instrument_for_each_question_class + ( + "is this pointer's behaviour live, or only present in the code", + &["a census over the store or session"], + ), ]; /// The issue that owns the per-component disposition this file must point at @@ -118,11 +135,13 @@ const BARE_PRODUCTS: &[&str] = &["`grep`", "`rg`", "`Grep`", "`Read`", "`Glob`"] /// The [`INSTRUMENTS`] rows whose instrument cell answers with a CAPABILITY. /// -/// Row one, because which search surface a session carries varies; and row four, -/// because which connector answers the board does. Rows two and three are -/// deliberately absent: row two names a class whose winner CLOUD-310 owns, and -/// row three names the three tools that do name resolution here. -const CAPABILITY_ROWS: &[usize] = &[0, 3]; +/// Row one, because which search surface a session carries varies; row four, +/// because which connector answers the board does; and row five, because which +/// store and which counting tool a session has does too (CLOUD-1533). Rows two +/// and three are deliberately absent: row two names a class whose winner +/// CLOUD-310 owns, and row three names the three tools that do name resolution +/// here. +const CAPABILITY_ROWS: &[usize] = &[0, 3, 4]; fn rules_text() -> String { fs::read_to_string(at_root(RULES)).expect("`.claude/rules/scanning.md` is committed") From 2d9b014ffddde1fadbdcb303f3e3e8f600218e27 Mon Sep 17 00:00:00 2001 From: Alec Wenzowski Date: Wed, 9 Sep 2026 00:31:08 +0000 Subject: [PATCH 2/2] =?UTF-8?q?docs(memory):=20the=20fifth=20connector=20s?= =?UTF-8?q?tate=20=E2=80=94=20a=20proxy=20approval=20the=20harness=20self-?= =?UTF-8?q?heals=20and=20an=20out-of-band=20client=20cannot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mem:connector-allowlist-recovery` carried four states and a recovery whose step 2 (a user-level allow entry) is the wrong layer for the one measured 2026-09-08: the CCR proxy answering `-32003 needs_approval` on the harness's own calls, healed by a retroactive approval card keyed on the `_meta["claudecode/toolUseId"]` the harness sends, while `batten mcp call` sends no id and is refused on every call. The discriminating read is the CLI's own MCP log, which names the state in its own words; the memory now routes a reader there before the settings surface. Series on CLOUD-178. Refs: CLOUD-178 Admits: 3a8de44aa692e537bafbe37ff585bb424c352a0999c7a7b0c0c19cf0f77f8590 Admits-rule: protected-mutation Admits-verdict: path write refused Admits-subject: .serena/memories/connector-allowlist-recovery.md Admits-anchor: call:a003aa3e88000073ad0bbfabea628e496afa232e Admits-epoch: 718625aad61944a4dac21416f2cdc7fae14da565cbe39a4f9855f59e7ac0df63 Admits-author: alec@wenzowski.com Admits-prev: - Admits-answer-lost: Without the edit the memory sends the next session to its step 2 (a user-level permissions.allow entry), which this session measured as the wrong layer for this state, and to arguing the connector is broken when the harness self-heals it and the user sees nothing. That cost ~5 hours and ~30 refused calls on 2026-09-08 and is recorded on CLOUD-178; the memory is the only surface a fresh container reads before repeating it. Admits-answer-precondition: protected-mutation's override route declares that the surface this class names cannot express the change and the write is one a reviewer sees in the diff it lands in. The change is a new section in mem:connector-allowlist-recovery recording the fifth connector state (the CCR proxy's per-call approval, keyed on the harness's _meta claudecode/toolUseId, which an out-of-band client cannot satisfy). It was written through Serena's edit_memory, the exact route the .serena/memories/** redirect names, and it lands as a reviewed diff in a draft PR; the commit gate refuses the commit until this block travels with it. Admits-answer-rejected-route: config read first (document, batten.toml) is a read of the authority and the work is a write to a memory it protects; reading it again tells me the path is protected, which is already known. patch run first (command, git restore) undoes the edit rather than landing it, and was already run once this session by mistake at the cost of redoing the work. --- .../memories/connector-allowlist-recovery.md | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.serena/memories/connector-allowlist-recovery.md b/.serena/memories/connector-allowlist-recovery.md index bacbd850c..632794090 100644 --- a/.serena/memories/connector-allowlist-recovery.md +++ b/.serena/memories/connector-allowlist-recovery.md @@ -148,6 +148,46 @@ connector shows **all 20 `always_ask`, including read-only `get_session` and is `always_ask` is a mandatory-approval connector, not an ungranted one, and no local change will move it. +## The fifth state: the proxy demands a per-call approval the harness self-heals and an out-of-band client cannot (2026-09-08) + +**The tell is the CLI's own MCP log, and nothing else discriminates it.** Read +`~/.cache/claude-cli-nodejs//mcp-logs-/.jsonl`. In +this state the harness's OWN calls are answered +`-32003 needs_approval` and the log says so in as many words — +`Tool 'save_comment' returned -32003 needs_approval (tool_name=…) — surfacing +retroactive approval card` — followed by a retry that succeeds. Measured in one +session: 22 harness Linear calls 20:32Z→21:37Z, **11 refused and retried**, 0 of +200+ in the same session's earlier logs. In auto mode the card is auto-approved, +so **the user sees nothing and every interactive call works.** "Nothing is broken +in any other thread" is therefore TRUE at the same time as "the reduced verb is +refused on every call", and arguing either point against the other is wasted. + +**Why `batten mcp call` fails 100% while the harness fails ~50% and heals.** The +CLI attaches `_meta["claudecode/toolUseId"]` to every `tools/call`; the proxy's +`-32003` carries `{tool_name, args_sha256, tool_use_id}`, the harness's retry +path (`ccrNeedsApprovalRetry`, wire code `no_approval`) approves THAT id and +re-sends the identical call, which then passes. An out-of-band client sends no +id and has no approval surface, so nothing it can send is ever approved. The +`permission_policy` in the injected config read `always_allow` for every refused +tool throughout — step 1 below does not discriminate this state either, and +step 2 (a user-level allow entry) is the wrong layer and changed nothing. + +**What does and does not end it.** Not a mode change (auto mode spanned both +success windows), not the binary, not the source row, not the token (rotated +mid-window, still refused). It ended when the session re-provisioned — new +injected config, new ingress token, new harness process — and the reduced verb +answered on the next call. A backgrounded retry loop is refused by +`run-shape-guard`; the retry is one call per turn, so **do the work that needs +no receipt (comments still land; `list_issues` still reads) and re-try the +reduced verb once per turn** rather than sitting on it. + +**Consequence for the board**: `an-update-owes-a-recent-read` accepts only the +receipt the reduced verb mints and `no-raw-issue-read` refuses the interactive +`get_issue`, so every `save_issue` is blocked for exactly as long as the proxy +holds this posture. That is the gate refusing correctly over a missing input, +the same shape as the claim receipt below. Series: CLOUD-178, comment of +2026-09-08. + ## Recovering a DATA connector (Linear/Gmail/Xero), in order 1. **Find the live names.** The host writes its injected MCP config to