Skip to content

docs(memory): a permission-mode verdict is not a memory (CLOUD-178, CLOUD-1166) - #791

Draft
wenzowski wants to merge 1 commit into
mainfrom
claude/stage-2-3-grooming-uqk71k
Draft

docs(memory): a permission-mode verdict is not a memory (CLOUD-178, CLOUD-1166)#791
wenzowski wants to merge 1 commit into
mainfrom
claude/stage-2-3-grooming-uqk71k

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Refs: CLOUD-178, CLOUD-1166. Closes neither — both stay open on their own scope.

What this changes

Two memory bullets recorded a session-scoped permission reading as a durable capability boundary, and both were later quoted back to refuse work. This excises them, keeps the re-derivable half of each, and states the rule in mem:core so the class does not recur.

connector-allowlist-recovery.md

Removed: "Replaying the injected config's headers … is blocked by the auto-mode classifier, correctly — it is credential replay. Do not route around it."

CLOUD-178 is careful about exactly this claim — it labels the auto-mode classifier "a hypothesis and NOT a finding" and names the experiment nobody has run (auto vs default, same tree, same verb). The memory promoted it to an imperative, and it was then cited to refuse a design that was not credential replay.

Kept, because it is re-derivable and load-bearing: what the headers areX-MCP-Server-ID, X-MCP-Server-Origin, X-Session-UUID, no bearer token, so the endpoint authenticates the session — and the two consequences that follow regardless of any session's permission state (the material is session-bound and cannot be carried; it sits in an agent-readable file).

prior-art-and-issue-hygiene.md

Removed: "add_repo on a survey target is declined by the permission classifier."

This contradicts the standing instruction to call add_repo rather than pre-judge it — an unauthenticated probe of a private repo returns 404 whether or not the session is authorised, so a pre-check reports a false negative. Rewritten to say the tool's own answer is the authority.

workflow/agent-fanout.md — deliberately NOT changed

Its permission-mode paragraph stays. It records a divergence between what create_session's schema documents ("omit to inherit it") and what was measured, which is precisely what a memory is for. Naming the one that stays is the point: the rule below is not "delete anything mentioning permissions."

core.md — the rule, at the graph root

A memory records what is re-derivable — a path, a file format, an argv, a measured flip, an upstream issue number. If a claim would change depending on which permission mode the session is in, it is not a memory.

With the tell: an imperative with no re-derivable object behind it ("do not route around it", "is declined by"), where the honest form is the attempt and the tool's own answer.

Why the graph root

mem:core is where every other memory's trigger already lives, so a rule about what may be written to any of them belongs there rather than duplicated per file. It sits immediately under the "read on demand, never all of them" paragraph.

Verification

  • hk pre-commit gate green: rules-drift, module-map-check, suite-bench-check, license-table-check, no-docs-tree, prettier, conventional-commit, commit-attribution.
  • Written through Serena's edit_memory.serena/memories/** is protected, so a shell write is refused by the engine's protected-path gate.
  • Docs-only; no crate source, no gate, no config. Nothing to bump.

Branch note

Two earlier commits on this branch were already on main, and a third (963bff95) was a duplicate of landed work whose only remaining delta was stale countsmain carries the preset arm at policy/shell-retirement.rego:946 with its test, and newer ledger figures (725 arms / 113 engine-source / 21 whole-file) than that commit's 609 / 110 / 18. The branch was reset onto current origin/main and only the new commit replayed, so the stale counts do not travel.


Generated by Claude Code

@linear-code

linear-code Bot commented Aug 31, 2026

Copy link
Copy Markdown
CLOUD-178 claude.ai connector tools flip between readable and UUID names, silently breaking the permission allowlist

Split out of CLOUD-177, which is Done on its own scope. Evidence thread: the comments on CLOUD-177, in particular the final correction establishing verdict (a).

Ready

A claude.ai connector MCP server is exposed to a session under two different names over its lifetime, and a permission allow rule can only name one of them.

At session start the Linear connector appears as mcp__Linear__*. On a mid-session disconnect/re-register it comes back as mcp__4db58e41-cd4e-4818-8922-46cf616593f4__*, and the readable tools vanish from the listing entirely. .claude/settings.json allows "mcp__Linear", a prefix match, which matches nothing under the UUID name. Every Linear call then prompts for approval, with no signal that anything changed.

This hits the board-in-lockstep rule directly: moving a CLOUD-* issue between states is a save_issue call, so the workflow AGENTS.md mandates starts requiring a human tap per transition, mid-session, without warning.

Measured, across two containers

d38efda7 047c8272
At session start readable readable
After reconnect UUID UUID
Linear UUID 4db58e41-cd4e-4818-8922-46cf616593f4 same
Gmail UUID d648a34b-ef40-4201-b1af-9123d05d7a1e same
Xero UUID 0c388dc8-630a-4020-96c7-ff71ce1af383 same

Verdict: the UUID is stable and equal across containers. It is a durable identity for the connector; what varies is which of the two names is live at a given moment. This was initially mis-called as "unstable per boot" from a session that had not yet reconnected — absence of a reconnect is not evidence of naming stability, and any future measurement here needs to span at least one reconnect to say anything.

Confirmed load-bearing, not cosmetic: a save_issue under mcp__4db58e41-…__save_issue prompted, while a Linear write under the readable name moments earlier did not. (Both prompt-or-not observations are human-reported — an agent cannot observe its own approval prompts, and asserting otherwise is what made this take four passes to get right.)

Note this is a second, independent defect from the one that originally masked it: an org-level ask control on the Linear connector was overriding allow rules entirely, per Organization controls on connector tools. That control has been cleared and verified. Clearing it is what made this defect visible.

Ready predicate

A session survives a connector reconnect with Linear calls still auto-approved — no prompt on a save_issue issued under either the readable or the UUID name.

Done (proposed)

  • ~/.claude/settings.json allows both names:
"mcp__Linear",
"mcp__4db58e41-cd4e-4818-8922-46cf616593f4"
  • User-level, not the repo. A connector UUID is an account-specific identifier: meaningless to any other contributor or fork, and silently rotting if the connector is re-authorized. Repo rule 1 keeps those out of committed config. .claude/settings.json keeps "mcp__Linear" alone — correct and portable for anyone cloning batten — and the UUID lives in the personal scope where account facts belong.
  • Verified by observation across a reconnect, human-reported, not inferred from a session with no reconnect in it.

Open questions

  • Does the UUID survive re-authorization of the connector? Stable across two containers is not stable across an OAuth re-grant. If it rotates there, the user-level entry needs re-deriving and the failure mode returns silently. Worth knowing before treating this as closed.
  • Gmail and Xero have the same exposure and are not covered by any allow rule today. Not in scope here, but the same two-name fix applies if either is ever allowlisted.
  • No gate is possible for this one. Repo rule 2 wants a rule to ship with a runnable mechanism, and there is none available: the failure lives in a settings file outside the repo, keyed to an identifier the repo must not contain, and triggered by a remote-host event. Worth stating explicitly rather than leaving as an unmet obligation — this is a documented limitation, and the compensating control is that the failure is loud to the human (a prompt) even though it is silent to the agent.

A fourth connector, measured 2026-08-19, and it is NOT Linear

The injected config in one remote session (cse_0119SXbXYq1sNTdg4e8AjMzs) carried three mcpServers keys: github, 4db58e41-cd4e-4818-8922-46cf616593f4 (Linear, per the table above), and bf7c680d-5fdc-5ef4-b4a0-abadb619bf0a — a connector this issue has never recorded. All three proxy through api.anthropic.com, so the URL does not discriminate them.

The symptom that surfaced it: mcp__Claude_Code_Remote__get_session prompted for approval twice, human-reported, in a session where .claude/settings.json already carries "mcp__Claude_Code_Remote__get_session" in permissions.allow and there was no ~/.claude/settings.json at all. So this is the same two-name defect on a different connector: the readable alias Claude_Code_Remote is what the repo rule names, and the live key is a UUID no rule matches.

