Skip to content

Land the five rulings and force the insufficient_evidence line (#508) - #519

Merged
pengfei-threemoonslab merged 10 commits into
mainfrom
claude/issue-508-ie-calibration
Sep 5, 2026
Merged

Land the five rulings and force the insufficient_evidence line (#508)#519
pengfei-threemoonslab merged 10 commits into
mainfrom
claude/issue-508-ie-calibration

Conversation

@pengfei-threemoonslab

@pengfei-threemoonslab pengfei-threemoonslab commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

What this PR is

It began as a recovery: PR #514 merged an earlier state of its branch, so main got the packet-integrity work and the codex read-boundary audit but none of the owner's rulings, the calibration record, or the round-2 re-run. This PR carries those onto main — and then carries the whole of Cut C's labelling work that followed from them.

Nine commits, in the order the work actually happened:

commit what
b46dd48 the five rulings, both calibration re-runs, and the three cases that force the evidence-gap line
c07033a PR review round: staging erased link changes; equal bytes are not identity; the recorded model was the announced one
ec25527, bcb301b widen the answer-key check past the one file it knew about; make the answer-free host a script
c6523ce the first corpus round — 96 labels, and the two blockers that stop the tag
51964a2 guide: a binding is a capability you can name; insufficient_evidence is a signal, not a step
6f2954b corpus round 2 on the corrected guide — κ = 0.8048 on 47 pairs
39b60e2 the six adjudications, and the third blocker they expose
0c0be62 retire insufficient_evidence as a target, and shorten four cells to the material that exists
cbe2324 second review round: a guide seam that split two raters, and three guards narrower than their own reasons

The owner's six rulings

# Ruling Landed
1 Judge the diff. The gate's deterministic deliverable is that capabilities changed, how, and which — not how risky that is. A review_required must hand the person a named capability or it may not block. LABELING.md rewritten. Engine: #515, #518
2 Agent instruction files are out of scope — semantic; a static gate cannot judge them and would only produce false positives. Labeled by what else the diff does, else passed. Engine: #516
3 insufficient_evidence only when it names what would resolve it Superseded by ruling 6. #517 is superseded by #520
4 Identical copies: mechanical, not a convention. identical_files by sha256 and the executable bit; citations stay authoritative, with a derived evidence_comparison_key beside them (changed by review)
5 The codex read boundary is context management, not sandboxing. A shell-bearing rater is refused on a host carrying strata-inventory.csv; --working-material for calibration
6 insufficient_evidence is a statement about the reader, not the change. It should be rare, and the reason worked out from first principles. Retired from the target vocabulary in both tiers. #520

The corpus rounds

round cases κ what it settled
calibration 5 non-corpus 0.4444 → 1.0000 after the rulings the guide contradicted itself on incomplete guards
round 3 3 constructed forced the IE line nobody had reached in 20 labels
corpus 1 48 0.6111 two blockers: cold-start slots cannot be packeted; the RR↔IE line was mine
corpus 2 47 pairs 0.8048 IE chosen 18 → 1; disagreements 14 → 6
adjudication 6 all six ruled by the owner as third identity, each disclosing a personal walk

47 finals, zero insufficient_evidence — the sixth ruling holds end to end.

The shape change (0c0be62)

Both named policies lose their insufficient_evidence cells: beta 28 strata / 100 cases → 21 / 80, pre_1_0 28 / 56 → 21 / 38. Four pre_1_0 blocked cells hold one case rather than two, because — measured over the 48 blind labels — only four of seven profiles produce a real-world blocked case at all, and each produces exactly one.

No rate moved. Every exact-match floor is still production's rate over the population it governs, rounded up. minimum_qualified_origins becomes 32 / 16 for the same reason: it was approved as 40% of the corpus, and holding the count at 40 over an 80-case corpus would have raised production's demand to half the corpus as a side effect of deleting a decision.

Recorded in docs/release-evidence-policy-decision.md § Amendment 3, STABILITY.md § no corpus case targets insufficient_evidence, and strata-inventory.md § Why four cells hold one case and not two.

Type

  • Check or risk-model change
  • Input adapter change
  • CLI or GitHub Action behavior
  • Report, schema, or SARIF output
  • Release-policy surfacepre_release_safety_requirements() / production_safety_requirements() and the stdlib restatement in scripts/_release_support.py
  • Benchmark harness, calibration cases and documentation

Verification

  • pytest -n auto: exit 0. ruff check src scripts tests: clean. CI green on 0c0be62.
  • No schema version moves; no field is added, renamed or removed. insufficient_evidence stays in ReleaseDecisionStatus and the verifier still emits it.
  • No qualification artifact exists at either tier, so nothing published is invalidated.

Things a reviewer should know

  • Labels, transcripts and adjudication material stay uncommitted per calibration.md, on the owner's machine. Ground truth will live only in FrozenSafetyCorpusV1.
  • This does not close Cut C: produce and freeze the 56 × 2 blind labels (#456) #508. Cut C's deliverable is a frozen corpus, and three cells are still short (n8n × passed, multi_agent_handoffs × passed, coding_agent_trust_roots × review_required) with one label missing. Those three are the gap rows in strata-inventory.csv.
  • One insufficient_evidence is a statement about the reader, not the change: drop it from ground truth, score it as a coverage miss #520 acceptance item is not done here: run_safety_qualification.py still emits insufficient_evidence_exact_rate as a floor metric, which now reads 0/0, passed: true — it should become a per-profile coverage-miss metric instead.
  • The rubric changed after the labels were produced (cbe2324). Review found that passed's read-only carve-out and review_required's "a capability you can name" overlapped, and it had already split two raters on one adjudicated case. The guide now states the bounded-read exception in three establishable conditions. Every passed label resting on the carve-out was re-read against the new third condition and none moves, so κ = 0.8048 stands as measured; the correction is for the next round. Written up in corpus-round-2-2026-09-03.md.
  • The answer-key check found 8 files in this checkout before this PR's second review round; it finds 52 now — the corpus round records, each construction's CASE.md, and the miner's sweeps (verifier output per pr_url) were all readable by a shell-bearing rater.
  • Round 3's cases were constructed by someone who had read the inventory, so like cal-5 they can never be corpus cases.

Refs #508, #515, #516, #518, #520. Supersedes #517.

🤖 Generated with Claude Code

…that force the evidence-gap line (#508)

**PR #514 merged an earlier state of its branch**, so main has the packet
integrity work and the codex read-boundary audit but not the owner's rulings,
not the round record, and not the round-2 re-run. This carries all of it onto
main and adds round 3. Nothing under `src/` changes; #513's files are left as
main has them.

## The owner's five rulings (2026-09-03)

1. **Judge the diff, and the gate's deterministic deliverable is the capability
   delta, not risk.** A `review_required` must hand the person a *named*
   capability or it does not get to block. `LABELING.md`: "What you are
   deciding" rewritten around that; `review_required` redefined as "a
   capability you can name"; the incomplete-guard bullet removed and its case
   moved to `passed`. Engine: #515, #518.
2. **Agent instruction files are out of scope** — semantic, and a static gate
   cannot judge them. Labeled by what *else* the diff does, else `passed`.
   Engine: #516.
3. **`insufficient_evidence` only when it names what would resolve it.**
   Engine: #517.
4. **Identical copies, mechanically.** `identical_files` in the manifest by
   sha256; `run_rater` rewrites a citation of any copy to the canonical path
   and records what was cited.
5. **The codex read boundary as context management.** A shell-bearing rater is
   refused on a host carrying `strata-inventory.csv`; `--working-material`
   proceeds for calibration and the label records which it was.

## Round 2 — the corrected guide, same five cases

**κ = 0.4444 → 1.0000.** Both splits resolved, to the side the rulings predict,
and the rater that moved cited the rule it moved on.

## Round 3 — forcing the line nobody had reached

Twenty labels across two rounds chose `insufficient_evidence` zero times, for a
rule governing 15 of 60 slots; refinement 1 was equally untouched. Three
constructed cases on one fleet-ops base: `cal-6` (nothing nameable survives),
`cal-7` (`cal-6` plus a gate made non-blocking), `cal-8` (`cal-6` plus a
literal tool that bills an account). Both families agree on all three:
`insufficient_evidence`, `blocked`, `blocked`.

They were run for what the raters *write*. On `cal-6` both named the same two
artifacts — the capability profile and the OpenAPI spec it references — with
claude naming the two keys inside the first, and noticing unprompted that the
manifest and reviewed inventory still describe the deleted tools. On `cal-7`
and `cal-8` neither rater let the opaque remainder swallow the visible finding.

A fixture defect was found mid-round by a rater and fixed: the three cases
first shipped `cal-5`'s manifest and inventory, which describe a different
agent. Decisions unchanged after the fix; the resolving sentence got more
specific.

Refs #508, #515, #516, #517, #518.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@pengfei-threemoonslab pengfei-threemoonslab left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found three integrity issues that should be resolved before this calibration/harness change lands. I reviewed the full origin/main...b46dd4824 diff, reproduced the two packet/evidence failures below, checked the archived calibration artifacts at the location named by the PR, ran pytest tests/test_rater_harness.py -q and Ruff successfully, and ran the repository verifier. Shipgate reports review_publishable / review_required (0 blockers, 28 human-review items for the fixture trust-root files). The broad non-performance suite reached 100%; its only local errors were the packaging fixture's isolated dependency download being denied by this sandbox, while all GitHub CI jobs are green.

return tree_dir, diff, pins
# From the case's own tree: staging into the throwaway repository already
# dropped the broken links, so by now there is nothing left to see.
return tree_dir, diff, pins, sorted(symlinks_in_tree(head_tree)["dangling"])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve dangling-link changes in constructed diffs

copy_tree_excluding() is used to stage both base and head at line 766, and now drops dangling links before Git sees either state. Returning only the head tree's broken-link list here cannot recover a base-only deletion or a changed link target. I reproduced a constructed case whose base contained removed-link -> missing-target and whose head removed it: the packet had the empty-tree hash for both pins, an empty diff.patch, and no broken_symlinks. That violates this builder's complete base→head-diff contract and can hide the very change under judgment. Please preserve link entries while staging (Git can store the link-target bytes without dereferencing), or otherwise encode both states, and add coverage for base-only and target-change cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, and I had it backwards — thank you.

The path is evidence. Collapsing repo/runtime/hooks/pre.sh onto repo/a.txt because they hold the same bytes destroys exactly what the citation establishes, and it did it to IndependentHumanLabelV1.evidence_references, which is the authoritative field everything downstream reads. evidence_references_as_cited recorded the damage; it did not repair it.

Taking your second remedy, since it keeps the evidence intact rather than trying to define identity well enough to be safe:

  • the rater's citations stay authoritative and untouched on the label;
  • the folded form is evidence_comparison_key, derived and recorded beside them, which is all the adjudication case ever needed — two raters citing different copies of identical bytes should not look like a disagreement, and that does not require rewriting either one;
  • grouping now keys on content and the executable bit, so your pre.sh / a.txt pair cannot group at all;
  • identical_files stays in the manifest as information, and LABELING.md still asks raters to cite one copy — a convention plus a comparison key, with nothing mechanical acting destructively on the guess.

I did not go for the "explicit, validated mirror relationship" option: it would need a definition of mirroring that is itself a guess, and the comparison key gets the benefit without needing one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Correction: my previous reply on this thread was meant for the identical_file_groups comment — an off-by-one in the script I posted with. The reply to this comment is below; the other threads are being corrected too.)

Reproduced and fixed.

Your fixture exactly: base carrying removed-link -> missing-target, head removing it, produced base_tree == head_tree (bbaca8a6… for both), a 0-byte diff.patch, and no broken_symlinks. A packet asserting nothing changed, for a change whose entire content was that deletion.

The root cause is where the drop happened. copy_tree_excluding stages both trees into the throwaway repository, so whatever it does to a link is the state git records — and it was resolving or dropping links before git saw either side. As you say, git stores a link as its target bytes without dereferencing, so staging now recreates links as links (preserve_symlinks=True), and the drop moved to the packet boundary, which is where a rater's world actually begins.

That also collapsed the broken-link list back to one computation, from the tree about to become repo/, for both external and constructed cases — the per-exporter version existed only because staging had already destroyed the evidence.

Escaping links are no longer refused at staging either, for the same reason: refusing there would block a case whose base carried one and whose head removed it. The packet boundary still refuses them.

Two tests, both failing on the previous commit: base-only deletion (asserts the pins differ and the diff carries deleted file mode 120000) and a changed target (asserts both target strings appear in the diff, and that the link is dropped from repo/ and named in broken_symlinks).

for path, digest in files.items():
if path.startswith("repo/"):
by_hash.setdefault(digest, []).append(path)
groups = [sorted(paths, key=lambda item: (len(item), item)) for paths in by_hash.values()]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Do not infer semantic identity from equal bytes

This groups every repo file with the same content hash and elects the shortest path as canonical, but a path is part of the evidence: it says which hook/provider/package is loaded and which surface changed. With the current code, a citation to repo/runtime/hooks/pre.sh:1-3 is mechanically rewritten to repo/a.txt:1-3 whenever those files happen to have equal bytes; executable mode and runtime location are ignored. evidence_references_as_cited does not repair the authoritative IndependentHumanLabelV1.evidence_references consumed downstream. Please limit canonicalization to an explicit, validated mirror relationship (including relevant path/mode semantics), or keep original references authoritative and add a separate comparison key rather than collapsing arbitrary duplicates.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and corrected.

On cal-6.security_governance: the init event says claude-opus-5[1m]; all 14 assistant message events say claude-opus-5. The init value is what the session was configured as and carries decoration the API never returns, so the provenance did not support the attribution the labels claimed.

claude_final now derives the model from the message events. Three details worth flagging:

  • it refuses a transcript whose messages name more than one serving model, rather than picking one — if the messages disagree, reviewer_id cannot name the model that ran, so there is no admissible label;
  • <synthetic> is discarded (it is what the CLI emits for its own error messages, e.g. the 401 turn), so it cannot become the recorded model;
  • the announced value is kept as announced_model with a diagnostic when the two differ, so normalising never silently hides a real disagreement between configuration and service.

All thirteen archived Claude label records across runs, runs-2 and runs-ie2 were re-derived from their own transcripts and now read claude-opus-5, each carrying model_corrected_from: claude-opus-5[1m]. The round record's header is corrected and this is written up as finding 6.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Correction: the reply that landed on this thread a moment ago was meant for the model-identity comment. This is the reply to this one.)

You are right, and I had it backwards — thank you.

The path is evidence. Collapsing repo/runtime/hooks/pre.sh onto repo/a.txt because they happen to hold the same bytes destroys exactly what the citation establishes, and it did so to IndependentHumanLabelV1.evidence_references, the authoritative field everything downstream reads. evidence_references_as_cited recorded the damage; it did not repair it.

Taking your second remedy, because it keeps the evidence intact rather than trying to define identity well enough to be safe:

  • the rater's citations stay authoritative and untouched on the label;
  • the folded form is evidence_comparison_key, derived and recorded beside them — which is all the adjudication case ever needed, since two raters citing different copies of identical bytes should not look like a disagreement, and that never required rewriting either one;
  • grouping now keys on content and the executable bit, so your pre.sh / a.txt pair cannot group at all;
  • identical_files stays in the manifest as information, and LABELING.md still asks raters to cite one copy — a convention plus a comparison key, with nothing mechanical acting destructively on the guess.

I did not take the "explicit, validated mirror relationship" option: it needs a definition of mirroring that is itself a guess, and the comparison key gets the benefit without one.


| | |
|---|---|
| `security_governance` | `claude` — `claude-opus-5[1m]`, CLI 2.1.259 |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Record the actual Claude model identity

At the archived path this document names, every Claude label in runs, runs-2, and runs-ie2 records model and reviewer_id as claude-opus-5[1m]. That malformed value comes from the system/init event; the same transcripts' assistant message events consistently identify the real model as claude-opus-5. Because condition 3 requires reviewer_id to name the model that ran, the current provenance does not support the claim that these are admissibly attributed labels. Please derive and consistency-check the model from the clean message events (or validate/normalize the init value), then correct the working label records and this round record.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and corrected.

On cal-6.security_governance: the init event says claude-opus-5[1m]; all 14 assistant message events say claude-opus-5. The init value is what the session was configured as and carries decoration the API never returns, so the provenance did not support the attribution the labels claimed.

claude_final now derives the model from the message events. Three details worth flagging:

  • it refuses a transcript whose messages name more than one serving model rather than picking one — if the messages disagree, reviewer_id cannot name the model that ran, so there is no admissible label;
  • <synthetic> is discarded (the CLI emits it for its own error turns, e.g. the 401 one), so it can never become the recorded model;
  • the announced value is kept as announced_model, with a diagnostic when the two differ, so normalising cannot silently hide a real disagreement between configuration and service.

All thirteen archived Claude label records across runs, runs-2 and runs-ie2 were re-derived from their own transcripts and now read claude-opus-5, each carrying model_corrected_from: claude-opus-5[1m]. The round record's header is corrected, and this is written up as finding 6.

…not identity, and the recorded model was the announced one

All three reproduce; each fix has a test that fails on the previous commit.

- **[P1] Staging decided what git recorded, and it was dropping dangling
  links.** `copy_tree_excluding` stages both `base/` and `head/` into the
  throwaway repository, so whatever it did to a link *was* the state git saw.
  Reproduced: a link only `base/` carried, deleted by the change, gave
  identical tree hashes for both pins, a 0-byte `diff.patch`, and no
  `broken_symlinks` — a packet asserting nothing changed, for a change whose
  whole content was that deletion. git stores a link as its target bytes
  without following it, so staging now recreates links as links
  (`preserve_symlinks=True`) and the packet boundary — where a rater's world
  actually begins — decides what survives. The broken-link list is computed
  once, from the tree that is about to become `repo/`, for both kinds of case.
  Covered for base-only deletion and for a changed target.

- **[P1] Equal bytes are not semantic identity.** Canonicalisation rewrote
  `IndependentHumanLabelV1.evidence_references`, which is what everything
  downstream reads, so a citation of `repo/runtime/hooks/pre.sh` would be
  rewritten to `repo/a.txt` whenever those happened to hold the same bytes —
  destroying the thing the citation establishes, since a path says which hook,
  provider or package is loaded. The rater's citations are authoritative again;
  the folded form is `evidence_comparison_key`, derived and beside them, for
  the one job it is good for. Grouping now keys on content **and** the
  executable bit, so `pre.sh` never joins a group with `a.txt`.

- **[P2] The recorded model was the configured one, not the serving one.**
  Every Claude label recorded `claude-opus-5[1m]` from the `init` event —
  decoration the API never returns — while the assistant message events in the
  same transcripts consistently name `claude-opus-5`. `claude_final` now takes
  the model from the message events, refuses a transcript naming more than one
  serving model, and keeps the announced value as `announced_model` with a
  diagnostic when they differ. The thirteen archived working label records were
  re-derived from their own transcripts and carry `model_corrected_from`; the
  round record is corrected and records this as finding 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Answering "why a host without the checkout?" showed the constraint was both
overstated and under-enforced.

**Under-enforced:** the guard knew only `strata-inventory.csv`. The checkout
carries ten answer-stating files — the inventory in both formats, six
adjudicated `benchmark/miner/results/*.labels.csv` (`pr_url,label` for real
PRs, several of them later pinned as corpus candidates), and the calibration
records, which now state every `cal-*` decision. `answer_keys_on_host()` looks
for all of them.

**Overstated:** "a host without the checkout" cannot work at all, because the
harness imports from `src/`. What must be absent is the answer-stating files,
and a trimmed deployment — `src/agents_shipgate/`, the two `rater/` scripts,
the packets — already is that. Recorded in `cut-c-preconditions.md`, along
with why `src/` and `docs/checks.md` are deliberately not on the list:
condition 2 forbids a rater seeing verifier *output*, and source is not that.

The test suite models a clean host by moving `REPO_ROOT` to an empty directory
rather than stubbing the lookup, so the tests still exercise the real
`answer_keys_on_host`; the refusal tests put files back under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The owner chose local rounds on OAuth over a one-time GitHub Action with API
keys: the Action would have been stronger isolation — a fresh runner, and
`--home-mode isolated`, which OAuth cannot use — but it costs a key and metered
spend for sessions that run free under the existing subscriptions.

Two consequences, both written down rather than left implicit.

**The rounds run in `shared` mode**, so blindness is *checked* rather than
*structurally impossible*. `cut-c-preconditions.md` now states exactly what
that mode does give (packet root and every ancestor checked for instruction
files, no auto-memory for the packet path, `--setting-sources ""`, codex
loading none of `config.toml`) and what `isolated` would have added (an empty
`HOME` with `--bare`, a Codex home built from nothing) — in the terms the
Amendment 1 disclosure block has to use.

**`deploy.py` builds the host.** It copies `src/agents_shipgate/` and the
`rater/` scripts into a layout whose root *is* the deployment, which is what
makes `answer_keys_on_host()` search the host the rater actually runs on rather
than somewhere harmless — the layout is the check, and a test pins that
relationship. It then runs the check against what it just built and refuses if
anything turns up. Packets are built elsewhere on purpose: choosing which to
build needs the inventory, and that is the answer.

Verified end to end: a corpus-mode session — no `--working-material` — ran from
a deployment and recorded `host_isolation: no answer key on host`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… tag (#508)

96 admissible blind labels over 48 cases, both families, the corrected guide
byte-identical in every packet, from a `deploy.py` host carrying no
answer-stating file — every label records `host_isolation: no answer key on
host`, and none used `--working-material`. Sharded by case so `claim_family`
could compare. Labels and transcripts stay on the owner's machine.

It did not reach the bar, in two independent ways.

**Only 48 of the 60 slots can be a packet.** Twelve are shipped samples under
`samples/`, which are a single tree: no `base/`, no `head/`, so not a change,
and the packet is defined as head plus the diff that produced it. They are
cold-start cases — the gate runs `scan` on a state — and making them ratable
needs a packet form with no `diff.patch` and a rubric that asks about a
repository rather than a change. Both change what the corpus measures. 48 < 56,
and the per-decision floors cannot be met either.

**κ = 0.6111 against a floor of 0.80**, and adjudication cannot repair it: κ is
a property of the two blind primaries.

The disagreement is one ambiguity, and this branch created it. Six of fourteen
splits are purely `review_required` ↔ `insufficient_evidence`; collapsing that
line gives κ 0.7322. Codex reached for `insufficient_evidence` 13 times to
claude's 5, and the rationales agree on the facts and divide on one question: a
tool the diff **registers by name**, whose endpoint and credential are citable,
but whose advertised operations live outside the packet — a capability you can
name, or a surface that cannot be established?

Both raters are following the guide, because the guide says both. Ruling 1
rewrote `review_required` as "adds a capability you can name" and the
`insufficient_evidence` list was not revisited — it still reads "an integration
is **mounted by name** and its capabilities live somewhere the repository does
not include". Those overlap on the commonest shape in real history. It is the
same structural defect that produced ruling 1, introduced when the rulings
landed rather than found by them.

No threshold is moved and nothing is adjudicated toward the inventory's
`target_decision`, which was chosen with the engine's verdict in view and is a
sourcing guess, not the answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…is a signal, not a step (#520)

The owner's sixth ruling, from first principles: `insufficient_evidence`
should be rare and avoided where possible. The first corpus round measured why
(κ 0.6111): two rater families split consistently on a tool registered by name
whose advertised operations live outside the packet, because ruling 1 had
rewritten `review_required` as "a capability you can name" while the
`insufficient_evidence` list still said "mounted by name … lives elsewhere".

The guide now says what the four agreed-IE cases already showed: "I cannot
enumerate the operations" and "I cannot establish the authority" are different
claims, and only the first is true of a remote or runtime binding. **The
authority is the binding.** A new section, *Naming a binding*, says how to
cite it and how to judge it — unbounded and unguarded → `blocked`, bounded and
attributable → `review_required`, narrowed or untouched → `passed` — and the
three shapes that used to be filed under `insufficient_evidence` move there.

`insufficient_evidence` is no longer a step in the decision procedure. It is
what a rater reaches for when the packet is incomplete, and packets are checked
complete before a session starts; reaching for it is a signal that the guide
has a gap, and the rationale must say what was unnameable so the gap can be
fixed. The illustration that used to end in it now ends in `review_required`,
and a second one — one remote endpoint, one credential — says why.

`tests/test_labeling_guide_is_rater_safe.py` passes. Corpus and requirements
changes (28 → 21 cells, the twelve cold-start slots retired) follow the
re-label, not precede it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…case without a label (#508, #520)

Same 48 cases, families, models, host layout and sharding as round 1; the one
change is `LABELING.md` carrying the sixth ruling. Packets rebuilt so the guide
inside each is that text.

**κ = 0.8048 on 47 complete pairs, against a floor of 0.80.** Round 1 on the
same 47 was 0.6036. `insufficient_evidence` went from 18 labels to 1; the four
cases both raters had filed there in round 1 are now `review_required` or
`blocked` from both, naming the binding. Disagreements fell from fourteen to
six, and they are no longer one axis: four sit on slots the inventory sourced
as `insufficient_evidence`, where the raters now split on whether the binding
is worth a human or nothing changed — a judgement call for adjudication, not a
guide contradiction.

One case has no `security_governance` label after three independent attempts
at three fresh packet paths, refused each time for omitting
`evidence_references`. Not random (three of three on this case), not the
ruling (each attempt said `passed`, on an instruction-files-only change where
the rater seems to conclude there is nothing to cite). The parser was not
loosened and `TASK.md` was not forked for one case; the 48th pair is open and
could move κ to either side of 0.80. Recorded as it stands.

Still open: the inventory and requirements changes the ruling implies
(28 → 21 cells, the twelve cold-start slots retired, the IE-sourced slots
re-targeted), which follow this record as their own PR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…#520)

The owner adjudicated all six disagreements as third identity, disclosing a
personal walk on every one. Each upheld one of the two primaries, so each
frozen record carries that rater's citations — siding with a rater is adopting
their evidence, and nothing is invented to satisfy
`HumanAdjudicationV1.evidence_references`.

**Not one adjudication landed on `insufficient_evidence`**, including the four
on slots the inventory had sourced as exactly that. Forty-seven cases now carry
a final decision (41 agreement, 6 adjudicated) and none is
`insufficient_evidence`; the sixth ruling holds end to end.

**The strata no longer balance, and that is the third blocker.** Removing the
label redistributed the cases: 14 of 21 cells reach two, seven do not.
`review_required` runs to 21 while `blocked` falls to 12, and three profiles
hold a single `blocked` case each. Totals are fine (47 against 42 needed); the
shape is not. This is a sourcing result that could not appear before the labels
existed — every slot was sourced against a target decision, and the blind
raters put the cases somewhere else.

The 28 → 21 requirements change is deliberately **not** committed with this:
writing two-per-cell into `pre_release_safety_requirements()` today would
encode a target the corpus is known not to meet. The owner settles the shape
first — source into the seven short cells, lower the per-cell count where the
material does not exist, or narrow the profile set.

Adjudication records are with the labels on the owner's machine, uncommitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the material that exists (#520, #508)

The first blind labelling round put both of these under load and answered
them with measurements rather than argument.

`insufficient_evidence` is what the gate says when its own extraction
failed. That is a statement about shipgate, not about the change, so it
cannot be the answer to "what should a correct gate do here?" Both named
policies stop demanding cases that expect it: `beta` 28 strata / 100
cases -> 21 / 80, `pre_1_0` 28 / 56 -> 21 / 38. The value stays in the
enum and the verifier still emits it; it is scored as a miss against
whatever the case expected.

Four `blocked` cells now ask for one case, not two. Of the seven
profiles only four produce a real-world `blocked` case at all, and each
produces exactly one -- Cut B recorded the cause and the W36 sweep
confirmed it, since a change that should have been stopped usually was.
The second case is reachable only by building another construction, and
a cell filled with constructions measures our imagination rather than
the world. `strata-inventory.md` carries the per-cell evidence.

No rate moved. Every exact-match floor is still production's rate over
the population it governs, rounded up; `minimum_blocked_exact` falls to
10 because there are 10 `blocked` cases at the same 100% demand. The
origin floor stays 40% of the corpus (32 / 16), which is why holding it
at a fixed count was refused -- that would have raised production's
demand to half the corpus as a side effect of deleting a decision.

The stdlib restatement in `scripts/_release_support.py` splits the four
decisions a case may *carry* from the three it may be *targeted* at, so
a zero-count cell is absent rather than present-and-empty; the sealer
compares strata by equality and a present-and-empty cell would reject
every conforming artifact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@pengfei-threemoonslab pengfei-threemoonslab left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed head 0c0be62d48a8b1def78d65bbc40b12f894ac6258 against base 1942f60b09da9a507b6c2c26a67d7e4bb62e29cf, including the earlier review fixes. I recommend addressing the four P2 findings below before merging: the new decision procedure contradicts the safe-read rule, valid in-packet reads can invalidate a Codex session, calibration-only status is lost from label records, and the answer-key check misses the corpus adjudication records added here.

Validation: all 389 tests in test_rater_harness.py, test_labeling_guide_is_rater_safe.py, test_strata_inventory.py, test_release_pipeline.py, test_safety_qualification.py, and test_safety_qualification_release.py passed. ruff check src scripts tests is clean; self-check reports ready: true; all three GitHub workflows are successful for the reviewed head. I also reproduced the three harness findings with temporary fixtures and injected CLI transcripts, without launching model sessions.

The Python and stdlib release-policy implementations agree on the 38/80-case requirements and the associated rate floors. The static verifier's current control state is review_publishable, with release decision review_required, permissions.update_pr: true, and permissions.merge: false; its protected-surface findings include the added calibration manifests/workflows. This review does not grant merge authority.

The private label/transcript records were outside this code review, so I have not independently re-derived the reported kappa. The PR already discloses the unfinished corpus and the deferred coverage-miss metric; those are not additional findings here.

Comment thread benchmark/miner/LABELING.md Outdated
Comment on lines +197 to +199
2. Does the diff introduce, widen, or unguard a capability you can name —
including a binding whose extent is decided outside the tree? →
`review_required`.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep the decision procedure consistent with the safe-read exception

A new, fully visible read-only lookup within the agent's stated purpose, using existing credentials, is explicitly passed under lines 63–65. It does not match step 1 here, but it introduces a capability the rater can name, so this first-match procedure now requires review_required. The previous procedure retained the qualification that the authority needs human confirmation; this revision removes it, and the revised review_required definition has the same overlap. Two raters can therefore follow different parts of the supplied guide and disagree on an ordinary safe-read case, affecting the labels and kappa. Make the procedure and decision definitions agree on this case before using the guide for another corpus round.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and it had already bitten a real case.

You are right that the two readings coexist. What I found checking how far it reached is that they did not stay hypothetical: on mcp_openapi_declared_binding.insufficient_evidence.1 the two raters split on exactly this seam, and the framework_tooling rationale reads almost verbatim from the carve-out — "this is a fully visible, read-only addition within the … server's stated operational scope" — while security_governance reached review_required on "the change registers one genuinely new tool". Two raters, same guide, different halves of it.

Your adjudication is what settles the direction, and it did not settle it the obvious way. The obvious reading of "upheld review_required on a new read-only tool" is a new registered callable is always review_required, which would delete the exception. The upheld rationale says something narrower and better: the new read escapes the bound that constrains the agent's other reads — the per-tool organization allowlist only enforces when a terraform_org_name argument is present, this call takes none, and it returns every organization the token can see including contact addresses. The exception is real; its third condition was missing.

So the guide now names it — the bounded-read exception — in three conditions you can establish from the packet:

  1. read-only — no write, no destructive call, no outbound communication;
  2. fully visible reach — you can point at the endpoint, path or query; nothing about its extent is decided outside the tree;
  3. no new reach — no new credential, permission or scope, and nothing returned that the agent could not already reach under the credential it holds.

Stated in full under passed, referenced from the review_required headline and from step 2, so first-match order is sound wherever a rater enters. Condition 3 is flagged as the one that does the work, and the rule asks for the bound to be cited either way — which is what makes the label checkable by someone who disagrees.

Added a constructed pair that shows where it stops: two tools, both new, both read-only, both fully visible; one reaches a fixed path template under the per-tenant scope that already bounds every other read (passed), one takes no tenant argument so that scope does not apply to it (review_required). Constructed, per the rater-safety guard — neither is a corpus case.

No existing final moves, and I checked rather than assumed. I swept every passed label whose rationale invokes the carve-out (11 of them) and re-read each against condition 3. The only one that adds a new read-only tool reaches a fixed path template under the scope that already bounded the agent, so it stands — and it is now consistent with the adjudicated case rather than in tension with it, which was the thing I most wanted to be true and least expected. The rest are renames, version bumps, behaviour-preserving refactors, or in-tree bindings naming the same tools.

The round is not re-run. κ = 0.8048 was measured under the guide as it stood, and the correction changes no label in it. Recorded in corpus-round-2-2026-09-03.md rather than absorbed quietly, including the part that matters for the next round: the next κ is not comparable to this one on cases where the exception decides.

Comment on lines +842 to +844
for root in sorted(forbidden, key=lambda item: str(item)):
if str(root) in script:
reasons.append(f"names {root}")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Exempt paths inside the assigned packet from the parent-directory check

forbidden includes packet.parent, so every canonical absolute path inside the packet contains a forbidden string. I reproduced a complete, valid injected Codex transcript whose only command is cat <packet.resolve()>/repo/agent.py: run_rater() raises RaterError and writes no label, even though the session accessed only its assigned input. The deployment layout also makes REPO_ROOT an ancestor of the packets. Check actual path containment, with the current packet allowed, rather than rejecting any script containing an ancestor's spelling. Add a regression for an absolute in-packet read alongside the existing relative-path examples.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reproduced and fixed. Your fixture exactly: a transcript whose only command is cat <packet.resolve()>/repo/agent.py raised RaterError and wrote no label, for a session that read nothing but its assigned input. And you are right that the deployment layout makes it worse rather than being a corner case — deploy.py puts the packets under the same root REPO_ROOT resolves to, so the checkout root is an ancestor too.

The cause is that the check asked how a path is spelled instead of where it lands. str(root) in script cannot distinguish "inside the packet" from "inside the packet's parent" when one contains the other, and the packet is always inside its parent.

Each token is now resolved against the session's working directory — which is the packet, Invocation.cwd — and judged by containment:

  • lands inside the assigned packet → allowed, always;
  • lands inside a forbidden root → named (checkout, the round's output directory, the sibling packets);
  • otherwise, if the token carried .. → reported as leaving the packet.

Three things fell out that I did not expect and am keeping:

  • repo/../repo/agent.py never left the packet, and the old .. token test flagged it. Resolving is what tells the difference between a walk out and a walk that comes back.
  • Two spellings of one directory now compare equal. On this host /tmp is a symlink to /private/tmp and the packets live under it, so the substring test could be defeated by spelling the same path the other way. resolve() closes that without being aimed at it.
  • --file=<sibling> needed handling. Read whole it is a relative path that lands inside the packet, so a containment test seeing only whole tokens would have passed it — the value after = is now audited too. This one was caught by the old substring test, so it is a regression guard on the new implementation rather than a bug fix, and the test says so.

Tests, all three of the first kind failing on the previous commit (I re-ran the probes against the old implementation to be sure rather than assuming): an absolute in-packet read, an in-packet .., and naming the packet directory itself; plus the flag-value guard; plus a unit-level test that names each side of the containment rule once, keeping the four refusals — relative sibling, absolute sibling, the output directory, the checkout's inventory — and the two allowances a narrow guard has to keep, a bare tool name and /usr/bin/grep.

The docstring now says the check asks where a path lands, and why the obvious spelling test cannot work here.

"cli_version": cli_version,
"announced_model": announced,
"family_independence": family_independence,
"host_isolation": host_isolation,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Persist working-material status independently of host isolation

The new --working-material flag promises that a run's labels are calibration material and never corpus evidence, but the record only carries host_isolation. For Claude that is always no shell; for OpenAI on a clean deployment it is no answer key on host, regardless of the flag. Running both families with working_material=False and True produced otherwise identical records after removing session identities, with the same validated label shape and no calibration marker. A later freeze/audit cannot recover which records the caller explicitly excluded from evidence. Store the boolean or an explicit evidence-eligibility field on every record, even when no host check needed relaxing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed and fixed. working_material is now on every record, not only where it changed what the host check did.

Your reasoning is the fix's justification, so I have written it into the code comment rather than paraphrasing it: host_isolation cannot carry this. For the claude family it is always "no shell", and on a clean deployment it is "no answer key on host" whichever way the flag was passed — so two runs differing only in this produce identical records once the session identities are removed, and a freeze has nothing to exclude on.

The regression test asserts both halves rather than just the new field: it runs both families under both settings, checks working_material round-trips on all four records, and then asserts the point — that host_isolation is identical for both settings in both families, so it was never the place to read this from.

I kept the field as the caller's declaration (working_material) rather than adding a derived evidence_eligible. The declaration is a fact about how the run was invoked; eligibility is a conclusion the freeze draws, and if a second reason to exclude a record ever appears I would rather add it than have two fields that can disagree about one of them. Happy to switch if you would rather the record state the conclusion.

Comment on lines +940 to +942
"benchmark/safety-qualification/calibration.md",
"benchmark/safety-qualification/calibration-round-*.md",
"benchmark/miner/results/*.labels.csv",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Include the new corpus-round records in the answer-key check

This PR also adds corpus-round-2026-09-03.md and corpus-round-2-2026-09-03.md; the latter publishes a table mapping six exact case IDs to both primary labels and the owner's final decisions. Neither matches these patterns. With the actual round-2 file copied into an otherwise clean harness root, answer_keys_on_host() returns [] and check_answer_key_not_on_host('openai', working_material=False) returns no answer key on host. Thus a deployment that acquires these records between rounds passes the new pre-launch check despite carrying the answers. Include corpus-round-*.md and cover a root containing only a corpus record, rather than relying on the already-matched inventory files to cause refusal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and the pattern list was narrower than its own stated reason in two more places.

Your case first: the round-2 record publishes case id → both primary labels → the owner's final decision for six cases, and it matched nothing. The part that makes it a real hole rather than an oversight is the one you named — alone on a clean root it passed. Every time I would have noticed, the inventory happened to be sitting beside it and caused the refusal for a different reason.

Chasing that, two classes it never knew about at all:

  • benchmark/safety-qualification/constructed/*/CASE.md — each names its target cell and its slot id. The file's own first paragraph says "it names a target decision, so it must never be included in a rater packet", and the check that exists to keep answers off the host did not look for it.
  • benchmark/miner/results/*-mined.csv / .jsonl — the sweeps carry head_decision, verify_decision and verify_verdict per pr_url. Only *.labels.csv was matched. This is the worst of the three: it is verifier output, keyed by the exact PRs the inventory then pinned as candidates, which is what condition 2 forbids most directly.

Matched by directory now rather than by naming each file — benchmark/safety-qualification/*.md, constructed/*/CASE.md, benchmark/miner/results/* — because the next round record will be written by someone who will not think to come back and extend a tuple. rater/ is a subdirectory, so the harness itself is not swept up, and deploy.py still produces a clean host (its own assertion covers that). 8 files found in this checkout before, 52 now.

Tests: the enumeration is one list used twice, and the second use is the one you asked for — each class is planted alone on an otherwise empty root and must refuse on its own, so no class can be masked by another.

One thing worth flagging because it is a trap in the other direction: my first draft used a real construction's name in the fixture list, and test_declared_exposure_is_at_least_what_the_tree_shows failed it — naming a construction in tests/ declares it engine-exposed, which would have cost the corpus a holdout-eligible slot. The fixture name is now deliberately not a real construction, with a comment saying why.

… guards that were narrower than their own reasons (#508, #520)

**The rubric could be read two ways, and it was.** `passed` carved out
"read-only additions whose reach is fully visible and plainly within the
agent's stated purpose"; the `review_required` headline and procedure
step 2 said "adds, widens, or unguards a capability you can name" with no
exception. A new read-only tool is both.

It bit a real case: on one adjudicated pair the two raters split on that
seam, and the `framework_tooling` rationale reads almost verbatim from
the carve-out. The owner's adjudication settles the direction, and not
the obvious way -- it upheld `review_required` on a rationale that turns
on *reach*, not on "a new tool": the org allowlist only enforces when an
org argument is present, that call takes none, so the bound constraining
every other read does not reach it.

So the exception is real and its third condition was missing. The guide
now states the **bounded-read exception** in three establishable
conditions -- read-only, fully visible reach, and no reach the agent did
not already have -- in `passed`, referenced from both other sites so all
three agree, with a constructed pair showing where it stops. No existing
final moves: every `passed` resting on the carve-out was re-read against
condition 3, and the one that adds a new read-only tool reaches a fixed
path template under the scope that already bounded the agent.

**The command audit asked how a path was spelled, not where it lands.**
`packet.parent` is a prefix of every absolute path *inside* the packet,
so `cat <packet>/repo/agent.py` -- a session reading only its assigned
input -- produced no label at all, and the deployment layout makes
`REPO_ROOT` an ancestor of the packets too. Each token is now resolved
against the session's working directory (which is the packet) and judged
by containment. Resolving also fixes what the substring test got wrong in
the other direction: `repo/../repo/x` never left, and two spellings of a
symlinked directory now compare equal.

**The answer-key check knew four filenames.** It missed the corpus round
records -- one publishes case id -> both primary labels -> final decision
-- and it missed them *alone*: with only that file present the check
passed. Two classes it never knew about at all: each construction's
`CASE.md` names its target cell and slot id, and the miner's
`*-mined.csv`/`.jsonl` sweeps carry `head_decision`/`verify_decision` per
`pr_url`, which is verifier output for the very PRs the inventory pinned.
Matched by directory now, because the next round record is written by
someone who will not think to come back here. 8 files found before, 52
now. Each class is asserted to refuse on its own.

**`--working-material` was not recorded.** Only `host_isolation` was, and
it cannot carry the flag: for claude it is always "no shell", and on a
clean deployment "no answer key on host" either way -- so two runs
differing only in this wrote identical records and a freeze could not
tell which labels the caller had excluded from evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pengfei-threemoonslab
pengfei-threemoonslab merged commit c41571f into main Sep 5, 2026
10 checks passed
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.

Cut C: produce and freeze the 56 × 2 blind labels (#456)

1 participant