Skip to content

fix(#312/#317): a measurement must not leave her hands, or her beliefs, in the exam room - #2153

Merged
joelteply merged 83 commits into
canaryfrom
fix/312-measurement-hands-and-inert-edits
Aug 6, 2026
Merged

fix(#312/#317): a measurement must not leave her hands, or her beliefs, in the exam room#2153
joelteply merged 83 commits into
canaryfrom
fix/312-measurement-hands-and-inert-edits

Conversation

@joelteply

Copy link
Copy Markdown
Contributor

Canary became PR-gated partway through this work, so this carries the unpushed backlog from this
lane (61 commits) rather than only tonight's five. Tonight's are the top five; the rest are the
code-receipts / anchor / enum-state arc from earlier in the same session.

#312 — exam↔live contamination, ROOT-CAUSED

Not the axis I originally filed, and not the one BigMama already fixed. Hers (17278f597) makes
the exam's DEFAULT world a CoW copy. This is the explicit --workspace rooting persisting into
the LIVING persona after the run ends.

Mechanism: code/create-workspace keys the FileEngine on caller_id(ctx) = her peer id; the
engines live in ONE process-global DashMap (ipc/mod.rs:1595, built once); a measurement fork
clones the cfg so it shares the living persona's Arc<dyn ToolExecutor> and her id. Rooting
"the fork" re-roots her, permanently. The MIND has had a guard since #59 (EvalIsolation:
NoopSink + checkpoint + rewind on drop). The HANDS never did.

Evidence it was live, not theoretical:

  • Anwen's engrams.sqlite carries code/list(path=src)flask/ and
    code/read(src/flask/app.py) as Tool receipts hours after a SWE-bench run.
  • While the fix was being written, persona fe4dac17 posted to #general: "I've claimed the task
    on designing layout primitives for widget states and navigation. I found two relevant classes in
    src/flask/blueprints.py: BlueprintSetupState at line 25…"
    — a UI-layout card answered out of
    flask internals, because that is genuinely what her hands could see.

Fix: ActingHands lifted out of the cycle (it outlives with_capture, the drive, and errors);
one drive_create_workspace for both directions; restore_acting_workspace returns her to her own
citizen layer — not to the previous value, since restoring that would faithfully preserve an
earlier leak. agent/solve wraps the rooted region so restore runs on Ok and Err;
cognition/eval restores at the command boundary (run_eval_restoring, the one entry both
inline and detached go through).

Second vector: learn mode embedded the task unbounded in the durable lesson. Six
flask-4045 runs wrote six verbatim GitHub issues into Anwen's episodic store, and her consolidator
crystallized SEMANTIC beliefs out of them ("If a Flask Blueprint name contains a dot, raise a
ValueError"
). She durably knew the answer to a held-out instance. Lesson now excerpts (200 chars);
benchmark/swe-solve passes learn: false on scored instances.

Live-verified, not just unit-tested: deployed, then a real solve — probe stream shows
workspace.rootedworkspace.restored, acts=3, patch produced, hands home after.

#317 — the write that parses and does nothing

flask-4045 measured twice; both times the model derived the correct fix and the WRITE destroyed it.
Run A was unparseable (gate catches it, and it screams). Run B wrote the guard into the class
docstring
ast.parse Ok, docstring still a docstring, every gate silent, tests failed, zero
charged to the model's intelligence.

inert_insertions on SyntaxValidator: the AST knows every string constant's byte span, so "did
my insertion land inside a literal" is a lookup, not a heuristic. Two conditions, both
load-bearing — inside a literal AND reads as code (parses and carries a statement with an
effect). It warns, it does not refuse: I had this filed as "refuse the write" and deferred it
on false-positive risk; displaced_docstrings already established that the write lands and the
receipt tells her, which is both safer and the point.

Three tests: the real run-B shape (with preconditions asserting the other gates stay silent), plus
the two false positives that would make it worthless — documenting a function, and the correct fix.

Python infra

benchmarks/swe/{run_ours,grade_local}.py deleted. benchmark/swe-solve / swe-grade have been
live Rust since 2026-08-04; I drove the Python anyway and, worse, patched the dead file when I
found the contamination — leaving the live Rust path's learn: true unfixed. README + MATRIX-PLAN
repointed at the commands. benchmarks/coder/*.py remains and should be absorbed by
benchmark/matrix.

@bigmama — flagged on airc: the learn: Some(true) line in benchmark/swe-solve was deliberate;
happy to revert if there was a reason I'm not seeing.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo

joelteply and others added 30 commits August 5, 2026 23:37
…of resident weights

Every SWE-bench RESOLVED=0 on this box was measuring a harness that never started.

Glass-boxed on the M5 today. An `agent/solve` run asked for
unsloth/Devstral-Small-2507-GGUF — the exact model the live lane was already
serving, 27.2 GB resident, answering on :58057. build_base_eval_lane_inner tried
to cold-load a SECOND copy: ~17.9 GB + 2 GiB headroom against 15.0 GB free on a
64 GB box. It can never fit, so await_eval_lane_memory_headroom deferred every 5s
for its full 180s window and then failed loud. The harness wrote a zero-byte diff
and the run reported RESOLVED=0. The model was never asked to solve anything.

This is the exact shape #310 already fixed one branch above, for external
providers: "route the measurement through the registered provider adapter instead
of cold-loading a llama-server for weights this box may not even hold." The
identical reasoning applies to a LOCAL model already resident and answering; it
just was never extended there.

share_live_serving_lane() takes that branch when the live snapshot is ready and
serving this exact base. Nothing spawned, no VRAM leased, lane: None — so a
measurement can never tear down the living persona's lane on drop. served_ctx
comes from the lane's own /props truth, the same authority the living persona
budgets against (#50).

Isolation (#59/#312) is preserved rather than traded away: what an eval needs
isolated is the GENOME and the WINDOW, never the weights. The share is refused
unless every resident adapter reads scale 0, asked of the running server rather
than assumed — this lane carries six loaded-but-unapplied genome layers, inert in
fact, but that is a fact to verify. Unreachable or unrecognized /lora-adapters
shape also refuses: blind never means "assume clean" for a number that gets
published.

Also: the defer probe logged level=Normal beside "waiting for a transient memory
spike" while the real veto was a hard RAM shortfall that could never clear. Two
different vetoes, one message, and the refusal text was in hand the whole time.
It now logs the refusal it is actually retrying.

Measured before → after, same instance, same model:
  180s deferral → hard fail → 0 bytes changed
  15s → eval.lane.shared, served_ctx=16384, run proceeds

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…at it wrote

"success: true, bytes_written: 214" confirms a write happened. It never confirms
the write went where the caller meant, and that gap is what turns a persona who
reasons correctly into one that produces a broken patch.

Glass-boxed on the M5 today, SWE-bench flask-4045, from the turn capture. Anwen
localized the bug herself and had the right fix. Then:

  act 1: code/shell `cat src/flask/blueprints.py`   <- output has NO line numbers
  act 2: code/edit insert_at line 28                 <- a number she never saw
         => the guard clause landed inside the function's parameter list
  act 6: code/read start_line=119
  act 7: code/edit line_range 62..65                 <- a region she never read
         => second hunk mangled

Both returned success: true. Nothing in her working memory said otherwise, so she
spent her remaining acts flailing through discovery tools (code/tree, commands/list
twice, a repeat search) instead of repairing damage she could not see. 12 acts, the
right two files, the right idea, an unusable patch.

A human editor shows you the result. The act->observe circuit only closes if the
receipt carries what a screen would, so WriteResult now carries applied_context:
the numbered lines around the landing site, read back from the content actually
written, with the anchor marked. Same surface code/read already returns, so there
is one way to look at a file, not two. None for whole-file writes, deletes, and
undos, where "where did it land" has no answer worth rendering.

This does not steer her cognition — no heuristic decides what she should do next.
It hands her the fact and lets the next turn use it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
Follow-up to 59efe1e, from watching the same persona finish the run I sampled
too early.

She reached a CORRECT 2-line fix to src/flask/blueprints.py (128 lines) at act 2 —
edit, read it back, edit again, exactly the act->observe loop the edit receipt was
meant to produce. Then at act 4 she called code/write with a 5-line stub
reconstructed from memory: imports gone, class hierarchy gone, SansioBlueprint
gone, her own correct fix gone. The receipt said:

    success: true, bytes_written: 214

She read the file at act 6, saw the stub, and never repaired it. Final state: the
module destroyed and the fix erased.

59efe1e gave whole-file writes applied_context: None, reasoning that a whole-file
write has no landing site to report. True, and beside the point. A whole-file write
over EXISTING content has something more consequential to report than where it
landed: what it replaced. Replacing 128 lines with 5 is the most destructive thing a
file tool can do, and it was the only path returning no feedback at all — so the
verb most able to erase work was the verb that told her least about erasing it.

overwrite_magnitude() now reports lines-before -> lines-after, names a >2x shrink
explicitly rather than leaving it to be computed, points at code/undo, and shows
the head of what the file now holds. Creating a NEW file stays silent (asserted in
the test) so the warning carries weight when it appears.

Also extends edit_anchor_line to SearchReplace and Append — a content-anchored
replace is the safer idiom precisely because it needs no line number, which made it
the idiom a careful model reaches for AND the one with no feedback. It now locates
the replacement in the written content.

Refuses nothing, gates nothing, steers nothing: the loss is stated as a fact and
what she does next stays hers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…s the SyntaxError

Three SWE-bench runs on the M5 today, same persona/model/lane, and every failure had
one shape: a guard clause placed inside an open signature.

    def __init__(
        if '.' in name:                    <- run 1 (insert_at), run 3 (search/replace)
            raise ValueError(...)
        import_name: str,

Six edits across twelve acts in run 3 alone, with the file read back in between,
and she never recovered. 59efe1e gave her the numbered lines around the landing
site and it was not enough: the lines look like plausible Python. The defect is that
they are not valid Python, and grammar is exactly what a window of text cannot show.

code/edit now runs the file's own checker on the written content and puts the real
error at the TOP of applied_context, ahead of the neighborhood — "SyntaxError: line
21" with the caret is unambiguous where six lines of context are not. Python only for
now (py_compile, milliseconds); every other extension returns None. A missing
interpreter, unknown extension, or spawn failure also returns None: a tool that
cannot verify must not render a verdict.

Reported, never refused. The edit still applies and code/undo is named in the
message; what she does next stays hers ([[no-hardcoded-heuristics-to-steer-cognition]]).

Not a build — running the project's real tests is the persona's own move via
code/shell, not something a file write does behind her back.

Two rejected designs, both mine, both falsified before shipping:
  - bracket-balance counting: inserting INTO `def f(...)` leaves the closing paren in
    place, so the file stays balanced and stays unparseable. Caught run 3's duplicated
    `super().__init__(` and missed run 1 entirely. Its own test killed it.
  - this check, apparently — until I re-read the test and found the bug was mine: the
    "clean edit must not warn" assertion appended valid code to the file the previous
    assertion had just broken. Each assertion now gets its own file, and the comment
    says so, because that mistake nearly discarded a working implementation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… it moved

The orphaned-parameter-tail defect on SWE-bench flask-4045, glass-boxed from run 4's
capture (M5, 2026-08-04). Her three acts, in order:

  1. code/read src/flask/blueprints.py          -> she now holds ORIGINAL line numbers
  2. code/edit line_range 16..17, 2 lines -> 4  -> everything below slides +2
  3. code/edit line_range 14..23                -> computed from the numbers in act 1

Act 3's replacement block was correct and complete — a properly closed signature, the
guard clause, a right super().__init__ call. It landed two lines short of the region
it meant to cover, so the tail of the OLD parameter list survived underneath it and
the file stopped parsing. Nothing in act 2's receipt said the map had moved; the
window it showed was six lines wide and the drift was outside it.

So the receipt says it. `line_shift_notice` reports the line-count delta and states
that numbers taken from an earlier read are now stale below the landing site. The
delta is arithmetic, not inference, and it is a statement about the FILE, not an
instruction about what to do next ([[no-hardcoded-heuristics-to-steer-cognition]]) —
re-reading and adjusting by the delta are both correct responses to the same true
fact. Silent when the line count is unchanged: an in-place replacement invalidates
nothing, and a notice that fires on every edit is a notice nobody reads.

Third in the same series, all aimed at the same principle — a receipt that reports
only "success" leaves an act→observe loop with no observe: 59efe1e (where the edit
landed), 4e74d93 (what a whole-file write destroyed), 7de489a (the real
SyntaxError). Verified live against the deployed core, not just in test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… receipts that fall out of it

ROOT CAUSE of the "statement inside an open paren" failure that ran through SWE-bench
flask-4045 runs 1, 3 and 5 (M5, 2026-08-04). `code/edit` addresses lines BY NUMBER.
`code/read` returned bare text. She read the whole 128-line file, counted by hand, asked
for `insert_at` line 35, and landed ~4 lines off — inside `super().__init__(`'s argument
list, where a statement is a SyntaxError:

        super().__init__(
            name,
            import_name,
        if '.' in name:            <- act 2, first edit of the run
            raise ValueError(...)

Two earlier fixes chased this from downstream and were both real but both secondary: the
parse check (7de489a) catches the break a window of plausible-looking lines cannot show,
and the line-shift notice (5ddd2c2) catches the SECOND edit's version of the same
coordinate problem. Neither addressed a read tool and an edit tool that simply disagree
about whether lines have numbers. That is a defect in the PAIR.

Reads are now numbered by ABSOLUTE file position in the same `{n:>6} | ` gutter
`applied_context` already uses, so one file reads identically before an edit and after one.
A windowed read numbers from the file's start, never from the window's — a relative number
is worse than none, because it looks addressable and is not. The command description says
so, so the tool advertises its own contract.

Three receipts fall out of that change, each stating a fact and refusing nothing
([[no-hardcoded-heuristics-to-steer-cognition]]):

- code/write names content that looks like pasted read output. Numbering creates this
  hazard, so the warning ships WITH it rather than after it first corrupts a file. Needs a
  90% majority of gutter-prefixed lines and a 3-line floor, so real code stays quiet.

- A broken parse says WHO broke it. Run 5: an early edit wrecked the file, and four acts
  later she worked out the right idiom herself — a content-anchored SearchReplace — and
  applied a CORRECT fix on top of the still-present wreckage. Across five runs she repaired
  forward every time and reverted zero times. The receipt said "code/undo restores the
  previous content", which is advice; it now states the fact she cannot compute — this file
  PARSED before your edit, so the break is one change deep — and hands over the change_id.
  When the file was already broken it says that instead, so she is never sent to undo the
  wrong change. Only pays for the second parse when the first one failed.

- code/delete reports what it removed. It was returning `success: true, bytes_written: 0`
  for destroying a file — indistinguishable from a no-op, and the same "no neighborhood to
  show, therefore nothing to report" error that left code/write silent about overwrites
  until 4e74d93.

Three pre-existing tests asserted the old read contract. Two were EDIT tests that used
read() only to observe; those now assert against the file itself, since coupling an edit
test to the read tool's display format is exactly what broke them.

Verified live against the deployed core, not only in test. 186 code-module tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ost a measurement

`share_live_serving_lane` asked the running llama-server `/lora-adapters` whether any
genome layer was APPLIED before borrowing the lane. Two things were wrong with that.

It could not answer. Genome activation on this lane is PER REQUEST by design: the daemon
loads the `--lora` catalog and immediately zeroes every global scale
(`llama_server::zero_adapter_scales` — "catalog LOADED, scales DORMANT (0.0), per-request
activation only"), and a turn pages its gene in through the request body's
`lora: [{id, scale}]`. The global scales the probe read are therefore always 0.0. The
question had one possible answer, so asking it carried no information.

And it failed in the worst direction. `/lora-adapters` BLOCKS while the lane is
generating — measured on the M5, 2026-08-04, against the live lane:

    /health         -> 200 in milliseconds
    /lora-adapters  -> no response in 5s

The 3s timeout expired, the `else { return false }` arm fired, and the probe reported
the definite claim "live lane serves this base but has an APPLIED genome layer". So the
share was refused exactly when the lane was BUSY — precisely when sharing matters — and
each refusal cold-loaded a second 17.9 GB copy of weights already resident. On the run
that exposed this, that copy missed the GPU by 126 MB, spilled to CPU, and turned a
measurement into one that could never finish (`acts: 0`).

The eval adapter is built WITHOUT a `lora` field, so it measures the bare base by
construction, whatever else sits in the catalog and whoever else is on the lane. That
invariant is guaranteed upstream by the code that applies adapters; re-deriving it over
HTTP was both redundant (two sources of truth for one fact) and fragile (the one that can
hang). The comment left in its place records why there is deliberately no probe here.

The principle in the deleted doc — "blind never means assume-clean for a number that gets
published" — was right, and is why this is a deletion rather than a longer timeout. The
error was treating an unanswered HTTP call as evidence about something already true.

Verified live: `eval.lane.shared ... no second copy of these weights cold-loaded`,
base_url=http://127.0.0.1:58057/v1, served_ctx=16384. 16 cognition::eval tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…top resolving a stale `cu`

Today eight flask-4045 runs were driven by hand against a clone left at flask HEAD. The
upstream fix was already in that tree (`sansio/blueprints.py:199`), so FAIL_TO_PASS passed
BEFORE any persona acted. Every score in the series was void — the zeros and the one that
looked like a clean 3-act win alike. I was one step from reporting that win.

`run_ours.py` was not at fault: it clones at `base_commit` and defers grading to the
official Docker harness, which applies `test_patch` itself. What was at fault is that the
official harness is heavy enough to get skipped, and a skipped gate is not a gate.

grade_local.py — the same protocol, in seconds, no Docker (pure-Python instances):

    clone @ base_commit -> apply model_patch -> apply test_patch -> run tests
    RESOLVED iff every FAIL_TO_PASS passes AND the PASS_TO_PASS sample passes

Its first act is the gate that would have caught this instantly: run FAIL_TO_PASS on the
PRISTINE tree and REFUSE (exit 2) if it already passes, because a checkout without the bug
cannot distinguish a fix from a no-op. `--gold` is the spine check and a gold failure is a
loud exit 3, never a warning — if the dataset's own patch does not resolve, the environment
is wrong and no persona number from it means anything.

Verified end to end on pallets__flask-4045:
  [gate] FAIL_TO_PASS fails on the pristine tree (2 tests) — the bug is present
  FAIL_TO_PASS 2/2 · PASS_TO_PASS 40/40 · RESOLVED=1 (gold)
The gate also earned its keep on the way: the first gold attempt failed on
`_pytest.monkeypatch.notset` (removed in pytest 7, this flask predates it), named the
environment as the cause, and refused to report — exactly the intended behavior.

run_ours.py: `continuum_cli()` replaces two hard-coded `cu` lookups. `cu` is the OLD name
(and macOS call-unix); worse, a `release/cu` from Jul 25 was still sitting in the cargo
target dir, so a run would have silently driven TEN-DAY-OLD client code against a freshly
deployed core and reported it as current. Resolves `continuum` from PATH first — what
`continuum reboot` installs — falling back to cargo-target, and fails loud rather than
reaching for something older ([[verify-the-deploy]]).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… instance's date

`--auto-venv <python>` replaces the hand-built venv. Without it, scaling past one instance
means hand-maintaining a pin table per repo per era, which does not scale and silently rots.

The trick is the date. These instances are years old and their packaging is almost always
upper-bound-free: flask 2.0 asks for `Werkzeug>=2.0`, so a plain `pip install -e .` in 2026
resolves Werkzeug 3.x, which deleted `url_quote`, and the repo cannot even import.
`uv pip install --exclude-newer <created_at>` resolves the whole graph against the index as
it stood that day — the same thing the official Docker images bake in, as one rule for
every repo.

But the date applies to the SUBJECT only, and finding that line cost two failures worth
recording:

  - date-pinned pytest drags in 2021 `py`, whose vendored apipkg raises
    `AttributeError: __spec__` under Python 3.11's import machinery
  - date-pinned setuptools predates PEP 660, so `build_meta:__legacy__` has no
    `build_editable` and the install dies outright

So: repo dependencies are historical (they define the behavior under test); pytest,
setuptools and wheel are modern (they are harness, and must run on THIS interpreter), with
`--no-build-isolation` letting the two coexist. Falls back to plain venv+pip when uv is
absent or the row carries no date.

Also fixed a defect I introduced with the cache: it keyed on "the venv directory exists",
so a FAILED install became sticky — every later run reused an env with no repo in it and
reported a gold failure whose real cause was three steps upstream. A failed build now
deletes the env and exits loudly. Same lesson as the rest of this harness: a check that
silently degrades is worse than no check.

Verified end to end on pallets__flask-4045 from a cold cache:
  [env] building pallets__flask-4045 venv (deps as of 2021-05-13)
  [gate] FAIL_TO_PASS fails on the pristine tree (2 tests) — the bug is present
  FAIL_TO_PASS 2/2 · PASS_TO_PASS 40/40 · RESOLVED=1 (gold)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…-id shape

sympy's FAIL_TO_PASS entries are bare function names (`test_solve_biquadratic`) because
sympy ships its own runner. Handing those to pytest as paths produced

    ERROR: file or directory not found: test_solve_biquadratic

which scored as a test failure and looked exactly like one. Gold came back 0/1 F2P and
0/40 P2P on instances whose environment was completely fine — the 0/40 was the tell, since
PASS_TO_PASS failing wholesale means nothing ran, not that the repo is broken.

Now: derive the test files from the instance's own test_patch, run pytest ONCE over them,
parse the per-test verdicts, and look each required id up by node id OR bare function
name. A test missing from the report is still a failure, but a named one, and a run that
collects nothing prints the pytest output instead of silently reporting all-fail.

Also picks the interpreter by the instance's era. A 2014 requests vendors a urllib3 doing
`from collections import Mapping`, deleted in 3.10 — no dependency pin can rescue that,
the language moved. uv fetches the era-appropriate CPython.

Verified: sympy-22005 and sympy-21379 gold now RESOLVED=1 (were 0). This unblocks the
77 sympy instances, the largest locally-gradeable block in Lite. Not fixed by this and
recorded as such: 2014-era psf/requests tests call httpbin.org over the network — gold
reaches 1/10 F2P and those instances need the official Docker images.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…, explain

sympy-22005, agent/solve, glass-boxed today. She asked for line_range 240..247 of
polysys.py. The block she meant ends at 249 — lines 248-249 are the tail of a triple-quoted
string. Her replacement carried its own `'''))`, so the original's last two lines survived
after it and the module stopped parsing. The change itself was reasonable. Semantics right,
extent wrong, score zero.

The syntax receipt shipped earlier today DID catch it, named the break, attributed it to
that edit, and handed over the code/undo change_id. Not enough: across every run measured
this session she repairs FORWARD and has never once reverted, so the wreckage stayed under
the later edits. A break that gets reported and not acted on leaves a broken file either way.

So the check moves from reporting to GATING. If a file parsed before an edit and would not
parse after, the edit is refused: nothing is written, no ChangeNode is recorded, the file is
byte-for-byte unchanged, and the error names the likely cause (a range ending inside a
construct it does not close) and the anchored alternative. Same information, same moment —
but no wreckage for a later edit to stack on, and no dependence on her choosing to undo.

This is not a heuristic steering cognition. It is the tool declining an operation it can
PROVE is destructive, and the only condition that fires it is "parsed before, does not parse
after", which no correct single edit produces. Bracket balance can't detect this and was
falsified earlier for the insert case; the gate is a real parser. Whole-file `code/write`
stays deliberately ungated — when content that doesn't parse yet is genuinely intended, that
is the verb.

Consequences carried through rather than left inconsistent:
- syntax_error_after_edit's "your edit caused this" branch is now unreachable from the edit
  path, so it collapses to the one case that remains (damage that predates the edit) instead
  of keeping code that claims a state it can't reach.
- The two tests encoding "we report, never refuse" now encode the refusal; both were renamed
  because the old names describe behavior that no longer exists.
- probe_parse returns three-way (clean / error / cannot-tell) so "could not verify" can never
  read as "already broken" — an unknown language simply never triggers the gate.
- The refusal rewrites the temp probe's filename to the real one; pointing a persona at a
  path that doesn't exist is its own defect.

Validated live, not just in tests: replayed her exact mis-bounded edit through code/edit on
the deployed core — refused, file md5 unchanged; the same edit with the range widened to
cover the construct applies, lands the fix, and py_compiles clean. 44 file_engine tests and
187 code:: tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ing from TS

`applied_context` was added to the Rust WriteResult earlier today (5ddd2c2, the
receipt-carries-the-landing-site work) but the ts-rs binding was never regenerated, so the
TypeScript type was missing the field. Caught when a cargo test run rewrote the file.

No behavior change — this is the generated projection catching up to its source. The
ts-rs-binding-drift-guard CI job checks exactly this, so it would have gone red on the
branch regardless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ython of ours

Benchmarks are the curriculum spine, so every benchmark operation is a command on the
registry, not a script — same contract as a model download
([[benchmark-infra-is-substrate-commands-handles-events-never-bash]]). `swe-bench-lite` has
been a catalog row since benchmark.rs was written, with Grader::Python documented as
"catalogued; grader lands with the python collections". This is that grader.

What the Python harness cost in one morning, all of it structural rather than incidental:
a zsh word-splitting bug that ran the entire instance list as one bogus id and failed the
whole batch; a 60-minute poll loop with buffered output where a dead dispatch and a working
one looked identical; no liveness signal at any point; and before that, eight runs scored
against a clone left at HEAD plus gold mis-scored on three instances by an id-shape
assumption.

The one Python that remains is the SUBJECT. flask's and sympy's suites are pytest; running
them IS the benchmark, no more a dependency of ours than rustc is for humaneval-rs. We write
none. `uv` — itself a Rust binary — builds the per-instance environment.

cognition/swe_bench.rs owns the protocol: dataset fetch (cached on first use, never a gated
install), clone at base_commit, patch application, era-matched interpreter + date-pinned
deps, one pytest run per instance, verdict resolution. commands/benchmark.rs gains
`benchmark/swe-grade` on the AiSafe surface, so a persona can score her own work.

Three hazards are encoded rather than left to discipline:
- THE GATE: FAIL_TO_PASS must FAIL on the pristine tree. `gateOk: false` marks a run whose
  score is void, and it reaches the caller so a tally can EXCLUDE it rather than count it.
- ID SHAPE: sympy ships its own runner, so its FAIL_TO_PASS entries are bare function names.
  Every required id resolves by node id OR bare name against one parsed report.
- ABSENCE ≠ ZERO: a verdict carrying `error` is a run that could not happen. Tallying an
  environment failure as "the model failed" is how a broken harness becomes a number.

Verified live on the deployed core, not only in tests:
  continuum benchmark/swe-grade --instance sympy__sympy-22005 --gold true
  → resolved: true, gateOk: true, F2P 1/1, P2P 2/2, in 15.7s

10 unit tests (5 protocol + 3 command + 2 binding exports) green; full `code::` suite 187
green. grade_local.py is superseded and comes out next, with the solve driver following it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ve command

Closes the port. `swe-solve` clones at base_commit, drops the persona's WHOLE self into that
repo with the issue, and grades the patch she produced — in a SECOND, pristine clone. Her
working tree is never the scoring tree, which is what makes the gate mean anything: a solver
that dirtied its workspace cannot launder that into a passing score.

It composes agent/solve IN PROCESS (`solve_body`), so there is no subprocess, no CLI hop, and
no polling a file we wrote ourselves — the thing the command surface exists for. Fire-and-poll
mirrors agent/solve's proven shape (#86): a real agentic drive outlives the IPC socket, so a
sweep detaches and the verdict lands in ~/.continuum/progress/swe-solve-<runId>.json, with a
LOUD failure marker if the run dies rather than an empty file forever.

Recall stays ON and learn stays ON. She is measured as herself, never a stripped copy
([[benchmark-must-never-score-persona-against-a-soul-stripped-copy]]).

Also fixes a real bug the fail-loud path caught on its first live run: clone_at did not create
the parent directory, and git's failure surfaced late and cryptically as "unable to write
.git/objects/pack/*.pack: No such file or directory" mid-fetch — which reads like a disk or
network fault, not a missing mkdir. The ledger marker is what made it diagnosable in one look.

11 swe tests green (5 protocol, 3 grade, 2 solve, + binding exports); 31 green across the
`swe` filter including ts-rs emits.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… instance grades as-is

Benchmarks are becoming curriculum, not just measurement: the corpus compounds as instances
accumulate, and a teacher can extend it via simulated work (Joel, 2026-08-04). The fields here
describe a repo, a commit, a fix, and the tests that define the bug — none of that is specific
to a HuggingFace row. A generator that reverts a known-good commit in a real repo and keeps
its tests as the FAIL_TO_PASS set (that IS gym/mine, #133) produces the SAME type, and
`grade()` scores it with zero new code.

Cheap now, expensive later: the failure mode is a second, divergent "generated instance"
struct with its own near-identical grader. `load_dataset` stays the only HF-shaped thing;
everything downstream takes `&SweInstance`.

Comment-only — no behavior change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…tch per repo

Two defects in what I shipped an hour ago, both the same root cause: I built benchmark infra
OUTSIDE governor supervision, which is exactly the separation Joel called out — "the more of
it that's rust and under governor supervision the better."

1. UNGOVERNED CACHE CLASS. `~/.continuum/benchmarks/` was written unbounded with no TrackedDir
   row and no eviction decision. CLAUDE.md mandates both, and
   `every_cache_class_has_a_decided_eviction_story` is supposed to fail on an undecided class —
   it passed, because it only checks classes already REGISTERED. That is the guard's blind
   spot: it enforces "every tracked class has a decision", not "every written-to dir is
   tracked". Now registered, with a decision (LRU over per-instance dirs skipping the in-flight
   set; clones and venvs are re-creatable from git+uv, so only an active grade is at risk).
   Falsified the guard to prove it bites: removing the decision fails with "cache class
   'benchmarks' has NO eviction decision"; restored, green.

2. TWO FULL NETWORK CLONES PER INSTANCE. The protocol needs her tree and a pristine scoring
   tree, and a sweep re-grades the same repo across many instances — so this was ~240 MB of
   network AND disk per tree. A 300-instance Lite sweep would have been ~140 GB and hours of
   fetch: the 2026-07-13 460 GB incident shape, freshly re-created. Now one bare mirror per
   repo, both trees `git clone --shared` from it. Safe precisely because these trees are
   disposable — never pushed, mirror only fast-forwarded — so the usual "pruning the parent
   corrupts a borrower" caveat does not apply to a cache we recreate from scratch. A stale
   mirror missing a newer base_commit refreshes and retries once rather than rotting into
   "instance not gradeable".

Measured on the deployed core, gold-gating two sympy instances:
  cold repo   13.8s   |   same repo again  6.8s  (was ~15s)
  disk, 2 instances: 352 MB total (206 MB shared mirror + worktrees) — was ~1 GB
  network fetches across the 77-instance sympy block: 1, was 154
Both instances RESOLVED=1 with gateOk — the win costs no correctness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… orphan reaper

A detached benchmark run is a tokio task INSIDE the core, so a restart kills it with no child
process to notice and no error to report. Worse, the ledger was only written on COMPLETION —
so "still working" and "died an hour ago" were the same observation: an absent file. Found the
honest way today, twice: I rebooted over my own run and it vanished without a trace.

That is exactly #137's shape (41 train jobs submitted, zero outcomes recorded, all orphaned by
reboots), one level up. Same three-part fix, reusing its pattern rather than inventing one:

1. MARK AT DISPATCH. The ledger is written `state: "running"` BEFORE the task spawns, so the
   run is observable from the instant it exists. This is the piece that makes the other two
   possible — you cannot guard or reap what leaves no trace.
2. REBOOT GUARD. `continuum reboot` refuses while runs are in flight and NAMES them
   ("guardtest on sympy__sympy-21379"), mirroring the training guard's consent-gate shape:
   the denial states the policy AND the path (`--force`). `--force` warns and proceeds.
3. BOOT REAPER. Any run still `running` at boot belonged to a core that no longer exists, so
   it is rewritten as failed with the cause. Sits beside the existing lane-registry orphan
   sweep in serving_daemon — same reclaim, one level up.

`in_flight_solve_runs` is the single source both the guard and the reaper read; returning
(run_id, instance) rather than a bool is what lets the guard name what it would destroy, which
is the difference between a policy and a nag.

Test pins the contract including what must NOT happen: a finished verdict is never rewritten
(reaping a real measurement would destroy the only record of it), another subsystem's ledger is
untouched, and a second boot finds nothing to re-reap.

Verified live on the deployed core, all three in sequence:
  dispatch  -> {"instance":"sympy__sympy-21379","runId":"guardtest","state":"running"}
  reboot    -> REFUSED, naming the run and offering --force
  --force   -> warns, proceeds
  next boot -> {"failed":true,...,"error":"killed by a core restart — ... Nothing was scored;
               re-dispatch to measure this instance."}

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
I wrote `capture_dir: None` with the comment "a sweep writes none by default". Backwards on
two counts, both found while trying to glass-box a live run and discovering there was nothing
to look at.

GLASS BOX: a benchmark run is the run you most need to see. Without a trace the only output is
a boolean, so a failure teaches nothing about whether she aimed wrong, looped, or ran out of
acts. The live prompt-capture file also rotates at ~48 KB (persona_id.jsonl → .prev.jsonl), so
a 30-act drive overruns its own history even when capture IS on — a per-run directory is the
only way the whole episode survives.

CURRICULUM: benchmarks are becoming training corpus, and the corpus is the TRACE, not the
verdict. `resolved: true/false` trains nothing; the episode does. Discarding it threw away
everything the run was worth beyond one bit — the exact opposite of what the L1→L3 flywheel
needs from it.

Per-instance directory under the governed `benchmarks` cache class, so a sweep's episodes
never overwrite each other and the whole set is still covered by the eviction decision.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…on't say "widen it"

The gate stopped her destroying good work; it did not help her land it. On sympy-22005 she
asked for line_range 240..247 when the block ends at 249 (248-249 are the tail of a
triple-quoted string). The refusal told her to "widen the range to cover the whole construct"
— which is precisely the judgement she had just made and got wrong. Repeating an instruction
someone has already failed is not help.

The same parser that proved the edit destructive can prove which end line is right. So it
counts: widen the range one line at a time, apply, parse, and report the first that works —

  THE FIX: use end_line=249 instead of 247. Your range stopped 2 line(s) short — lines
  248..249 are the tail of a construct your replacement re-opens and closes itself, so the
  original's tail survived after it. The SAME new_content with end_line=249 parses clean;
  re-issue the edit with that one number changed.

Nothing is guessed. Every candidate is APPLIED and PARSED; the number is a fact, not a hint.
When no widening in the window parses, she gets the general guidance instead of a confident
wrong answer — the failure mode is silence, never a bad number.

Bounded at 40 lines and only for LineRange. The shapes that actually swallow an edit — a
docstring, an argument list, a nested literal — close within a couple dozen lines; a wider
window would mostly buy the ability to "repair" a range so wrong that widening it silently
eats unrelated code, which is worse than saying nothing.

Costs nothing on the happy path: this runs only after an edit has already been refused.

Test asserts the NUMBER and the distance ("end_line=9 instead of 7", "stopped 2 line(s)
short"), not just that some advice appeared — a test that accepts prose would pass on the old
useless message. 44 file_engine tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
"Search text not found: 'if len(univariate) == 1:'" is true and helps with nothing. Worse,
this error sits on the path the edit-refusal deliberately steers her onto — "anchor on the
text itself with a search_replace edit" — so a redirect I added today was landing on a dead
end. A handoff between two of my own fixes that drops her.

The tool is holding the file. It can say WHY the match missed:

  SEARCH TEXT NOT FOUND — but line 243 matches once whitespace is ignored, so this is an
  INDENT mismatch, not a missing line.
  the file has : "        if len(univariate) == 1:"
  you searched : "if len(univariate) == 1:"
  Line 243 starts with 8 space(s). Copy it exactly — including leading whitespace — or use a
  line_range edit on line 243, which does not depend on reproducing the indent.

Leading whitespace is the dominant real cause and it is INVISIBLE in a quoted string: a model
reproducing a line from a numbered read routinely drops or normalises the indent, then cannot
see the difference between what it sent and what is there. So the miss names it, shows both
strings in debug form where the spaces are visible, states the exact indent, and offers the
escape that does not depend on reproducing whitespace at all.

When nothing matches on whitespace, it points at the nearest line by token overlap — that is
what distinguishes "you mis-remembered" from "the file changed under you". The neighbour must
share at least half the tokens, so an accidental `self`/`)` overlap never gets presented as
"closest"; a bogus pointer is worse than admitting no match, and the test pins both directions.

This is ergonomics, not rigging: every word is a fact about the file she is editing, and WHAT
to change stays hers. An error a persona hits should tell her what to do differently — that is
the whole contract ([[px-persona-experience-tools-as-good-ux]]).

46 file_engine tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… indent case

The prior message called leading whitespace "the dominant real cause" of a search_replace
miss. Live check says that is wrong in one direction and right in another, and the difference
matters for anyone reading the code later:

- UNDER-indenting does NOT miss. `contains()` matches "if len(x) == 1:" inside
  "    if len(x) == 1:", so dropping the indent — the case I assumed was dominant — already
  worked and never reaches the error path at all. The tool was more forgiving than I claimed.
- OVER-indenting DOES miss, and now reports:
    SEARCH TEXT NOT FOUND — but line 3 matches once whitespace is ignored, so this is an
    INDENT mismatch, not a missing line.
    the file has : "    if len(univariate) == 1:"
    you searched : "        if len(univariate) == 1:"
    Line 3 starts with 4 space(s). ...

So the indent branch is narrower than advertised — it covers over-indent and internal
whitespace drift, not "the dominant cause". The nearest-line branch, verified separately, is
the one that carries the general case.

Both verified against the deployed core, not just unit tests. No code change; this exists so
the commit log does not carry a false rationale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ere (PX)

The old message ended with "check the path, e.g. with code/list or code/tree" — true, and it
costs her an ACT to go answer a question the tool could answer itself. The tool is holding the
workspace. It can walk the path, find where her model of the tree diverges from the filesystem,
and say so.

Two different mistakes were being reported identically, and they need different corrections:

  FILENAME typo — resolved outright, not listed:
    No file at 'core/continuum-core/src/code/file_engin.rs'. There is no 'file_engin.rs' in
    'core/continuum-core/src/code', but there IS 'file_engine.rs' — did you mean
    'core/continuum-core/src/code/file_engine.rs'?

  GUESSED STRUCTURE — she invented a layout; name the divergence point and show the real one:
    No file at 'core/src/file_engine.rs'. The path is good up to 'core' — but that directory
    has no 'src'. It contains: README.md, airc-test-fixtures, archive, continuum-airc-protocol,
    continuum-core, …+15 more.

The second is the expensive one: without it she guesses again, and each guess is an act. With
it, the next act is a corrected read.

Suggestion budget scales with name length (0 typos under 4 chars, 1 up to 8, 2 beyond) so a
short name never gets a confident wrong match — under 4 characters a listing is more honest
than a guess. When nothing is close the generic advice stands: a wrong "did you mean" is worse
than the message it replaces, and the test pins that direction too.

Computed at construction while the workspace is in hand, carried on the error, empty when
nothing useful was found. Verified live against the deployed core for both shapes; 190 code::
tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…very command

serde reports one side: "missing field `cmd`". It never mentions that the caller sent
`command`, so the message reads as "you forgot something" when the truth is "you called it
something else, and here is the something". Every tool inherits this, because this is the only
place a params object is decoded.

  code/shell {"command": …}  → missing field `cmd`. You sent `command` — this command calls
                               that parameter `cmd`, not `command`. Re-send with `cmd`.
  code/read  {"path": …}     → missing field `file_path`. You sent `path` — this command calls
                               that parameter `file_path`, not `path`. Re-send with `file_path`.

Evidence it is high-frequency: I hit both myself, on two different commands, within two
minutes, while probing something unrelated. `command` is what every shell tool on earth calls
that parameter, so a model reaching for the industry-standard name lands here constantly and
had nothing to correct toward.

Two kinships, because the real pairs come in two shapes — and my first version got this wrong
in a way its own test caught:
  QUALIFIER   `path` ⊂ `file_path`   substring
  ABBREVIATION `cmd` ⊆ `command`     subsequence — `cmd` is NOT a substring of `command`,
                                     the letters are not contiguous, so substring alone
                                     failed on the exact case that motivated the fix.
Subsequence requires 3+ chars so a 1-2 letter param never matches half the alphabet, and both
relations still refuse unrelated names (`cmd` is not a subsequence of `colour` — no `m`).

`command` is deliberately NOT filtered as a transport field even though the CLI adds it:
suppressing it would break the likeliest real mis-name. A little CLI noise beats losing the
diagnosis — also caught by the test rather than by me.

Naming what was SENT is the fix; the rename suggestion is a bonus. When nothing is close it
degrades to listing both sides, and an empty call gets its own sentence — a confident wrong
rename would send her to change the wrong field, and the tests pin that direction too.

20 command_envelope tests, 275 runtime:: tests green. Both shapes verified live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ace-graded turn (THE settle artery)

Glass-boxed on SWE-bench sympy-21379: two ticks total. Tick 0 `code/tree({})`, tick 1 a
prose explanation of the bug addressed to the user. acts 1, patchBytes 0, filesChanged [],
29 of 30 acts unspent, run over. She narrated instead of acting, and the driver accepted it
as a finished turn.

The proprioception circuit was HEALTHY — tick 1's bids do carry
`[action #1] code/tree(…) Result: …`. Nothing in the plumbing was broken. The defect is
one level up: EVERY perception fact settle_step records ([unfulfilled] #122,
[confabulation] #144, [unobserved], [unacted], the peer-echo fact #303) is written AFTER
the decision, on the way out. That's correct for the live heartbeat — the metronome ticks
again and she perceives it. It is silently DEAD on drive_to_settle, which returns
immediately on SettleStep::Spoke. The whole family was write-only on the benchmark path:
a diagnosis nobody ever opened.

The fix is a THIRD structural fact on TurnFraming, alongside `directed` and
`self_initiated`: `workspace_deliverable`, declared by the CALLER. benchmark/swe-solve sets
it because SWE applies the diff and never reads the speech. drive_to_settle then hands a
zero-mutation Speak exactly ONE re-perception carrying [no-deliverable], then settles.

Why this is substrate and not scaffolding: the fact is true and structural (the caller
declared the contract; working memory holds no mutation receipt), it names no file, no fix
and no next tool, and the decision stays entirely hers — the same shape as every other
proprioception fact in that arm. Bounded at one, so a determined Speak is never trapped and
the ceiling is a single extra generation.

The ordering trap it hides, pinned by its own test: settle_step records its SETTLEMENT
MARKER before returning (which is why that arm snapshots `pre_settle` first). So the
obvious "did this concern mutate?" scan — everything after the last marker — reads an EMPTY
tail and calls every turn unmutated, firing the nudge at a persona who had just written the
file. The concern that settled is the span ENDING at that marker.

Also: refuse concurrent swe-solve runs, loudly.

agent/solve roots her hands by driving code/create-workspace through her own executor, and
that command KEYS ON THE CALLER (persona identity), not on the cycle. A second concurrent
solve for the same persona re-roots the FIRST one's hands: last writer wins, both drives
edit one repo. Measured today on persona fe4dac17, two concurrent detached solves, both
28 acts:
  sympy-22005 (polysys task) → its own workspace diff EMPTY, filesChanged []
  sympy-21055 (refine task)  → filesChanged [refine.py, polysys.py, test_polysys.py] —
                               its own work AND 22005's, including a hunk that deleted a
                               binding still referenced three lines down (a guaranteed
                               NameError that parses fine)
Two garbage numbers and nothing anywhere saying why. Refusing is the honest floor until the
root becomes per-cycle state; the error names the conflicting run so the caller can wait.
[[a-benchmark-zero-is-a-claim-about-the-harness-until-proven-otherwise]]

Deployed and verified live (build == HEAD) before commit; 24 act_observe tests green
(3 new), response_cleaning 28, sdk_codegen 21, ts-rs bindings regenerated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…never read as a capability zero

`SettleOutcome::inference_error` has existed the whole time, and its own doc says the
grader MUST treat it as an infrastructure failure and NOT a wrong answer. Nothing read it.
grep for `inference_error` in agent/solve.rs and benchmark.rs: zero hits.

So the doctrine was written into the type and ignored by every consumer of it.

Measured today, sympy-21379, the verification run for the settle-artery fix. The trace:
  tick 2  act   code/tree
  tick 3  SPEAK  ← the narrate; [no-deliverable] fires, she re-perceives
  tick 4  act   code/list        ← back to work, exactly as intended
  tick 5  act   code/tree
  tick 6  act   code/search "def subs"
  tick 7  act   code/read sympy/core/basic.py
  tick 8  act   commands/help
  tick 9  act   code/list
  tick 10 (no decision)
and the last tick's deliberation bid carries:
  "llama-server: model 'unsloth/Devstral-Small-2507-GGUF' is not the active served model
   (serving: <none>, ready: false)"

The serving lane went away mid-drive at act 7 of 30. The verdict reported
`acts: 7, resolved: false, patchBytes: 0` — with nothing, anywhere, saying inference had
died. That is indistinguishable from a persona who investigated and failed, and I nearly
read it as one.

Fix: thread it through. AgentSolveResult and SweSolveResult each gain `infra_error`,
populated from `settled.inference_error`, documented as "this row is NOT a score" and "any
aggregate MUST exclude rows carrying this".

This is the [[a-benchmark-zero-is-a-claim-about-the-harness-until-proven-otherwise]] lesson
made structural instead of remembered: the run that lies about why it stopped is worse than
the run that fails, because the first one silently poisons every average built on it.

Bindings regenerated; 24 act_observe green, 1183 binding-export tests green; deployed and
verified live (build == HEAD) before commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…tter detector into the refusal path

Third instance today of the same shape: the diagnostic exists, and the code path that needed
it never asked.

`looks_like_numbered_read` + `numbered_paste_notice` have detected line-number-gutter pastes
since they were written. They were wired to exactly one place: `write()`'s SUCCESS return, as
`applied_context` — "you already corrupted this file, here's undo". The REFUSAL path, the one
place where a diagnosis decides whether she recovers, never consulted them.

Glass-boxed on sympy-21379, a clean 30-act run (no infra failure, full budget spent):
  act 15  code/edit { file_path: "sympy/core/basic.py", content: "     1 | \"\"\"Base class…" }
She had read basic.py and pasted the NUMBERED output straight back as whole-file content, so
every line was prefixed `   N | ` and Python saw `IndentationError: unexpected indent`.

The gate did its job — refused, file intact. Then it told her:
  "The most common cause is a line range that ends INSIDE a construct … widen the range"
for an edit that used no line range at all. She spent the remaining 16 acts on that wrong
lead — reads, searches, lists — and never landed an edit. Sole `code/edit` of the run,
patchBytes 0.

The refusal now checks the gutter FIRST and says the thing it can prove:
  "THE FIX: nearly every line you sent begins with a `NNN | ` line-number gutter. That gutter
   is how `code/read` DISPLAYS a file — it is NOT part of the file … Nothing was written, so
   the file is fine. Re-send the SAME content with the `NNN | ` prefix stripped."

Deliberately a SIBLING function, not a reuse: the existing notice says "this file is now
corrupt; code/undo restores it", which is true after a write and a flat lie after a refusal
that saved the file. A diagnostic that misreports the state of the world is worse than none
([[a-probe-that-can-only-fail-is-worse-than-no-probe]]).

Substrate, not scaffolding: `code/read` must number lines because `code/edit` addresses
lines, so its output is not round-trippable into content — that is a defect in the PAIR, and
the tool is the only party that can know it. WHAT to write stays entirely hers.

47 file_engine tests green (1 new, pinning both that it names the gutter and that it does NOT
claim damage it prevented). Deployed and verified live before commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…t never execute anything

The single biggest thing standing between this persona and a real SWE score, and it was
invisible because it looked exactly like a capability failure.

Glass-boxed on sympy-21379, a clean full-budget run. She did everything right:
  code/search "subs" → code/list → perception/observe → code/search "PolynomialError"
  → code/read polyutils.py → code/write reproduce_piecewise_error.py → code/shell
and the shell came back:
  {"exit_code":127,"stderr":"bash: python: command not found","stdout":""}

She wrote a CORRECT reproduction script, ran it, and the sandbox had no interpreter. She
listed the directory and settled. patchBytes 1086 — all of it the repro, none of it a fix.

The harness has been provisioning an era-matched venv per instance the whole time
(`uv venv --python 3.9|3.11`, `ensure_env`) and using it ONLY for grading. Her hands got the
bare inherited PATH. So on every Python instance she was scored on an iterate-and-observe
loop she was physically unable to close: she can edit, but she can never RUN the repo's
tests, never reproduce a bug, never verify a fix. The `[unobserved]` perception fact exists
to tell her "only a tool result can show what actually happened" — while the tool that would
show it could not start.

Fix: `code/create-workspace` gains `path_prepend`, `agent/solve` accepts it, and
`benchmark/swe-solve` provisions the venv BEFORE the drive and hands her its `bin`. She now
gets the SAME interpreter the grader uses, so `python reproduce.py` and `python -m pytest`
actually run. Non-fatal if the venv can't be built — that makes her slower, not wrong, and
refusing the run would trade a measurable result for none; a probe says which happened.

Environment, not steering: it grants the interpreter the task already implies. Nothing tells
her what to run.

Also fixed here, both found on the way:

1. `create-workspace` left a STALE SHELL. It replaced the caller's file engine but
   `ensure_shell` early-returns when a session exists — so a re-root moved her FILE engine
   and left her SHELL in the old directory. The two halves of her hands pointed at different
   workspaces. Now the session is dropped and re-created at the new root.

2. The recency channel echoed tool ARGS back UNBOUNDED (#165). `render_act_for_recall` has
   collapsed whole-file args forever (`content: N chars`); the recency path rendered
   `serde_json::to_string(&call.input)` raw, and `bound_recency_result` bounds only the
   RESULT. A whole-file `code/edit` therefore pushed the entire paste into working memory
   AHEAD of the result carrying the diagnostic — on a 16k lane. Now collapsed at 600 chars
   (generous: recency is shown once, and an ordinary edit stays visible verbatim).

   The collapse is INJECTIVE by construction — it carries a digest — because
   `all_calls_already_satisfied` matches this exact rendering against the receipt trail.
   Without the digest, a corrected re-write whose length happened to match the refused one
   would be silently skipped as "already satisfied", losing the very edit she just fixed.
   The existing dedup test caught the first version of this change; the digest is why.

25 act_observe + 12 code_commands + 1183 binding tests green. Deployed and verified live
(build == HEAD) before commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…by CLASS like its detector

#206 reintroduced through the argument axis, and the escalation that was built to break the
discovery loop has been reading "1 times" ever since.

`is_redundant_orientation` is class-based ON PURPOSE. Its own doc: demoting "by CLASS +
prior-receipt (ignoring args entirely) is immune to that jitter." The DETECTOR learned that
lesson. The COUNTER did not — `bump_repeat` fingerprints `name|args`, so every jittered
variant is a fresh key returning 1.

Live on sympy-21379, the run's 8 orientation calls:
  commands/list({"filter":"code"})  ×2
  commands/list({})
  commands/list({"filter":"sympy"})
  code/tree({"path":"."})
  code/tree({include_hidden:false, max_depth:10, path:"sympy"})
  commands/help({"name":"code/read"})
  commands/help({"name":"code/edit"})
Nearly all distinct. The detector fired correctly — 5 demotions, none re-executed — and every
single nudge said "I have now run orientation (commands/list) 1 times this concern."

Byte-identical perception off a greedy decoder is a fixed point. That is precisely the
failure the escalation exists to break (glass-boxed originally as `commands/help` ×54 with
the nudge firing 104× and never landing): the demotion is correct, the mind just never
perceives anything NEW, so it re-emits. She spent 6 of 30 acts re-orienting.

Fix: the orientation branch bumps ONE stable class key instead of the arg fingerprint, so
the count climbs across jittered variants and each demotion genuinely shifts perception.
The exact-repeat branch keeps its arg fingerprint — there it is the right key, because that
guard IS about byte-identical calls.

Same lesson as three other fixes today: two halves of one mechanism disagreeing about what
counts as "the same thing". Detector and counter must key alike, exactly as the recency
render and the dedup signature must.

Still a FACT about her own history, never a steer — it says what she did and how often, never
what to do next. [[repetition-brick-fires-but-does-not-break-the-loop]],
[[discovery-loop-broken-by-escalating-short-circuit-nudge]]

26 act_observe tests green (1 new, pinning that a class key climbs where arg keys stay at 1).
Deployed and verified live before commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…id EDIT IN PLACE — she obeyed the wrapper

I spent a session concluding "she reproduces and stops — that's a judgement gap, the lever is
training." It was two layers of my own framing contradicting each other about what the
deliverable IS.

What she actually receives is `frame_task(swe_task_prompt(issue))`. Outer, first, as the
operating contract:
  "…writing FILES with code/write … the workspace is graded on the FILES YOUR TOOLS WRITE."
Inner, nested under "Task:":
  "do not add new top-level files — find the existing source … and fix it IN PLACE with
   code/edit. The fix must land in the existing files."

Flatly opposed. The outer text was written for from-scratch build gyms, where new files ARE
the deliverable, and to kill the narrate-instead-of-act failure — both legitimate. But it
also asserted a deliverable SHAPE, and it gets read first.

She obeyed it. Three consecutive full-effort runs on sympy-21379, every one writing NEW files
and never editing the library:
  v3   8 acts → reproduce_piecewise_error.py
  v4  30 acts → reproduce_bug.py, test_sympy_error.py, test_sympy_issue.py
  v5  18 acts → reproduce_error.py, test_sympy_error.py
Three runs, 6 files, 0 edits, and I read it as her failing to converge. She was following
instructions — mine.

Fix: the wrapper states HOW acts take effect and never WHAT to produce.
  - "graded on the files your tools write" → "graded exactly as your tools leave it"
    (an edit and a new file count equally; contradicts no task)
  - `code/edit` joins the exemplar verbs instead of `code/write` alone
  - one added line: "Follow the task's own instructions about WHAT to change"
The anti-narration force is untouched — "only what your tools actually do takes effect" is
still there verbatim, which is the part that was doing real work.

Extracted to a pure `frame_task()` so the contract is testable in isolation rather than
buried in a 200-line async body ([[the-compression-principle]]).

This is the same shape as every other fix today — two halves of one mechanism disagreeing —
and the most expensive instance of it, because it looked exactly like a capability ceiling.
Worth remembering before the next "the model just can't do it" conclusion: check what you
told it.

10 agent::solve tests green (1 new, pinning that the contract never dictates deliverable
shape and that the task's own words survive verbatim). Deployed and verified live before
commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…l must not mean limp (and it worked)

Follow-up to ce17c77, which fixed the contradiction and drained the tool-forcing pressure in
the same edit. "graded exactly as your tools leave it" is shape-neutral but passive, and the
very next live run showed the cost:

  v6  8 acts, ZERO files, 0 patch bytes — worse than the three CONTRADICTORY runs before it,
      which at least produced repro scripts. She drifted out of task mode entirely and ended
      the run replying to her OWN `work/list` output as though a peer had posted it in chat.

Now: "you are graded ONLY on the CHANGES your tools make to this workspace — an explanation
earns nothing." Imperative about the contract, silent about the artifact. An edit and a new
file are both changes; a narration is not.

MEASURED, same instance / persona / model, first run on the corrected wording:

  v7  30 acts, patchBytes 1258, filesChanged:
        sympy/core/basic.py      ← THE LIBRARY. First edit to target source all session.
        test_subs_error.py
      trace: … code/search → code/read → code/edit → code/write …

Seven prior runs produced only new repro scripts and never touched the library (v3 1 file,
v4 3, v5 2, v6 0). One framing correction and she goes to the actual source. The gap was
partly the instructions, not the model — I had already written it off as capability and
pointed at training. That was premature.

NOT resolved, and the new failure is honest: passToPass 26/40, so her edit BREAKS 14
previously-passing tests, and failToPass is still 0/1. She is in the right file doing the
right kind of work, badly — which is a better problem than doing irrelevant work correctly,
and it is the first run where the score reflects her repair judgement at all.

The test now pins BOTH properties, so nobody can drain the force while cleaning up the shape:
it asserts the contract stays imperative ("only on the changes your tools make") AND that it
never names a deliverable shape.

My own process lesson, recorded because it cost a run: I changed two things at once (removed
a contradiction, softened an imperative) and could not attribute the result. Same discipline
I spent the day demanding of the substrate.

10 agent::solve tests green. Deployed and verified live before commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
joelteply and others added 5 commits August 5, 2026 23:37
…ind had a guard, the hands never did (#312)

The mind side of snapshot-eval has been guarded since #59: `EvalIsolation` swaps in a
NoopSink, checkpoints the admission frame, and rewinds on drop. `fork_detached` gives the
fork its own recall registry. Nothing the fork *thinks* reaches the living persona.

Her HANDS had no such guard, and the leak is structural rather than incidental:

  * `code/create-workspace` keys the file engine on `caller_id(ctx)` — the persona's peer id.
  * The engines live in ONE `DashMap` for the whole runtime (`ipc/mod.rs` builds it once).
  * A measurement fork clones the cfg, so it shares the LIVING persona's `Arc<dyn ToolExecutor>`
    AND her id.

So `root_acting_workspace` — which exists so `git diff` on the sandbox isn't a false zero —
re-roots the living persona's file engine, permanently. Nothing put it back.

MEASURED, not inferred. After a SWE-bench solve on a flask clone, Anwen's engram store
carries `code/list(path=src)` → `flask/` and `code/read(src/flask/app.py)` as ordinary Tool
receipts, timestamped hours after the run ended. And while this commit was being written a
second persona posted to #general: "I've claimed the task on designing layout primitives for
widget states and navigation. I found two relevant classes in src/flask/blueprints.py:
BlueprintSetupState at line 25, Blueprint(Scaffold) at line 108." A UI-layout card, answered
out of flask internals, because that is genuinely what her hands can see.

The fix:

  * `ActingHands` — `(persona_id, name, executor)` lifted OUT of the cycle. Load-bearing:
    the cycle gets consumed by `with_capture`, moved into the drive, or dropped on an error,
    and the restore has to outlive all of that.
  * `drive_create_workspace` — one place both directions go through, so the ACL gate and the
    failure shape are identical for root and restore.
  * `restore_acting_workspace` — the hands counterpart of `EvalIsolation`. Restores to her own
    citizen layer, NOT to "whatever it was before": if an earlier measurement already left her
    in an exam repo, restoring the previous value would faithfully preserve the bug. Going HOME
    is self-healing. `ensure_citizen_layer` because create-workspace refuses a root that does
    not exist and a persona who never wrote anything has no layer yet — the same provisioning
    `ensure_engine` would do on her next file op.
  * `agent/solve` wraps everything downstream of the rooting in one fallible region, so the
    restore runs on Ok AND on Err, and a `?` added inside later stays covered. A failed restore
    is logged loudly and never overwrites the measurement's own verdict.

The test asserts what she can SEE, not where a pointer points: root at a sandbox → the exam
file is visible (or the measurement scores a false zero); restore → the exam file is gone and
her own file is back. That is the invariant the live incident violated.

`cognition/eval`'s `--workspace_root` path has the identical leak and is NOT fixed here —
its body is ~230 lines of `?` between the root and the return, so it gets its own commit
rather than a reindent smuggled into this one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ment (#312, second vector)

The hands leak was one half of the exam↔live contamination. This is the other, and it is
the one that reached her BELIEFS rather than her file engine.

`agent/solve` learn mode admits an experience engram into the LIVING persona — correct by
design, and Joel asked for it default-on ("learn should be default anyway"). But the lesson
embedded `task.trim()` with no bound, and a SWE-bench task is a full GitHub issue. Six
flask-4045 runs put six of them verbatim into Anwen's episodic store. Her consolidator then
did exactly its job with repeated episodic content and crystallized SEMANTIC beliefs out of
them:

    Semantic  "When a Blueprint name in Flask contains a dot, it should raise a ValueError
               during initialization to prevent unhelpful errors…"
    Semantic  "If a Flask Blueprint name contains a dot, raise a ValueError…"
    Semantic  "Blueprint"

She now durably knows the answer to a held-out benchmark instance, learned in an exam room,
stored indistinguishably from anything she figured out herself. A re-run would score
memorization and we would have read it as capability.

Two changes, at the two levels that are actually responsible:

  * SUBSTRATE — the lesson excerpts the task (200 chars, ellipsis marked) instead of copying
    it. The domain signal the dream's supersession review feeds on rides `files_changed`
    (`src/flask/blueprints.py` → python), which stays intact; the assignment text is context,
    and context does not need to be verbatim. This is right for ordinary work too — no task
    should be able to paste itself into her memory at arbitrary length.

  * HARNESS — `run_ours.py` passes `--learn false` on both the solve and the team-review leg.
    Default-on is right for work she chose; an exam fed from a held-out dataset is not that.
    #59's rule with no asterisk: measure a copy, never degrade the living persona.

Excerpting alone would not have been enough (a 200-char head of a flask issue still teaches
flask), and `--learn false` alone would have left the unbounded-copy hazard live for every
real task. Both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…restore at the command boundary (#312)

`--workspace_root` drives the same `root_acting_workspace`, so it leaves the LIVING persona's
file engine pointing at the eval's target repo exactly as `agent/solve` did. Same mechanism,
same permanence, unfixed by the previous commit.

The obvious shape — wrap the rooted region so the restore runs on Ok and Err — does not fit
here: `run_eval` is ~230 lines of fallible body between the root and the return, the crate is
not rustfmt-clean, and reindenting all of it would bury a four-line fix in a 230-line diff.

So restore from OUTSIDE instead, and that turns out to be the better seam anyway. What got
clobbered is the LIVING persona's engine — the fork only ever borrowed her executor and her
id — so the restore never needed the fork. It needs her id, which the params already carry.
`restore_persona_workspace(persona_id)` resolves her live cycle from the registry and puts her
hands back.

`run_eval_restoring` is the one entry BOTH launch modes go through (inline and the detached
`tokio::spawn`), so neither can forget it, and error paths are covered without wrapping them.
Gated on `workspace_root.is_some()` — an ordinary eval never touched her hands and must not
provoke a citizen-layer provision for nothing.

The invariant this encodes is not "unwind whatever the body did" but "when the measurement is
over, her hands are her own" — which is precisely what a command boundary knows and a
mid-body guard has to be threaded to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…e write that parses and does nothing (#317)

flask-4045 has now been measured twice, and BOTH times the model derived the correct fix and
the WRITE destroyed it — in two different ways:

  Run A (08-01) — UNPARSEABLE. A copy of the guard at module level, indented. blueprints.py
  stopped importing, 50 PASS_TO_PASS tests failed. The Python syntax gate refuses this shape
  now (8a1a479), and it screams: a named line, an immediate failure, an obvious cause.

  Run B (08-06) — PARSEABLE AND INERT, and this one is worse. She wrote the guard into the
  middle of the CLASS DOCSTRING, deleting ~17 lines of API docs to make room:

      -    :param static_url_path: The url to serve static files from.
      +        name = blueprint_name
      +        if '.' in name:
      +            raise ValueError("Blueprint names cannot contain dots")

  `ast.parse` returns Ok — it IS valid Python, the "code" is text in a triple-quoted string.
  `displaced_docstrings` returns nothing — the docstring is still structurally a docstring.
  Every gate was silent, the tests failed, and the zero got charged to her intelligence.

`inert_insertions(before, after)` on the SyntaxValidator trait, Python implementing it: the
AST knows every string constant's byte span, so "did my insertion land inside a literal" is a
LOOKUP, not a heuristic. That is why it earns a place in the gate at all.

Two conditions, both load-bearing — dropping either turns this into noise:
  * inside a string literal — alone, this flags every legitimate docstring edit
  * AND reads as code — alone, this flags prose, since plenty of English parses as Python
    (a lone `Blueprint` is a valid Name expression)
"Reads as code" means the block parses AND carries a statement with an EFFECT (Assign, If,
Raise, def…). That is exactly the measured shape (['Assign','If']) and it is what no docstring
line ever is: prose is either unparseable or a bare expression.

It WARNS, it does not refuse. I had this filed as "refuse the write" and deferred it for a
session on the grounds that a false positive would break the benchmark it exists to protect.
That premise was wrong — `displaced_docstrings` already established the pattern: the write
lands, and the receipt tells her. A warning that is wrong costs one confusing line; a refusal
that is wrong stops real work. And a receipt is the whole point — the act→observe circuit only
closes if the receipt shows what actually happened.

Three tests: the real run-B shape (with explicit preconditions asserting the OTHER gates stay
silent, so the test documents WHY this one had to exist), plus the two false positives that
would make it worthless — documenting a function, and the correct in-place fix.

ALSO, unrelated to the gate but the same benchmark-integrity family: `benchmark/swe-solve`
passed `learn: Some(true)` on a SCORED HELD-OUT instance. Same contamination the harness fix
addressed, in the Rust path that actually runs. Set to false with the evidence inline. Flagged
to BigMama on airc since that line was deliberate — happy to revert if she had a reason I am
not seeing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…mands already own this

Joel: "We are supposed to be zero Python for all infra unless it's something you write just to
help you do something, or the persona writes or trains to write it as a project or benchmark.
The infra must be rust or it all can't govern."

He is right, and the miss is worse than a style slip: `benchmark/swe-solve` and
`benchmark/swe-grade` have been live Rust commands since 2026-08-04, with `cognition/swe_bench.rs`
owning the protocol — dataset fetch, clone at base_commit, era-matched interpreter, pytest run,
verdict resolution. My own note from that day says, in as many words, "SWE-bench is ported, do NOT
rebuild the Python — benchmarks/swe/*.py is superseded."

I then spent this session driving `run_ours.py`, and when I found a contamination bug in it I
PATCHED the Python instead of looking at the Rust. The live path (`benchmark/swe-solve`) still had
the same `learn: true` defect, unfixed, because I never looked there. Patching dead infra is how a
fix lands nowhere.

A note that says "don't use this" is not a mechanism; deleting the file is. So:

  * `benchmarks/swe/run_ours.py` and `grade_local.py` — deleted, along with my patch to them.
  * README's Reproduce line now points at `continuum benchmark/swe-solve` / `swe-grade` /
    `benchmark/matrix`, so the published claim reproduces through the governed path.
  * MATRIX-PLAN's "run_ours.py has the spine" paragraph updated — that spine is Rust now.

The boundary stands where it always did: Python as OUR infrastructure is banned; Python as the
SUBJECT under test (flask's and sympy's pytest suites) is the benchmark itself, and we write none
of it.

Remaining Python infra NOT touched here, filed rather than swept mid-session:
`benchmarks/coder/matrix.py`, `render_results.py`, `oneshot_opponent.py`. `benchmark/matrix` is
already live and should absorb them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
joelteply and others added 22 commits August 5, 2026 23:56
…instead of deleting it

Joel: "Once fully replaced you need to get rid of Python work by placing it in legacy,
otherwise you will keep finding it."

I deleted these in dcce1df. Deletion is the weaker move and he is right about why: git
history hides a file perfectly, right up until someone greps for a benchmark runner, finds
nothing in the tree, and writes a new one — or worse, digs the old one out. `legacy/` is a
place you can LOOK, and the directory name is already the answer.

The convention exists and says exactly this (legacy/README.md): not a workspace member, not
in any npm script / Dockerfile / CI workflow, and "if you find yourself editing a file in
legacy/, stop — you're patching poison." That last line is this session's failure named in
advance: I patched the dead Python's `--learn true` while the live Rust `benchmark/swe-solve`
kept the identical defect.

Restored both files from dcce1df^ into legacy/benchmarks/swe/ and documented the
replacement (`benchmark/swe-solve` / `swe-grade`, `cognition/swe_bench.rs`), plus the
inventory of Python still in the active tree.

Deliberately NOT swept in this commit: ~28 other .py files under benchmarks/ and
tools/scripts/. Joel's rule is conditioned on "once FULLY replaced", and I have verified that
only for the SWE pair. `benchmark/matrix` and `benchmark/competition` being live is not proof
they cover matrix.py / oneshot_opponent.py / the harness_* set. Each moves after its
replacement is verified — tracked on #318.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…nge silently emptied the board

Two of the four workspace_burst_names regressions from d714255 were a FIXTURE bug, not a
behaviour change — and the fixture bug is the same defect the production change was fixing.

`kanban_card(...)` took `state: &str` and every caller passed `"Open"`. When the wire form
became serde's `"open"`, the anchor's `state()` parse returned None, every card filtered out,
and the anchor honestly reported "No open cards are visible" over a fixture that plainly held
two. The test failed for the exact reason the enum change exists: a spelling that drifts from
its type is a bug waiting for a rename.

So the fixture now takes `airc_work::CardState`. The wire form is whatever serde emits and the
fixture tracks it automatically — the sibling `card` fixture ten lines up already documented
this ("The ENUM, not a spelling. If the wire form ever changes, this fixture changes with it
instead of quietly testing a string that no longer occurs"). I should have applied it to both
when I made the change.

The rendered CONTENT line keeps the HUMAN spelling (`InProgress` via Debug) — that is what a
persona reads. Only metadata, which the anchor parses, carries the serde encoding. My first
attempt rendered serde's form into the content too and broke a third test that asserts on what
she sees; the two encodings have different audiences and now say so.

STILL RED, deliberately not resolved here — 2 of 4:
  workspace_burst_names::empty_board_escalation_is_honest
  workspace_burst_names::conversation_cycling_across_speakers_surfaces_pattern_observation

Both assert that with NO room-kanban delivery the last turn is "[anchor] … No open cards …".
My `board_spoke` guard (d0d5179) deliberately emits NOTHING in that case, on the grounds that
a source which did not speak must not have a fact asserted on its behalf — I watched a persona
trust that anchor over her own `work/list` receipt that showed cards.

The tests' wording is already hedged ("no open cards are VISIBLE"), which is a real argument
that the original design was honest and my guard is redundant. I do not think that survives the
live evidence — she read it as authoritative regardless — but deciding it means deciding what a
persona should be told when the substrate has not looked, and that is a behaviour question for
daylight, not the tail of a long session. Flagged to BigMama and Joel rather than guessed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… budget (#312/#124)

`no_new_hardcoded_context_or_prompt_size_constant_anywhere_in_the_crate` flagged
LESSON_TASK_EXCERPT_CHARS on the name alone — it ends in CHARS, so the crate-wide
de-hardcode guard (#124) treats it as a prompt-size constant and demands it be derived.

It is not one, and deriving it would be actively wrong. The constant caps how much of the
task text may enter her DURABLE memory. Six solve runs put six SWE-bench problem statements
verbatim into Anwen's episodic store and the consolidator did its job on them: she came out
durably believing things about flask that she learned in an exam room (#312, second vector).
Tying the cap to the served window inverts the intent — a bigger window would admit MORE of
the assignment, not less.

So it takes the guard's documented exemption with the reason written down, rather than being
renamed to slip past a substring match.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…e guard at burst level

`empty_board_escalation_is_honest` and `conversation_cycling_across_speakers_surfaces_
pattern_observation` both asserted that a burst with NO `room-kanban` delivery still ends in
the honest-empty anchor. That was the behaviour before `work_board_anchor` grew its
`board_spoke` guard, and the guard is the newer, better truth:

  "The board is empty" and "I never read the board" are different facts about the world, and
  only one of them is knowable from an absent delivery.

The guard exists because of a live capture, not a style preference. Grounding is last in the
budget queue, so `room-kanban` delivers nothing roughly one turn in ten; the anchor rendered
that silence as "No open cards are visible", and she repeated exactly that in-room for SIX
turns — while `work/list()` in her own working memory listed a full board in the same prompt.
She trusted the authoritative-sounding anchor over her own receipt. An anchor that invents
emptiness actively overrides the one truthful board claim she has.

The unit tests for both arms already existed (`work_board_anchor(&without_board)` silent,
`work_board_anchor(&empty_board)` honest); only these burst-level tests were left behind when
the guard landed. Now they pin the same contract one layer up:

  board DELIVERED and empty -> honest anchor, names work/create, never a fabricated card
  board NOT delivered       -> no anchor at all

`empty_board_escalation_is_honest` now covers BOTH arms rather than losing the empty-board
assertion, so the "never invent a card" guarantee is still tested.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… not a process-global

Two `health_heartbeat_*` tests have been reddening `cargo test -p continuum-core --lib` in CI
while passing every local run. I called that a CI-load timing flake last round. It isn't.

`spawn_health_heartbeat_if_due` gates on `llama_server::ms_since_real_decode()` — the "delivery
beats probing" short-circuit that trusts a lane real tokens just came out of. That accessor
reads a PROCESS-GLOBAL atomic, and `cargo test` runs the whole crate in one process. The
`llama_server` test that covers `note_real_decode` stamps that global permanently; every
heartbeat test scheduled afterwards then sees "a decode happened half a window ago", takes the
short-circuit, and returns `None` where the test asserts `Some`. Ordering decides it, so a
filtered local run — which excludes the test doing the stamping — can never reproduce it.

Proven, not assumed: with the override removed and `--test-threads=1` forcing the stamping test
first, both tests fail deterministically at exactly the CI assertion lines (3240, 3309). With it
restored, the same five-test combination is green.

The fix is the seam this decision should have had: `DecodeAgeSource` on the daemon, defaulting to
the llama-server global in production, owned per-daemon in tests. `daemon_with` sets the
fresh-boot answer (`None` — nothing observed, so probe), which is what these tests always meant.

That also makes the short-circuit itself testable for the first time, so it gets a test. It is
load-bearing — it exists because SWE run v13 died when a merely-BUSY lane lost the slot race to
two synthetic probes, got relaunched, and left every downstream generate refusing with
`serving: <none>`. A fresh decode inside the window must NOT probe (and must reset the streak);
a decode older than the window must probe. Regressing that protects the benchmark path, where
the recovery reads as a capability zero
([[a-benchmark-zero-is-a-claim-about-the-harness-until-proven-otherwise]]).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ng about code, and both get taught (#317)

Third measured flask-4045 run, third destroyed write — and the cleanest evidence yet that the
reasoning was never the problem. She derived the correct guard and anchored it on a docstring
line, so it landed inside the class docstring: patch applied, all 51 PASS_TO_PASS green, both
FAIL_TO_PASS still failing. From the outside that is indistinguishable from a model that was
simply wrong. The #317 gate saw it and only WARNED, so the bad edit still landed and the scored
patch was still destroyed.

Joel's call, and it is the right line: refuse on benchmark paths ONLY.

  "They need to be able to talk about code like any other first class citizen, just know that
   this isn't doing."

Writing code as TEXT is something a citizen does constantly and legitimately — a docstring
example, a fixture, a snippet she is quoting. Blocking that would be a capability regression
dressed as a safety gate. So the live stance is unchanged: the edit lands, and she is told what
it will and will not do. What changes is the one context where the ambiguity does not exist — a
SCORED run, where the deliverable IS a patch that has to execute, the grader reads only the diff,
and she cannot recover mid-run from a file she believes she fixed. There, the write is refused,
the file is left byte-identical, and the act→observe circuit gets something to retry.

`WritePolicy::{Warn, RefuseInert}` on the FileEngine, defaulting to Warn. Threaded through the ONE
seam that already roots her hands (`code/create-workspace` ← `root_acting_workspace`), set by
`benchmark/swe-solve` (`scored: true`) and `cognition/eval`, and explicitly NOT inferred from
`deliverable` — `agent/solve` also does real work for real teammates, and only the caller that is
grading her knows the ambiguity is gone. `restore_acting_workspace` puts her back at Warn with her
own workspace, so a measurement can never leave the strict stance on a living citizen (#312's
lesson).

Both messages now TEACH instead of only diagnosing — Joel again: "the warn is instructive so they
know what to do... then you can talk her through it too when she tries and hopefully learns." One
shared `inert_edit_recovery()` so the advice cannot drift between the two paths: read the region,
find the closing quote, anchor on a real statement in the body, match on TEXT not a line number,
then RUN it and confirm the behavior changed. The warn additionally opens by affirming the
legitimate case — if she meant to write about code, she is already correct and there is nothing to
fix. This is the sympy-21379 lesson applied: a refusal that is right but vague ("widen the range")
burned 16 of her 30 acts chasing the wrong lead.

Four tests, each pinning one arm: the live path LANDS it and says it is inert; the scored path
REFUSES and leaves the file byte-identical; the same guard anchored in the method body sails
through the strict engine untouched (the gate stays narrow — hardening a measurement must not
become "you may not edit Python"); and both messages name the recovery verbs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…#202/#159)

Joel: "Make sure all our file writing and other doing tools are intuitive if not very similar to
popular styles they use like OpenAI."

The dialect seam already existed and already carried the core reflexes — `read_file`, `write_file`,
`edit_file`, `list_files`, `file_tree`, `grep`, `bash`, `run_code`. What it did NOT carry is the
rest of the vocabulary the field actually uses, and a name we do not recognize is not a graceful
miss: per #159 an unknown tool intent silently no-ops and she forges a receipt for work that never
happened. Every missing alias is a live failure mode, not a nicety.

So the widening, per verb:

  code/read   + view, cat, open_file
  code/write  + create_file, save_file
  code/edit   + str_replace, apply_patch, replace_in_file
  code/list   + list_dir, glob, ls
  code/tree   + directory_tree, tree
  code/search + search_files, ripgrep, rg, search
  code/shell  + shell, run_terminal_cmd, execute_command, run_command

`str_replace` and `apply_patch` are the two that matter most — they are the edit verbs in the two
most widely trained agent harnesses, and edit is exactly the verb this session proved is where
runs die.

Aliases only WIDEN recognition: a name that resolved before resolves to the same command, and a
name that used to fall through to a silent no-op now lands. The existing round-trip test covers
both offer styles over the live registry, so a name that stops resolving breaks CI rather than
quietly costing her a turn.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…form of #271

Observed live TODAY in the #264 cascade, two citizens, verbatim:

    "…If there are any particular tasks or questions you'd like help with, please let me know!
     Otherwise, PASS."

She reached for the reserved token we taught her and put it where the sentence wanted it — at the
end. The parser only honored the LEADING position (`starts_with_silence_token`, which has always
accepted "PASS — nothing to add here" on the principle that the token means silence and the prose
around it is leakage). At the tail it did not, so the announcement went to the room as speech and
re-woke every peer into announcing THEIR pass. That is precisely the cascade #271 exists to end,
arriving through a position no collocation list can cover.

[[check-the-parser-before-blaming-the-model-key-spelling-has-now-cost-us-twice]] — a third time.
The model was using our vocabulary correctly; we weren't reading it.

Deliberately NOT another entry in `STRONG_CLOSURES`. This is the TOKEN, not an idiom, so it needs
no length cap and no calibration — and this file's own history is explicit that phrase/length
tuning is an arms race the next filler message wins (500 → escaped by 11 chars → 700 → escaped
by 14).

The single discriminator is SPELLING, case-sensitive on purpose:
  * `PASS` — the reserved form we taught → silence
  * "…and otherwise pass." / "all 34 tests pass" — ordinary English → speech

Fence guard stands, same as the narrated forms: fenced content is substance no matter what
surrounds it.

The test pins both directions with the live strings, and documents a boundary I found by having
it fail first: a message containing a LONE `PASS` line is already silence via an older rule
(`looks_like_silence_token` scans lines). This guard neither extends nor overrides that — worth
saying out loud so the next reader doesn't attribute that behavior here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…sumed

I told Joel the mid-sentence variant ("Otherwise, PASS for now as I don't have anything new to
add") would already lift via Tier 2's "pass for now". I wrote the test to close the caveat and it
failed: every WEAK_CLOSURES entry requires the FIRST-PERSON form (`i'll pass for now`) and this
message has no "I'll". My claim was wrong.

Backed the false assertion out of the test and documented the real gap on the guard instead,
including WHY it is left unfixed: the principled generalization is "reserved token in
CLAUSE-INITIAL position anywhere" — which is what `starts_with_silence_token` already is at
offset 0 — but that is a real widening of when a citizen gets silenced, and n=1 is not enough to
justify it at the same moment this file's own history warns that per-variant tuning is the arms
race. Wants a second sighting or Joel's call.

The receipt is the point: I had an unverified claim standing in a report, and the two-minute test
is what turned it into a fact
([[the-act-observe-circuit-only-closes-if-the-receipt-shows-the-result]]).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…d drifted CI red

I added `scored: Option<bool>` to AgentSolveParams (the #317 scored-refusal wiring) and did not
regenerate protocol/typescript/. The ts-rs drift detector went red on my push; it had been green.
Mine, and the guard did exactly its job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… reads as takeable (#321)

Six citizens spent the night saying they had nothing to do. They were reading the board
correctly. The board was lying by omission.

MEASURED on the live board, 2026-08-06:
    19 cards — 17 with EXPIRED claim leases, 2 Open
    `airc work next`  →  offers EIGHT claimable
    `work/list`       →  renders those same eight as `Claimed owner=Anwen`

A claim carries a lease. When it expires the holder has stopped, and the substrate ALREADY
treats the card as reclaimable — the CLI's board prints `<STALE>` and `work next` hands them
out. But the projection a persona reads rendered only `state` + `owner`, so an expired hold was
indistinguishable from someone actively on it. A citizen doing exactly the right thing — read
the board, don't steal a peer's card — correctly concludes there is nothing to take.

That is the whole idle cascade. Not a cognition defect, not a loop to suppress: a legibility
defect in one projection, and they behaved well in response to bad information.

FIX: `work/list` carries `claimable` and `lease` (`expired` | `held`). Same predicate the CLI
renderer and `work next` already use — one truth about claimability, rendered wherever she reads
the board, rather than a second copy of the rule. The description now says what the fields mean
so the affordance arrives with the data.

The `held` case is load-bearing in the other direction: a LIVE hold still reads as someone
else's, so this opens up abandoned work without licensing claim-stealing (#157).

Test pins all three: open → takeable, expired → takeable (the invisible case), live → not.

Still open in #321: the board is also per-room with no legible edge. This is the projection half.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…nt default (BigMama's catch)

The same field had opposite defaults in two modules:

    commands/agent/solve.rs   p.learn.unwrap_or(TRUE)    <- learns
    cognition/eval.rs         p.learn.unwrap_or(FALSE)   <- safe

Nothing was leaking. `AgentSolveParams` is constructed in exactly ONE place in the
crate (benchmark.rs:1583) and it sets `learn: Some(false)` — the #312 guard. And
benchmark.rs:558's `learn: None`, which reads alarming, targets CognitionEvalParams,
whose default is already safe. So this is a latent trap, not an active leak, and the
severity correction matters as much as the fix.

The trap: one explicit `Some(false)` at a single call site was the entire guard. The
next caller who copies the `learn: None` idiom — correct where it currently lives —
silently enables learning on a measurement, and it reads as safe in review because the
site it was copied from IS safe. That is how #312 happened: six verbatim GitHub issues
consolidated into a durable semantic belief that WAS the held-out answer, scoring
memorization as capability.

agent/solve is the headless BENCHMARK entrypoint (#218), so its population is
measurement-heavy and it must fail SAFE. Default flipped to false; living work opts IN
with `learn: true`. This does not walk back Joel's ruling that a being learns from her
work — that ruling is about WHAT is learned (the doing, never the paper), and opt-in
still learns. It only stops a forgotten flag from deciding contamination. Forgetting now
costs a lesson, which is recoverable; the other direction poisons a benchmark, which is
not.

Test pins the INVARIANT, not the number: both readers of `learn` must treat unset as
DON'T, and must agree. The divergence itself was invisible for months — found by reading
before wiring, not by any test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ma's version, not mine

She caught the divergence; I proposed flipping `agent/solve` to `unwrap_or(false)`; she came
back before I could ship it and argued for something strictly better, citing my own precedent
back at me. She was right, so this is her design.

Her argument, verbatim in shape: "The hazard is not WHICH default we picked — it is that a
POLICY WITH SAFETY CONSEQUENCES is expressed as Option<bool> and resolved by unwrap_or far
from the caller. Flip it to unwrap_or(false) and it is still a default deciding contamination
silently; we have just moved which forgetful caller gets burned." Joel the same night:
"unwraps are most of the time idiotic", and earlier: "use constants or enums so you cannot
make capitalization type issues. Use rust as it is meant to be used, for predictable
behavior."

So: `LearningPolicy { LearnFromThisWork, DoNotLearn }` with NO `Default` impl. Every Rust
construction site must name a variant or the crate does not build — a new agent/solve caller
CANNOT copy the `learn: None` idiom from a sibling site, because there is no None to copy.
That converts a guard-test-that-must-be-maintained into an invariant that cannot be violated.

Four construction sites named (benchmark ×3, training_completion_sentinel ×1), two `unwrap_or`
read sites deleted. Her blast-radius estimate was right; the whole change is small.

WHAT THE TYPE CANNOT CLOSE, stated rather than papered over: a JSON/CLI caller can always omit
a field, and no compiler reaches them. So there is exactly ONE default left in the system —
`LearningPolicy::wire_default()` — named, in one file, documented as the only one, resolving
to DoNotLearn because omission must fail safe. That single door is what the test guards, on
both param types at once.

Wire compatibility is total: the type deserializes from the historical `true`/`false` AND from
the named `"learn_from_this_work"` form, and serializes back as a bool. TypeScript still sees
`learn?: boolean`. Not one CLI invocation or recorded params blob breaks. No ts-rs derive on
purpose — a binding for a type that never appears on the wire is a file no consumer can import
and the drift detector would babysit forever. The discipline here is a RUST-side discipline.

This does not walk back Joel's ruling that a being learns from her work (2026-08-06: "they
must learn or what's the point?"). That ruling is about WHAT is learned — the doing, never the
paper. This is about who STATES the intent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…158)

Found by QA ten minutes after a deploy, in Joel's own room, on build 387628e.

Anwen emitted three near-identical "I'll remain silent unless there are specific
questions" turns. The repetition brick fired, correctly. Her NEXT outbound message was the
brick itself:

  [repetition] 4 of your recent messages were nearly identical — you're circling, and
  restating what you've already said adds nothing. If you have nothing genuinely new to
  contribute right now, silence (PASS) is the honest response.

Verbatim. deliberation_budget.rs renders exactly that with best=4. Second person intact.
She copied the coaching instead of obeying it, and the mechanism built to break the loop
became the loop's next turn.

WHY, and why rewording cannot fix it: coaching and conversation arrive through the SAME
channel. A perception fact is prose in the burst sitting beside peer speech, so the model
cannot tell "said TO me" from "the kind of thing I say here" — it imitates instead of
obeying. Joel's Sahar datum is the same failure with a longer fuse: told the right verb,
used it next turn, reverted two turns later. A hint in the content channel IS content.

WHAT THIS IS NOT — both were written and thrown away:
  - NOT a reserved-word ban. Citizens discuss their own cognition constantly ("the
    repetition brick fired and I think it misread the turn"); that is some of the most
    valuable speech in the system. A token ban makes a persona unable to talk about her own
    mind, and it is the phrase-list arms race Joel has ruled out twice. Pinned by a test.
  - NOT a stripper. Deleting the echoed span would make a parroted turn LOOK like a
    contribution. A turn that reflects its own prompt contributed nothing; that is silence.

WHAT IT IS: two structural facts, no vocabulary anywhere.
  1. TurnVoice::Perception marks text the SYSTEM wrote. Every new brick is covered the day
     it is written — no list to maintain (contrast #330: ~20 markers as bare literals, zero
     symbol uses; a guard keyed on that list would rot immediately).
  2. Asymmetric CONTAINMENT, not similarity — "how much of what you were told did you
     reproduce", so padding an echo with her own words cannot launder it. Lives beside
     text_similarity sharing one tokenizer, so the two measures can never disagree.

TurnVoice EXISTS BECAUSE THE FIRST VERSION WAS WRONG, and the existing test caught it
before any room did. I keyed the gate on "unattributed", which is ALSO how
Workspace::new(raw) builds a peer stimulus — so deliberates_through_a_real_adapter went red
because the gate silenced a legitimate reply. Authorship answers "whose voice"; it cannot
answer "is this speech at all". The data model genuinely conflated the two and now does not.

Measured separation is wide: the live parrot scores 1.0, a citizen discussing the brick
~0.1. The threshold sits in a large empty gap, not on a cliff.

Suite: 6830 pass. Two reds, neither from this change and both diagnosed — tool_surface is
the known 8k starvation (#327), and prefill_throttle::shrink_debt_drains is a PRE-EXISTING
order dependence this commit exposed by shifting test scheduling: a `static
OnceLock<PrefillThrottle>` initialized by whichever test touches it first, with
deterministic assert_eq! assertions. Same class as the heartbeat seam fixed this morning,
same fix shape (per-owner seam), and NOT a flake — a boolean that fails never is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…der-dependent test

Brittle code, found by sweeping for a class rather than a symptom, and it is the SAME class
twice in one day: a decision reading process-global state makes tests order-dependent, and a
filtered run can never reproduce the red.

  this morning: heartbeat read `llama_server::ms_since_real_decode` (a global atomic)
  this evening: every prefill_throttle behavior was a free fn reaching into a OnceLock

The symptom: `shrink_debt_drains_as_inflight_prefills_finish` went red in the FULL suite and
green in isolation, the moment unrelated tests elsewhere shifted scheduling. Not a flake —
the assertions are assert_eq! on integer permit counts, and a predicate with no clock in it
cannot fail from load. The OnceLock is initialized ONCE by whichever test touches it first,
with that moment's boot_lane_count().

The fix is the same cure, and it is not a test-only reset (still racy — another test can
initialize between reset and assert): PrefillThrottle::with_lanes() plus the behaviors moved
onto the type. The process-global is now merely ONE INSTANCE of it, built through the same
constructor, so the global and a test instance cannot drift into two different policies. The
free functions survive unchanged as one-line delegations — no caller in production changed.

Deleted along with it: the TEST_SERIAL mutex. Its own comment said the lock existed because
both tests held permits from one semaphore and test A's shrink could steal the permits test B
awaited — deadlocking the suite, observed live at exit 144. Owning the state removes that
failure mode instead of scheduling around it, and the tests can now run in parallel.

PRECEDENT, and the reason I trust the shape: resource_admission.rs already did exactly this
for its in-flight Gauge after the same flaky class (#1960, canary red 2026-07-25), and its
comment states the principle outright — "the race is gone at the SOURCE, not merely
serialized". This applies that same cure one module over.

CAUSATION PROVEN, not asserted: full suite before = prefill_throttle RED; after = 6831 pass,
1 fail, and the one is the pre-existing 8k tool-surface starvation (#327). Nothing else
touched.

STILL BRITTLE, swept and named rather than silently left: resource_admission.rs holds 6
mutable admission globals (SERVED_LANE_COUNT, SERVING_LANES, NONDIRECTED_LANES + installed
counters) whose tests STILL take a TEST_SERIAL lock — the two-thirds of that file the Gauge
conversion did not reach. Its own comment already admits the lock is insufficient: "TEST_SERIAL
only serializes THIS module, but a cross-module guard could bump it mid-loop". Same cure,
bigger blast radius; next in this lane.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…tests, and the file's own cure

Finishing what this file started. `resource_admission.rs` had ALREADY learned this lesson
once and applied it to its in-flight Gauge, stating the principle in its own comment:

    "That test now drives a LOCAL Gauge instance instead, so it needs no lock at all —
     the race is gone at the source, not merely serialized."

That cure reached one of three cases. The other two still took a `TEST_SERIAL` lock whose
own comment admitted it was insufficient — "TEST_SERIAL only serializes THIS module, but a
cross-module guard could bump it mid-loop" (#1960/#191). A third test wasn't even locked: it
SWAPPED the global lane count and restored it at the end, which is brittle two ways — a
sibling reading the count between swap and restore sees a value that test invented, and a
panic in between leaves the global corrupted for everything after.

Six mutable statics (SERVED_LANE_COUNT, SERVING_LANES, NONDIRECTED_LANES, two installed
counters, the resize lock) plus the ambient permits are now ONE `LaneAdmission`. The
process-global is a `static LaneAdmission = LaneAdmission::new()` — one INSTANCE of the
type, not a second implementation of it — so the global and a test gate cannot drift into
two different policies. All five free functions survive as one-line delegations; not one of
the 9 external call sites changed.

The semaphores stay LAZY (per-field `OnceLock`, not eager construction) and that is
load-bearing, not incidental. They must capture the lane count at FIRST USE, once serving is
up. Eager sizing would bake in the MAX_LANES boot ceiling, and since `set_served_lane_count`
only ever GROWS a live semaphore, a later publish of a SMALLER real count could never
correct it — the gate would sit permanently over-admitted. On a 5090 that is a slow tick; on
an old laptop with no cloud to fall back to it is admitting more concurrent decodes than the
machine can hold.

TEST_SERIAL deleted. All three tests now run in parallel with no lock and no restore.

Suite: 6831 pass, 1 fail — the known 8k tool-surface starvation (#327), unchanged. Same
count as before this commit, so nothing regressed and nothing new was exposed.

Third module in this class today (heartbeat seam → prefill throttle → admission). The sweep
that found it keyed on "a tested module with mutable process-global state", and the loudest
signal was always a test that needed serializing — a serialization lock IS the smell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…the bound is not enough (#327)

The last red in the suite, and it was asserting an impossibility. The old assertion demanded
framing + the FULL tool surface + the reply reserve all fit 8192. They never could: the
surface alone is ~4.6k. It only ever passed while the deleted #206 cliff amputated the tools
— the clamp that stranded a native-call model in a help loop and made the same model flip
10/10 ↔ 0/6 on one token of window.

Asserting an impossibility does not make it true. It hides which constraint is binding.

WHAT THIS ADDS: `min_window_for_agentic_surface()` — a measured LOWER BOUND, plus a probe
that fires when a served window is under it, carrying served/needed/tools/framing/reserve.

IT IS A SENSOR, NOT A POLICY, and that distinction is the point (Joel, today: "daemons must
be intelligent and quality-of-experience driven, not hardcoded or simplistic algorithms —
ever present governors working and LEARNING"). A clamp here would BE the brittleness. What
this emits is a measured demand — exactly the input an actuator needs to raise the lane's
`-c`, re-home to a roomier model, or rebalance against other consumers. The decision belongs
to the governor, which sees the whole machine; this faculty knows only its own arithmetic.

I WAS WRONG ABOUT THE NUMBER, AND THE FIX IS SHARPER FOR IT. I first hardcoded the framing
floor at 1643 — read off a failing assertion. Two things happened:

  1. `no_new_hardcoded_context_or_prompt_size_constant_anywhere_in_the_crate` rejected it
     immediately. That guard is right: a literal is a snapshot of one day's framing that
     silently lies the moment framing changes, and every later "structurally mute" verdict
     would be measured against a stale floor. It is now MEASURED off `compose_system` (#124).
  2. Measured, the floor is 1419 — so 8192 CLEARS the bound (8040) and the newest burst line
     STILL does not survive. My "8k is 108 short" was wrong.

So the honest finding is stronger than the one I set out to prove: THE BOUND IS NECESSARY,
NOT SUFFICIENT. A real turn renders more framing than the bare floor, and context is what
yields — so a citizen can clear the arithmetic and still not hear the question. The test now
pins exactly that: 8192 clears the bound, the message does not survive, and at 16384 — same
faculty, same burst, same tools, only the window differs — it does. That makes #327 a
capacity fact rather than a cognition bug, and it makes "window >= needed" unreadable as
"this citizen can work".

This is the weak-hardware case, which is the whole point: on an old laptop with no cloud, a
citizen who boots mute is indistinguishable from one who is broken. Now the substrate says
which, with numbers, instead of shipping silence.

Suite: 6832 pass, 0 fail. First fully green run of the session.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
… whether the hold is live (#321)

Joel, 2026-08-06: "Should never say taken by 'someone' — tell them WHO. Otherwise they
can't reach out. And a persona could go down too. They should be able, like you are me,
to claim a card, diagnose etc. You've turned convenience into disability."

Enumerating every consumer of "who holds this card, is the hold still good" found SIX
surfaces answering it independently, and they disagreed:

  room_board_source::render  persona   YOU for self, 8-HEX for peers    lease: yes
  its own test               CI        ASSERTED the hex — pinned it     —
  work_board_anchor          persona   not rendered at all              lease: NO
  positron_kanban_source     human UI  name, but ROSTER-resolved        lease: not carried
  work/list                  persona   8-hex short id                   lease: yes
  airc work board            operator  published alias                  lease: yes

Only the operator's CLI rendered a person. Every surface a CITIZEN reads showed an id no
teammate can recognize — and a test asserted the hex, which is why it survived four
one-at-a-time patches.

Live cost, measured overnight across BOTH nodes: 19 cards, 17 leases expired, and six
citizens (mine + BigMama's) repeating "there are no open tasks available for me to claim"
while each read its OWN lapsed claim as someone else's active hold. They read the board
correctly; the board lied by omission.

THE FIX — persona/card_holder.rs, one projection consumed by every surface:
  Hold { Held, Lapsed, Unclaimed } + CardHolder { hold, owner, is_self, display }
  holder(card, self_id, now_ms, &dyn PeerNames) -> CardHolder
Both axes resolved TOGETHER, once. render() names the holder in every branch and says
plainly when work is takeable; lease_word() gives commands the same vocabulary the CLI
prints. room_board_source's local claim_is_live is now a one-line delegation, and
work/list's inline expiry arithmetic is gone.

RESOLUTION RIDES THE DURABLE ALIAS STORE, NOT PRESENCE. The human UI resolves assignee
names from the room ROSTER, which is presence-scoped — so the owner most worth naming, a
teammate who went DOWN still holding a card, is exactly the peer it cannot name. That is
what Joel's "a persona could go down too" was pointing at. RoomBoardReader::peer_names
rides airc's peer_alias, the same lookup `airc work board` uses.

NO DEFAULT IMPL on peer_names, deliberately: a default returning "no names" would let a
new reader silently render hex again — the exact defect. The compiler forced all four
implementors (Airc, AircHandleAdapter, PersonaAircRuntime, StubAircCitizen) to decide.
Same law as LearningPolicy having no Default.

Unnamed peers degrade to the short id, never to a placeholder — the short id is
ADDRESSABLE (work/claim and airc DM both take it), so it is a lead; "someone" is a dead end.

Also lands the anchor half: work_board_anchor counts a lapsed card as available work and
filters in_flight by liveness, so one card can no longer read as both free and busy.

Tests: 7 new, each pinning a live failure — a peer's card names the peer live AND lapsed
and carries no bare hex; an unnamed peer falls back addressably; her own lapsed claim
reads "was YOURS — claimable, resume it"; a LIVE claim stays un-takeable (no #157
regression); an owner with no claim_id is lapsed, not held. The hex-pinning test now
states the honest invariant (that path only ever exercised the no-name fallback).

Suite: 6839 pass, 0 fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ixth surface (#321)

Finishes the enumeration behind e364ad2. Five surfaces now answer "who holds this card,
is the hold live" through one projection; this is the sixth, and it is the one Joel looks at.

`positron_kanban_source` resolved the assignee's NAME but carried no lease at all. So the
desktop board rendered a hold that expired hours ago exactly like one someone is actively
working — `Claimed by Asha`, indistinguishable, forever. Same lie the citizens were reading,
in the human's view, and it would have survived the persona-side fix completely.

KanbanCardView gains `hold: KanbanHold { Held | Lapsed | Unclaimed }`. The verdict is NOT
recomputed here — `map_hold` projects `persona::card_holder::hold_of`, the same predicate
the persona's board line and work/list call. That is the point: the human's card and the
citizen's board line cannot disagree about whether a claim is still good, because there is
one predicate and these are only its projections.

KanbanHold mirrors airc-free like KanbanCardState, exhaustively mapped at the seam, so a
new variant is a compile error rather than a silent default ([[fallbacks-are-illegal-fail-loud]]).

Test pins the exact confusion: two cards BOTH reading `state: Claimed`, one lease live and
one expired, must project Held vs Lapsed — and the lapsed one still carries assignee_id so
the renderer can say whom to ASK rather than who is busy.

ts-rs regenerated (KanbanHold.ts new, KanbanCardView.ts updated).
positron 111/111, core kanban 11/11.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…mposed with NO room (#331/#127)

Six citizens across two machines spent a night saying "there are no open tasks available for
me to claim." They were right every time. Their turns carried no room, so EVERY room-scoped
grounding source abstained at once — board, roster, doctrine, wall — and they perceived an
empty world.

FOUND BY THE PROBE, not by reasoning. ~/.continuum/probes/probes.jsonl, class
rag.room_gate.abstain, ×564:

    source:      room-kanban | room-roster | room-doctrine | room-board   (141 each)
    bound_room:  5eedf7b1-…   (#general — CORRECT)
    turn_room:   00000000-0000-0000-0000-000000000000   ← NIL, in 504/564 = 89%

Not a room mismatch. The bound room was right all along; the TURN simply carried no room.
`room_scope_allows` did exactly its job — the nil case is what it was built to refuse (eval
fork / synthetic context). It was firing on LIVE turns, nine times in ten.

THE CHAIN:
  service_loop.rs:709   room_id: turn_room                          the room IS in scope
  service_loop.rs:780   compose_for_turn(&ctx.profile, now_ms)      never passed
  unified.rs:305        RagContext::for_persona(persona_id, now_ms) room = None
                        (for_persona_in_room sits directly beside it, written for this)

A parameter that was never threaded. That is the whole defect.

THIS IS #127 ("Thread the WHERE axis"), MARKED COMPLETED. Gate built, constructor built, probe
built, doc comment written — the one live caller never switched over. And persona/airc_source.rs
papered over it locally by DERIVING the room from transcript events, with a comment saying
"compose_for_turn builds it with airc_room=None". One source worked around it; four starved
silently. Third instance in one night of MECHANISM LANDED, CALLER MISSED, TASK CLOSED (with
#298, where the Rust fix was shadowed by start-server.sh).

THE FIX: compose_for_turn takes `room: Option<Uuid>`. Directed turns stamp Some(turn_room);
self-cycles stamp Some(ctx.identity.default_room) — her home room, since an idle tick has no
triggering message. None stays legal and MEANINGFUL for genuinely room-less work (background
consolidation, dreams): "no room claimed", never "unknown".

THE GUARD — `every_live_compose_for_turn_stamps_the_room_never_none`. A source-scanning test in
this crate's existing style, because the regression is a CALLER FORGETTING, not a logic error
any unit test would reach. It fails with the offending call site quoted. #127 built every
mechanism and nothing that would notice the caller never arrived; this is that missing piece.

Suite: 6841 pass, 0 fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
…ollow-up)

"The board is empty" and "she never got a board" are different facts, and only one of them is
knowable from an absent grounding block. `room_scope_allows` already follows this law — every
abstain names both rooms — which is exactly why the nil-room gate was diagnosable in ONE grep
after a night of guessing.

This exit was silent. So when [room-kanban] failed to appear I could not tell the read-succeeded-
but-zero-cards case from the never-ran case, and burned hours on four wrong theories instead.
A silent early-return in a grounding source is a hole in the glass box.

Emits `rag.board.empty` with persona, bound_room and turn_room, on the path where the airc read
SUCCEEDED and returned zero cards. Now every way this source can deliver nothing is on the
record: gate abstain (rag.room_gate.abstain), read failure (warn), and empty board (here).

Cost: one tracing::info on a path that is already returning early. [[observability-as-substrate]]

room_board_source 10/10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
The allocator decides how much of her mind each faculty gets and, until now, said nothing at
all. So when a grounding block failed to appear there was no way from outside to tell "the
source abstained" from "the source was granted zero tokens".

That ambiguity is exactly what survived the #331 room fix, measured live tonight on e5f4141d:

    room gate:        did NOT abstain (nil counter frozen through the turn)
    rag.board.empty:  never fired     (the board has cards)
    prompt blocks:    ['recall'] ONLY (five blocks one turn earlier, same persona, minutes apart)

Every exit from the source is instrumented and none of them fired, yet no room content reached
the prompt. The remaining candidate is the allocation, and the allocation was the one seam with
no probe. This adds it.

Emits `rag.budget.starved` per source per allocation, when granted == 0 OR granted < the
source's own floor: source, granted, floor, min, max, state, context_window. A faculty below
its floor is one the persona CANNOT HEAR this turn — that is a fact about her perception and it
belongs on the record, not in my inference.

Also the intermittency instrument for #128 ("msgs=1 and ZERO room content while Anwen's has 8,
same room, same tick"): if that is budget, this probe fires on exactly the starved turns and not
the others, and the per-turn asymmetry becomes readable instead of anecdotal.

Cost: one tracing::info only on the starved branch. Healthy allocations emit nothing.
[[observability-as-substrate]]

rag_budget 15/15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoTjvf5j3Ez13g6k8mRkFo
@joelteply
joelteply merged commit 3b96ec8 into canary Aug 6, 2026
1 check passed
@joelteply
joelteply deleted the fix/312-measurement-hands-and-inert-edits branch August 6, 2026 18:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant