Skip to content

Make the developer's own wrong account one of the options - #23

Merged
imkp1 merged 5 commits into
mainfrom
probe-misconception-distractor
Aug 8, 2026
Merged

Make the developer's own wrong account one of the options#23
imkp1 merged 5 commits into
mainfrom
probe-misconception-distractor

Conversation

@imkp1

@imkp1 imkp1 commented Aug 8, 2026

Copy link
Copy Markdown
Owner

What

On explained_it_back — and only there — stage 3 now has to build one distractor from the mechanism as the developer stated it.

Every other signal leaves stage 3 guessing at what a half-understanding looks like, working from the taxonomy of wrongness in the prompt. Rank 0 already has one in the transcript, stated by the developer and known to be wrong. The design puts the whole burden of multiple choice on the distractors — "the dangerous failure is a fluent answer describing a different mechanism, and the distractors are the only place left to catch it" — and an option someone actually believed is not a guess at that.

Two constraints keep it from backfiring:

  • Paraphrased, never quoted. grask does not show a developer their own mistake; it is why the hypothesis is internal. An option reading as your words, wrong is the accusation this design refuses to make.
  • Required to be false. Rank 0 covers accounts that were incomplete as well as wrong, and an incomplete account is often true as far as it goes — which is exactly the probe stage 4 discards for having two true options. Those get sharpened into the false claim they would imply if taken as the whole mechanism, rather than restated.

Prompt-only, and deliberately not a fifth structural gate: the four gates are all mechanically decidable, and "is this option the developer's stated mechanism" is a semantic judgement that could only be checked with a model call in the capture path — a judge by another name.

Why not thread shows through

Moment.shows names the wrong part and is the strictest-gated field in triage (triage.py:319 rejects a rank-0 moment without it), but it reaches nothing downstream — it is not in stage 2's prompt, not on Seed, not in the seeds table. Threading it end-to-end was the alternative, and it is deferred: stage 3 already receives the developer's verified quotes and the full dialogue, so the misconception is in front of it either way. If rank 0 fires and the distractor still comes out invented rather than recovered, that is the evidence for threading shows — and not before.

Measuring it

The corpus holds zero explained_it_back seeds:

asked_why            43
pushed_back          17
explained_at_length   2
new_pattern           1
explained_it_back     0

The signal shipped in #22, after every stored seed was captured, so "inspect the next N rank-0 probes" currently has N=0. The prior question is whether rank 0 fires at all — and the old report could not answer it, because a rare signal and an over-strictly gated one looked identical. Those need opposite fixes.

So triage now records every rejected moment, not only the rejections that emptied a session. demoted_from_ask answers "did this session lose all of its moments"; it names no gate and says nothing about a session that kept one moment and threw another away — which is most of them. Those rejections were computed and dropped.

triage_run reports:

  • surviving moments by signal, every ranked signal including those at zero — a signal absent from a report reads as not measured rather than never fired;
  • a tally of rejections by reason, with the turn N: prefix stripped, or one gate firing on four turns reads as four unrelated one-offs.

Run it over the whole corpus, not --limit N: that flag takes the newest sessions, which measures the deployment window rather than the signal.

Repairs the end-to-end test

test_capture_smoke is the only test that proves the four stages compose rather than each working alone, and it had been failing since stage 4 landed — broken twice over, invisibly, because calibration is deselected by default:

  1. The autouse no_real_model_calls guard that arrived with stage 4 refused the calibration run too, so the one test meant to spend money could not.
  2. Underneath it, the test asserted probes.criteria was non-empty — a NOT NULL column left from the judge design that add_probe writes [] into on every row (storage.py:847, and test_storage.py:181 asserts exactly that). No probe has satisfied it since multiple choice replaced the judge.

It now asserts the answer key that replaced criteria: option count, no duplicates, an index naming a real option, a non-empty explanation.

Verification

  • 478 passed on 3.12 and on the 3.8 floor; ruff and mypy clean; coverage 87.5% (gate 84%).
  • Real end-to-end run against a non-grask transcript: ask → seed → probe → verify, valid key, $0.73.
  • Twelve new tests, each watched failing first.

Version bumped 0.1.0-rc70.1.0-rc8 in pyproject.toml and .claude-plugin/plugin.json.

🤖 Generated with Claude Code

imkp1 and others added 5 commits August 8, 2026 21:03
On `explained_it_back` — and only there — stage 3 now has to build one
distractor from the mechanism as the developer stated it. Every other signal
leaves it guessing at what a half-understanding looks like, from the taxonomy
of wrongness in the prompt. Rank 0 already has one in the transcript, wrong,
and the design puts the whole burden of multiple choice on the distractors:
an option someone actually believed is not a guess at that.

Paraphrased, never quoted — grask does not show a developer their own mistake,
which is why the hypothesis is internal — and required to be false. Rank 0
covers accounts that were incomplete as well as wrong, and an incomplete
account is often true as far as it goes, which is the probe stage 4 discards
for having two true options. Those are sharpened into the claim they would
imply if taken as the whole mechanism rather than restated.

