Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions .claude/rules/scanning.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions .serena/memories/connector-allowlist-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<cwd-slug>/mcp-logs-<Server>/<newest>.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
Expand Down
29 changes: 24 additions & 5 deletions crates/batten/tests/it/scanner_taxonomy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand Down