This is the first instance where the affected connector is the one the fleet procedure runs on. create_session is how a dispatch fans out (PR #454 granted it for exactly that reason), so the flip does to plan-fleet what the Linear flip does to the board-in-lockstep rule: it turns an automated step into a per-call human tap, mid-campaign, with no signal.

mcp-allow-check was green throughout, reporting "every allow rule in .claude/settings.json names a tool it can match." That is the blind spot mem:connector-allowlist-recovery already names — "A gate over settings cannot see which name is live; only the injected config can" — now confirmed on a second connector. The gate is not wrong; it answers a different question, and its green is not evidence.

Mitigation applied, user-level only (repo rule 1, and the Done section's reasoning above): ~/.claude/settings.json grants the five read-only remote-session tools under the readable name and under both live UUIDs, since which UUID is Claude_Code_Remote is not determinable from the injected config alone.

One trap worth recording, because the obvious form of this mitigation is unsafe. A wildcard grant ("mcp__bf7c680d-…__*") would have been the natural way to write it, and it launders a deliberate deny: .claude/settings.json denies subscribe_pr_activity, send_later and create_trigger by their readable names only (d671184), so under the UUID form the deny misses and a UUID wildcard would allow exactly what the repo refuses. The user-level file therefore enumerates individual read-only tools and mirrors the three denies under both UUIDs. Any future application of this mitigation has to do the same: a two-name allowlist needs a two-name denylist, or the second name is a hole.

A second open question is now partly answered. mem:connector-allowlist-recovery lists "whether a SessionStart hook's settings write affects the session that is starting" as unmeasured. Adjacent observation, one data point only: a mid-session write to ~/.claude/settings.json took effect within the same session — the next get_session call did not prompt, with nothing else changed. That is not the hook-ordering question and does not settle it, but it does show permissions are not exclusively a startup snapshot.

The readable-phase test PR #488 called for has now run, and it FAILED — so the flip is not the only cause

PR #488 closed with an explicit outstanding test: "This session is in the UUID-exposure phase, where no readable-name rule matches either, so the fix cannot be exercised here. The next session that starts in the readable phase is the test."

That session has happened. It started in the readable phase, and the corrected spelling did not stop the prompting.

  • At ~14:2x the live tool was mcp__Claude_Code_Remote__get_session — the readable form, underscores, exactly as committed.
  • The checked-out tree carried mcp__Claude_Code_Remote__get_session in permissions.allow throughout. Verified against the session's actual HEAD (228c29d, 42 commits behind main at the time) rather than against the branch tip, since the pre-commit tree is what the CLI reads: the six Claude_Code_Remote grants were all present there.
  • It still prompted. Twice, human-reported — an agent cannot see its own approval prompts, so this is the user's observation, not an inference.
  • mise run mcp-allow-check was green throughout.

Name, spelling and rule matched exactly, and the call prompted anyway. So the two-name flip is not sufficient to explain the prompting, and neither is the retired typo theory. Something above permissions.allow is adjudicating these calls.

The candidate, recorded as a hypothesis and NOT as a finding. This session ran permission_mode: auto. Three refusals in it are consistent with the auto-mode classifier deciding independently of the allowlist: the readable-name get_session prompts above; a UUID-name create_session refused with "denied by the auto mode classifier"; and a Bash heredoc writing ~/.claude/settings.json refused the same way. Nothing here establishes that — the mode was not varied, and a single session cannot separate "the classifier outranks allow rules" from "these particular verbs are classified". **The measurement that would settle it is one session in **auto and one in default, same tree, same readable exposure, same verb. Nobody has run it.

What this bounds. CLOUD-191's resolver — built today — repairs the UUID episode, and that repair is real and independently worth having, because during such an episode the two committed denies enforce nothing and AGENTS.md's ban on babysitting timers rests on them. But it cannot repair the readable phase, because in that phase there is no name mismatch to translate. A session that lands CLOUD-191 and still sees prompts under the readable name has not found a regression — it has found this, and should add its observation here rather than re-diagnosing the flip.

The CLI's own MCP logs carry a STABLE identity, and they date the flip inside a single session (measured 2026-08-31)

Every previous pass at this row worked from the tool NAME, which is the one identifier that flips. ~/.cache/claude-cli-nodejs/<project>/mcp-logs-<name>/ is a second instrument nobody had looked at, and its first line per session decides what the name cannot:

Initializing HTTP transport to https://api.anthropic.com/v2/ccr-sessions/<cse>/mcp
  ?mcp_url=https%3A%2F%2Fmcp.linear.app%2Fmcp
  &mcp_server_id=<rotates>
  &toolbox_mcp_server_id=4db58e41-cd4e-4818-8922-46cf616593f4

Three identifiers, and they do three different things. Read on one machine across 2026-08-28 → 2026-08-31:

field behaviour evidence
toolbox_mcp_server_id STABLE — and it is the one that becomes mcp__<uuid>__* 4db58e41-… in the 08-28T06:02 line and unchanged in the 08-31T20:22 line
mcp_server_id ROTATES per registration episode 0eeb2024-ad5c-5220-8a36-3d02b419b5d2 on 08-28 → c903baab-167c-55dc-9105-233bc731837c on 08-31
mcp_url stable AND human-meaningful https://mcp.linear.app/mcp

The two UUIDs were never distinguished before, and conflating them is why "is the UUID stable?" kept resolving differently. One is stable and one is not, so the answer depends entirely on which was read.

This answers a stated open question and retires a mitigation this row calls unsafe

The row records, of the remote-session grants: "which UUID is Claude_Code_Remote is not determinable from the injected config alone", and therefore mirrors the three denies under both UUIDs. It is determinable — from mcp_url. Resolved on this machine:

toolbox_mcp_server_id mcp_url connector readable dir
4db58e41-cd4e-4818-8922-46cf616593f4 https://mcp.linear.app/mcp Linear mcp-logs-Linear
bf7c680d-5fdc-5ef4-b4a0-abadb619bf0a https://api.anthropic.com/v1/code/mcp/meta Claude Code Remote mcp-logs-Claude-Code-Remote
73c8685e-fc9a-4d5a-9d90-adab126c27ff https://mcp.miro.com Miro mcp-logs-Miro

Both halves are corroborated independently of the log: this session's own tool list carries mcp__4db58e41-…__get_issue and mcp__bf7c680d-…__get_session, and the host reports 73c8685e-… as needing authentication, which is consistent with Miro being unauthorized here.

Miro is a FIFTH connector this row has never recorded, and it is the cleanest case: readable-name directory with exactly one file, from session start, and nothing since.

The flip is dated, and it happened INSIDE one session

Every log line carries sessionId. All of these are cf3f34e7-adfa-55d5-b11d-5cdbd67ae944 — one session, still running.

directory files first last
mcp-logs-Linear 4 2026-08-28T06:02:35Z 2026-08-28T15:55:12Z
mcp-logs-Claude-Code-Remote 4 2026-08-28T06:02:35Z 2026-08-28T15:55:12Z
mcp-logs-Miro 1 2026-08-28T06:02:35Z 2026-08-28T06:02:35Z
mcp-logs-4db58e41-… 64 2026-08-28T06:02:35Z 2026-08-31T20:22:17Z
mcp-logs-73c8685e-… 64 2026-08-28T06:02:35Z 2026-08-31T20:22:17Z
mcp-logs-bf7c680d-… 64 2026-08-28T06:02:35Z 2026-08-31T20:22:17Z

Every readable name stops on 2026-08-28; every UUID continues. Both start at the same instant, so the two names were live in the same session batch and the readable one was then dropped and never came back. That is the flip, with a timestamp, from the host's own record rather than from a human noticing a prompt.

What this does NOT establish, stated so the next reader does not over-read it

  • It does not answer whether the UUID survives an OAuth re-grant. No re-authorization happened in this window. toolbox_mcp_server_id is stable across registration episodes, which is a strictly weaker claim, and the open question above stands unanswered.
  • It is not a cross-container record. These logs live under ~/.cache/, which dies with the container, so this is a WITHIN-session instrument. mem:connector-allowlist-recovery's point that no container keeps a durable history is unchanged.
  • It is not a gate, and this row's "no gate is possible" clause still holds for the same reason: the file is outside the repository and keyed to account identifiers rule 1 keeps out of committed config. What changed is that the identity a fix would need now demonstrably exists and is readable.

The local OAuth store is empty, so auth rides the injected headers

~/.claude/.credentials.json is 15 bytes holding a single empty mcpOAuth map. The injected config's servers carry X-MCP-Server-ID, X-MCP-Server-Origin and X-Session-UUID and no bearer token — the endpoint authenticates the session. So there is no local token to rotate, which is one more reason the re-grant question above cannot be answered from this machine.

The classifier paragraph above is a hypothesis and a memory had promoted it to a rule

This row is careful: it labels the auto-mode classifier a "hypothesis and NOT a finding" and names the unrun experiment (auto vs default, same tree, same verb). **That care was lost downstream. **mem:connector-allowlist-recovery had recorded a permission verdict as a durable capability boundary — "blocked by the auto-mode classifier, correctly … Do not route around it" — and it was quoted in a later session to refuse a design. Excised 2026-08-31, along with a matching one in mem:prior-art-and-issue-hygiene about add_repo, and mem:core now carries the doctrine: a claim that would change depending on which permission mode the session is in is not a memory. The unrun experiment this row names is still unrun.

Why the repo cannot fix this itself

In Claude Code on the web, connectors are "provisioned by the remote host and arrive as explicit --mcp-config entries" (MCP docs), which is also why they appear as mcp__Linear__* rather than the documented mcp__claude_ai_<server>__<tool> form. The naming is chosen per registration episode by the host, not by anything under this repo's control. The durable upstream fix would be a stable server name across re-registration, or permission matching on server identity rather than exposed tool-name prefix; until then, allowlisting both names is the available mitigation.


Generated by Claude Code

CLOUD-1166 A COUNT — or a membership LIST — copied out of another row's body is republished as “today” and nothing re-derives it: 37/49 against a tree holding 52/66, three mutated copies of one blocked-class list, and two blocked verdicts asserted from a bucket nobody opened

REOPENED 2026-08-31 — closed with no mechanism, and the defect recurred four hours later in a stronger form

This row went Backlog → Done directly at 04:36:53.403, inside the ~4-second bulk cluster that closed roughly twenty rows. Its §2 deliverable is "a cited-count notation with a gate behind it" and no such gate exists in the tree. Nothing was released, so Done was never true.

The same day, the defect it names recurred — three times, over a LIST rather than a count. CLOUD-1170 owns the lease/liveness blocked class and enumerates it per program with a stated reason. Three consumer rows restated a subset from memory instead of citing it:

row restated mutation
CLOUD-1164 3 names added signing-posture — a key-verification gate that decides over neither a clock nor a live process. A program with no blocker was parked on CLOUD-1170.
CLOUD-1155 6 names added landcorrectly; CLOUD-1170's own table had omitted it (land.sh:446 now=$(date -u +%s), :255 kill -0 -- -"$1"). The owner was wrong and a copy was right.
CLOUD-1151 10 names merged the owner's table with CLOUD-1164's error and dropped CLOUD-1155's correction — published one wrong name and one missing.

All four have been corrected and every consumer now cites rather than restates.

A FOURTH and FIFTH instance, and they need a SECOND predicate — citing would not have caught either

Auditing the correction pass above found two more wrong blocked verdicts, written inside the pass that was fixing the three. Neither restated anything and neither had an owner row to cite: each was a fresh verdict, reached by classifying a program from the bucket it sat in without opening the file.

program recorded as what its own source says
evaluator-closure-check third-party-exec's unreachable residue — "a verdict record cannot carry a resolved dependency graph" :98 runs cargo metadata --locked --format-version 1, which is exactly input.tree["tool-verdict"]'s (tool, pinned version, input digest) key, and :83–:89's BATTEN_EVALUATOR_METADATA is the recorded-verdict injection seam, already in the program. It is the class's best fit.
perf-assert a producer, not a gate its header: "Gate: every measured invocation path is inside its latency budget" and "A PURE FUNCTION OF STDIN, the graph-check/claim-check interface: agents fetch, gates decide". #MUTANT at :54, exits 0/1/2. Board-gate-shaped over landed facts.

A third, board-payloads, is the same shape on CLOUD-1254: recorded as transcript-blocked when its #MISE description says the transcript is its SOURCE and its consumer — a board gate's stdin — is what CLOUD-1188 is removing. Its axis is REACHABILITY, not a missing fact.

"Cite the owner, never restate it" is insufficient on its own, and these three are why. Nothing was copied in any of them. The predicate that catches them is a second one:

A blocked verdict must QUOTE the source that produced it — the program's own #MISE description, or a path:line in it. A verdict whose evidence is the bucket the program was filed under is refused.

Both predicates are the same failure at different hops: the first is trusting a copy of a reading, the second is trusting a category in place of one. perf-record and perf sat in the same bucket and ARE producers, which is the mechanism — three of four reading as a pattern is exactly when the fourth stops being read.

Why this row must widen: a LIST fails worse than a count

A stale count is read as a stale count. A mutated membership list is read as a verdict about a specific program, and it is load-bearing in both directions:

  • a false positive (signing-posture) parks a program on a blocker it does not have, so it is never dispatched;
  • a false negative (land, omitted by the owner) sends a program into a wave that cannot land it.

SubjectFacts::died is .all(), so a unit recovers nothing unless every member's blocker set is satisfied. One wrong entry in either direction is therefore fatal to a unit, not cosmetic — which is a sharper cost than the 15-row registry drift this row was filed on.

And the obvious check is the wrong instrument

The session that found this first tried to settle the membership with git grep for date +%s and kill -0. It reads false in both directions: target-ensure delegates its lock to with-lock so its own file carries neither, hook-latency-drift times through hk and says so in its own #MISE description, while step-receipt:296 and graph-check:916 merely WRITE a stamp and decide over nothing. Reading that gap as the board being unreliable — rather than the instrument being wrong — is CLOUD-844's defect, and it produced a since-withdrawn claim that the campaign could not be classified at all.

So the freshness arm below cannot be a re-run for a list. Membership is what a program decides over, established by reading it; no single-integer command produces it. The list arm's mechanism is therefore the provenance half only — §2 arm 2 — and that is the half this row already says a freshness-only gate does not have.

What that adds to §2

Two more arms, and neither needs a command to run:

3. One owner per set. A body naming the members of a blocked class, a unit, or any set another row enumerates must cite the owning row rather than list them. A restated list is refused; a citation passes; a row that owns its own set is untouched.

4. A blocked verdict carries its source. A body recording that a named program cannot move must carry either a quotation from that program or a path:line in it. A verdict with neither is refused.

Both are decidable over the payload alone — no allowlist question, no shell evaluator, none of the §3 bound this row calls "the hard part"; arm 4's pointer need not be resolved for the arm to fire. They are the cheapest arms and the ones with measured instances — three for arm 3, two for arm 4 — so they land first, ahead of the count halves rather than behind them.

They are independent and both are needed: arm 3 catches a copied set, arm 4 catches an unread one.

Recorded on CLOUD-1253 as a commitless Done alongside CLOUD-1151; the terminal-one-column-early half is CLOUD-1169's.

The instance. CLOUD-1117's body carried "37 verdict rows and 49 routes" as a statement about the current tree. A decision record written against that row on 2026-08-29 restated the pair, in bold, twice, with the word today. Measured the same day on origin/main:

git grep -c '^\[\[verdict\]\]'       -- batten.toml   →  52
git grep -c '^\[\[verdict\.route\]\]' -- batten.toml   →  66

Both citations were wrong, by 15 rows and 17 routes. The registry had grown ~40% since the number was written, and neither the source row nor the citing record could tell.

The class, stated as a predicate rather than as this instance. A quantitative claim about the tree can be written into a row's body, where it is a measurement with a timestamp, and then cited by another row, where it silently becomes a premise. Every mechanism this repository has stops at the first hop:

  • ready-lint checks the Ready block's shape, never a number in it.
  • graph-check reads relations and status, and a citation is neither.
  • rules-drift holds prose lists to a generated schema — the right shape, over a surface where a generated authority exists. There is no generated authority for "how many [[verdict]] rows are there".
  • The reproducing-command discipline covers a claim the citing session ran. It says nothing about one it inherited, which is exactly the hop that fails.

This is CLOUD-686's shape over a number instead of a condition. CLOUD-686 is the deferral whose reversal condition is later satisfied while nothing re-fires. A cited count is the same defect with the condition implicit: the row asserts a state of the tree, the tree moves, and the assertion is load-bearing in a second row that never looked. CLOUD-929 is the same failure with the census in the body rather than cited from it; CLOUD-633 is it over a prose literal's recall. Three instances, no mechanism.

Why the citing session did not catch it here, which is the honest part. That session read §1 of CLOUD-1117, which named batten.toml's verdict registry as the row's one in-tree read, and then never opened it — it restated the body's pair instead. So the failure is not exotic: copying a number is cheaper than measuring one, and nothing charges for the difference.

Refinement — Ready

Source of truth (§1). The tree is the only authority for a claim about the tree. batten.toml for the registry counts in the instance above; more generally, whatever file the claim is about. No count in an issue body is ever authoritative for another row.

Computable deliverable (§2). A cited-count notation with a gate behind it, so a number carries the command that produced it. Concretely: a body may state a tree count only in a form that names its own reproducing command, e.g.

`52` verdict rows (`git grep -c '^\[\[verdict\]\]' -- batten.toml`, 2026-08-29)

and a gate re-runs the cited command and compares. Two arms, and the second is what makes it a gate rather than a convention:

  1. Freshness — re-run the named command over the current tree; a disagreement is a finding against the row that states it.
  2. Provenance — a count restated from another row is refused unless it also carries the command. A bare number attributed to a sibling row is the exact hop that fails, and it must be unwritable rather than merely discouraged.

The command must resolve to a value, not to prose — non-negotiable rule 3 — so the notation binds a single-integer-producing command and nothing else. The pointer emitted is the count and path:line, never the matched body text (rule 4).

Acceptance predicate (§2).

  • A body stating a tree count without a reproducing command is refused.
  • A body whose cited command re-runs to a different value is refused, and the finding names both values.
  • A body whose cited command re-runs to the same value passes, and the pass is shown able to fail — a fixture that mutates the tree under a pinned citation.
  • A body citing another row's number, with no command of its own, is refused. This is the arm the instance above needed and the one a freshness-only gate does not have.
  • A row carrying no tree count is untouched: silence, not a demand for one.
  • CLOUD-1117's live 52/66 citation passes under the new notation without an edit to the number.

Effect (§3). A new gate task reading an issue payload on stdin, like its siblings. It executes the cited command, which is an effect: the command set must be bounded — the read-only allowlist (house style §5) is the existing boundary and the gate must not become a general shell evaluator over issue bodies. That bound is the hard part of this row, not the comparison, and a design that skips it is not landable.

Generated artifacts and drift (§4). None expected. If the notation is parsed in Rust rather than in bash, crates/batten/src/ready.rs is the surface and the verdict is a [[verdict]] row.

Output and exit contract (§5). Exit 1 for a stale or uncited count; exit 2 where the cited command could not be run at all — could-not-look, never a silent pass, and never a refusal that blames the author for the gate's own inability to look.

**Commit / bump (§6). **feat(gate) — patch until 0.1.0. Not ! for the consumer: it adds a refusal, raise-only per house style §8. mise run semver decides the library half.

Test and replay obligation (§7). A .bats suite over the compiled gate with the five acceptance cases above, plus a #MUTANT on the provenance conjunct — and it must be chosen so the freshness arm does not already exclude it, which is the mutation that survives if it is not. Replay over the board is required before this can be a deny: run it across every open row and count how many numbers on the board today carry no command. If that number is large, the notation lands as a report first and the deny follows on a ratchet.

**Relationships (§8). **CLOUD-686's shape over a number; CLOUD-929 and CLOUD-633 are the two prior instances. Found while remediating CLOUD-1117.

Review in Linear

@coderabbitai

coderabbitai Bot commented Aug 31, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 38 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 5c7539ce-a2df-4a7c-b532-498e1b21ea78

📥 Commits

Reviewing files that changed from the base of the PR and between 9ece058 and 242b883.

📒 Files selected for processing (3)
  • .serena/memories/connector-allowlist-recovery.md
  • .serena/memories/core.md
  • .serena/memories/prior-art-and-issue-hygiene.md

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

Two bullets recorded a session-scoped permission reading as a durable
capability boundary, and both were later quoted to refuse work:

- connector-allowlist-recovery said replaying the injected config's
  headers was "blocked by the auto-mode classifier, correctly ... Do not
  route around it". CLOUD-178 is careful to label that classifier a
  hypothesis and names the experiment nobody has run; the memory promoted
  it to an imperative. The durable half stays: what the headers ARE
  (X-MCP-Server-ID, X-MCP-Server-Origin, X-Session-UUID, no bearer), and
  that they are session-bound and agent-readable.

- prior-art-and-issue-hygiene said `add_repo` on a survey target "is
  declined by the permission classifier", which contradicts the standing
  instruction to call it rather than pre-judge -- an unauthenticated probe
  of a private repo 404s whether or not the session is authorised, so a
  pre-check reports a false negative.

workflow/agent-fanout keeps its permission-mode paragraph: it records a
divergence between what create_session's schema documents ("omit to
inherit it") and what was measured, which is what a memory is for.

core.md, the graph root, now carries the rule: a memory records what is
re-derivable -- a path, a file format, an argv, a measured flip, an
upstream issue number. If a claim would change depending on which
permission mode the session is in, it is not a memory. The tell is an
imperative with no re-derivable object behind it.

Refs: CLOUD-178, CLOUD-1166
@wenzowski
wenzowski force-pushed the claude/stage-2-3-grooming-uqk71k branch from 45dde19 to 242b883 Compare September 2, 2026 02:14
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

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