Prompt-only, and not a fifth structural gate: the four gates are mechanically
decidable, and "is this option their stated mechanism" is a semantic judgement
that could only be checked with a model call in the capture path.

Triage now records every rejected moment rather than only the rejections that
emptied a session. `demoted_from_ask` says a session lost *all* of its moments;
it names no gate and says nothing about a session that kept one and rejected
another, which is most of them. `triage_run` reports surviving moments by
signal — every ranked signal, including those at zero, since a signal absent
from a report reads as "not measured" rather than "never fired" — beside a
tally of rejections by reason with the `turn N:` prefix stripped. Rare signal
and over-strict gate were indistinguishable before and need opposite fixes.
The corpus holds zero `explained_it_back` seeds, so whether rank 0 fires at
all is the prior question, and this is the instrument for it.

Also repairs the end-to-end test, broken twice over and invisibly, since it is
deselected by default: the autouse `no_real_model_calls` guard that arrived
with stage 4 refused the `calibration` run too, and underneath it the test
asserted `probes.criteria` was non-empty — a NOT NULL column left from the
judge design that `add_probe` writes `[]` into on every row. It now asserts
the answer key that replaced criteria. A real run passes at $0.73.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A paired A/B over the three real rank-0 seeds — one seed each, stage 3 forked
twice, the frame text the only difference — recovered the misconception 3 of 3
with the block and 2 of 3 without it, the third a near-miss rather than nothing.
At n=3 that detects nothing. The design doc claimed the rule was doing work; it
now says the effect was not observed and the block is kept on cost, which is the
verdict the distractor-shape block already carries on the same evidence budget.

Records what the same run found upstream: rank 0 fires on 4 of 38 kept sessions,
threading `shows` is unnecessary because stage 2 restates an omission as a
falsifiable belief, and every gate rejection in 168 sessions was rank 0 on a
quote that asks rather than explains.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ery demotion a bad quote

Two review findings, both wording that asserted something untrue.

The frame told stage 3 the developer's account was "in the quotes below".
Nothing guarantees that: `seed.verified_quotes` keeps any claimed quote that
appears in some developer turn, and stage 2 chooses 1-3 freely — the selected
moment's quote need not be among them. On a seed where it chose others, the
instruction pointed at nothing. It now points at the rendered session, which
always has the account, because that is where triage found it. No threading of
`shows`; only the wording was wrong.

The report called `demoted_from_ask` sessions "demoted for a bad quote".
That flag means every moment was rejected, by any of five gates, and only one
of them is about a bad quote. It misdescribed real data on the first full-corpus
run: all three demotions were the `explained_it_back` question gate, reported
as bad quotes. In a report built to diagnose why rank 0 is scarce, that is the
kind of mislabel that sends you at the wrong fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…riage_run's spend

Three findings from the corpus run, plus the spend gate it should have had.

A question labelled `explained_it_back` is now demoted to `asked_why`. That
gate was the only one firing across 168 sessions — 3 of 7 rank-0 proposals —
and each rejection emptied its session, because those sessions had no other
moment. A question is exactly what `asked_why`'s own gate requires, so the
evidence was there and only the label was wrong; dropping threw away a probe
rank 1 would have kept. Demoted and never promoted, since rank 0 is the claim
the quote failed to support. An empty `shows` stays a rejection: no weaker
signal accepts it. `Moment.relabelled_from` keeps the mislabel rate visible
rather than folding it into rank 1.

`triage_run` refused to dry-run. `capture_run` and `reprobe` both gate on
`--go` and this one is the most expensive of the three — one call per session,
 across the corpus, billed on invocation.

Stage 1 is told that pasted prose is not the developer's account. A rank-0
moment died at a stage-2 decline on a session whose turn 0 was 2,173 characters
of pasted agent report against turns of 3-34 characters elsewhere: verbatim in a
developer turn, so the evidence rule passed, but not something the developer
believed. No structural fix exists — Claude Code inlines pasted text with no
marker, and a length heuristic would fire on anyone who writes long prompts — so
it is prompt-only and recorded as unmeasured.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
design.md had started absorbing PR bodies: the method of each A/B, the
branch that was built and reverted, the bug that motivated a gate. Those
can never disagree with the code, so they accumulate instead of being
corrected — the doc grew 1015 → 1230 lines in four commits.

Where a measurement is why a claim is weak, the number stays and the run
goes. Deletes the third copy of the rank-0 mislabel and pasted-prose
findings, which each already read once under Structural gates. States the
rule in the header so the next change has something to be held to.

No claim about the current system is dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@imkp1
imkp1 merged commit 5c2adcb into main Aug 8, 2026
10 checks passed
@imkp1
imkp1 deleted the probe-misconception-distractor branch August 8, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant