agent-bios 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -80,6 +80,7 @@ patching them one at a time is a queue that refills.
80
80
 
81
81
  - At each stage, run review loops as appropriate: self review, subagent review when available, and structured multi-lens review when the repository or domain supports one (concrete tool: Environment Binding below).
82
82
  - Iterate until material issues reach zero: review, identify material issues, fix them, and review again.
83
+ - "Material issues reach zero" is counted over the declared defect criterion's stop-relevant class; choosing and declaring that criterion is owned by `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/guides/review-defect-criteria.md`.
83
84
  - Use the severity contract for materiality — the canonical definition is the ladder below; external review tools map their levels onto it: blocker, high, and medium are material; low and info are non-material.
84
85
  - Treat blocker as primary happy-path or core-contract failure.
85
86
  - Treat high as supported user, environment, data, or execution path failure.
@@ -0,0 +1,205 @@
1
+ ---
2
+ guide_id: review-defect-criteria
3
+ language: en
4
+ status: active
5
+ use_when:
6
+ - declaring what counts as a defect before dispatching any review
7
+ - choosing or writing the defect criterion for a system type × work goal
8
+ - a review loop plateaus, diverges, or its "material 0" stop never arrives
9
+ - a finding's class is contested at the boundary, or two lenses class it differently
10
+ core_rules:
11
+ - choose and declare the criterion before the review starts — undeclared, every reviewer substitutes its own
12
+ - the severity ladder answers how bad; the criterion answers whether it is a defect, of which class, and what "zero" is counted over
13
+ - a criterion with an empty cell is a hunch, not a criterion — do not start the review
14
+ - paste the goldens into the packet verbatim; a definition alone does not classify consistently at the boundary
15
+ - put the class enum on the accepting channel where one exists; on prose routes, run the fold procedure
16
+ - a mixed packet is split — never run two observers in one trajectory
17
+ verification_focus:
18
+ - every dispatched packet names its criterion and carries its goldens
19
+ - each round audits reported class against post-measurement class; repeated disagreement mints the next golden
20
+ - the stop condition is evaluated over the stop-relevant class only, never the whole finding count
21
+ ---
22
+
23
+ # Review Defect Criteria
24
+
25
+ A defect criterion is chosen, not assumed. This guide is a scoped extension of
26
+ `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/guides/review-request.md`: before that guide's
27
+ request composition starts, this one decides what the review is hunting. The
28
+ evidence: one codebase, two stop criteria, opposite trajectories.
29
+
30
+ | Rounds | Criterion | Trajectory |
31
+ |---|---|---|
32
+ | early | material = contract violation **or** unprotected contract sentence, small frozen surface | 10 → 4 → 1 → 0 |
33
+ | late | same, scoped to client-observable behavior | 17 → 19 → 21 → 23 → 19 — **plateau** |
34
+ | final | split classes: behavioral_defect / coverage_gap / doc_gap; stop = behavioral 0 | behavioral 10 → 5 — falling again |
35
+
36
+ The plateau was the criterion, not the code: counting "a contract sentence no test
37
+ protects" as a defect means every fix adds contract rows, each a potential defect
38
+ next round — a self-refilling criterion cannot reach zero while its surface grows.
39
+ Yet that same criterion is exactly right for a library, where an unprotected promise
40
+ is a first-class defect. Neither is wrong; starting without choosing is.
41
+
42
+ The **severity ladder** (in `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/guides/coding-staged-workflow.md`)
43
+ answers *how bad* a finding is. The **criterion** answers *whether* it is a defect at
44
+ all, of which class, and what "zero" is counted over. The plateau happened entirely
45
+ inside "material" — no severity adjustment could have ended it. Ordering: system type
46
+ + goal → defect criterion → classification → defect? → severity → materiality → stop.
47
+
48
+ ## The criterion schema
49
+
50
+ One entry carries all of these. An empty cell makes it a hunch, not a criterion — do
51
+ not start the review on it.
52
+
53
+ | Field | Meaning |
54
+ |---|---|
55
+ | observer | whose eyes judge — client, caller, operator, a model reading the output |
56
+ | defect | what the observer must experience |
57
+ | classes | the enum, exactly ONE class stop-relevant; the others are relief valves that keep the stop class honest |
58
+ | evidence | what one finding must present |
59
+ | non-defects | defect-lookalikes excluded by this criterion — named explicitly |
60
+ | stop condition | what "0" means, counted over the stop class, and WHY it is reachable — how a growing surface is pinned |
61
+ | misclassification cost | which error is expensive — the direction to tell the reviewer to lean |
62
+ | goldens | ≥2 positive, ≥2 negative, ≥1 boundary; each with why, date, and provenance: `measured` (dated incident) or `constructed` (authored to pin a boundary) |
63
+
64
+ ## The catalog
65
+
66
+ Four system types. A starting set, not a census.
67
+
68
+ ### Web service / API surface (contract compliance)
69
+
70
+ - **Observer**: the HTTP client.
71
+ - **Defect**: a client today receives a response that differs from the contract — status code, envelope, stream terminal frames, the model actually run, isolation, resource caps.
72
+ - **Classes**: `behavioral_defect` (stop-relevant) · `coverage_gap` · `doc_gap`.
73
+ - **Evidence**: the concrete request and observed vs promised response.
74
+ - **Non-defects**: internal lifecycle (unless a request sequence shows two different responses), absence of a test, documentation wording.
75
+ - **Stop**: behavioral 0 — reachable: the code surface is finite and shrinks monotonically unless a fix creates a new behavioral defect.
76
+ - **Misclassification cost**: false negatives (a client actually breaks); but inflating coverage gaps into defects destroys the stop condition — audit category creep separately.
77
+ - **Goldens** (all `measured`, 2026-08-13–15):
78
+ - **+** `GET` on a nonexistent path returned 405, not 404 — an existing-method complaint attached to a path that does not exist.
79
+ - **+** a streaming endpoint's terminal delta carried only `output_tokens`, so cache/input token counts never reach a streaming client — non-streaming was correct, which is why nobody saw it.
80
+ - **−** "no test catches a one-line flip of this default": today's behavior is correct — `coverage_gap` here, a defect only under the library criterion.
81
+ - **−** child-process kill grace period — out of scope until a request sequence shows a client-visible difference; pins the observer to the client.
82
+ - **±** rejecting a documented-as-valid boundary value (compression 100, docs say "below 100 requires jpeg/webp") — the documentation is what makes it a defect; without that sentence it is taste.
83
+
84
+ ### Library / SDK / contract-first system
85
+
86
+ - **Observer**: the caller **and** the future maintainer.
87
+ - **Defect**: a promise violated today, or a promise unprotected — a one-line change breaks it and nothing catches it. The API criterion's `coverage_gap` is first-class here.
88
+ - **Classes**: `contract_defect` (stop-relevant) · `style_note` · `internal_change`.
89
+ - **Evidence**: for a violation, as above; for an unprotected promise, the promise sentence + the one-line change that breaks it + the absence of any catching test.
90
+ - **Non-defects**: style, internal structure.
91
+ - **Stop**: caution — this criterion self-refills while the contract grows. Declare "0" only over a surface whose growth has stopped; on a growing surface, narrow the stop to "every promise this change added is protected."
92
+ - **Misclassification cost**: balanced — miss an unprotected promise and the next refactor breaks it silently; over-report and the loop never ends.
93
+ - **Goldens**:
94
+ - **+** a streaming-image event test matched only `.completed`, so an edit stream misnamed `image_generation.completed` still passed — mutation survival showed the promise unprotected. (`measured`, 2026-08-13–15)
95
+ - **+** the contract promised "storage serves a large image at least once"; the code evicted it on the next request — resolved by fixing the **contract**: the disagreement is the defect, whichever side moves. (`measured`, 2026-08-13–15)
96
+ - **−** renaming an internal helper no document promises — the observer holds the contract; undocumented internals carry no promise. (`measured`, 2026-08-13–15)
97
+ - **−** a caller breaking on undocumented iteration order — no promise existed; the rename boundary pinned from the caller's side. (`constructed`, 2026-08-19)
98
+ - **±** an inventory-listed test turns out vacuous — asserts nothing about its promise. "A test exists" is a claim about names until the assertion is read; hand the reviewer "check the assertion, not the name." (`measured`, 2026-08-13–15)
99
+
100
+ ### AI workbench / harness (agent orchestration, review loops, verification pipelines)
101
+
102
+ - **Observer**: the operator, and any model consuming the harness's output.
103
+ - **Defect**: **a false signal that looks true** — a wrong PASS, a vacuous test, a silent fallback, a packet missing the call site, a wrong denominator. Being wrong is not the defect; being wrong while *looking right* is. Second form: a run claimed without provable dispatch — no receipt.
104
+ - **Classes**: `false_signal` (stop-relevant) · `detected_miss`. An unproven dispatch — a run or PASS claimed with no receipt — is classed `false_signal`, not given its own relief valve: a signal that cannot be shown true is counted false, or a round could declare completion while every dispatch stayed unproven.
105
+ - **Evidence**: the input on which the instrument gave a false verdict, plus the known correct answer. The standard probe: run the instrument against an input whose answer is known to be the opposite.
106
+ - **Non-defects**: one output's style; a false positive the harness itself **detected** — a caught error is the harness working.
107
+ - **Stop**: every PASS emitted this run survives a known-opposite check and evidences its own dispatch. The unit is "this run's signals are trustworthy," not a standing zero.
108
+ - **Misclassification cost**: false passes dominate. An instrument bug reporting failure dies in minutes because someone looks; one reporting success survives — a selection effect.
109
+ - **Goldens**:
110
+ - **+** a shell test runner received one nonexistent path (zsh does not word-split an unquoted variable), so every run exited 1 and every mutation reported KILLED — a green instrument that never ran its subject. (`measured`, 2026-08-13–15)
111
+ - **+** a test-inventory grep matched only `^test('...'` and missed every parameterized name; three reviewers judged a populated file empty — nothing tied the inventory's denominator to the source's own count. (`measured`, 2026-08-13–15)
112
+ - **+** a negative control kept passing after a faithful revert of the fix it was written against — a guard satisfied by an absence, a false PASS about the gate itself; found only by re-running reverts. (`measured`, 2026-08)
113
+ - **+** a review round returned "clean" with no receipt evidencing that the declared packet was dispatched on the exact seat — `false_signal` although nothing observed was wrong: what the stop refuses is absence of proof of dispatch, not proof of falsity. (`constructed`, 2026-08-19, pinned after a classification dispute at exactly this boundary)
114
+ - **−** one reviewer over-classed an item as material; another lens plus measurement filtered it — the harness caught it, so it worked: `detected_miss`, not a defect. (`measured`, 2026-08-13–15)
115
+ - **−** five review rounds returned 8 → 9 → 10 → 5 → 12 findings, refusing to converge — and every count was true. A truthful unpleasant signal is not a false signal; the defect lived in the undeclared criterion. (`measured`, 2026-08-16–17)
116
+ - **±** a surviving mutation proved equivalent — the platform already normalized what the mutated guard checked. Neither a harness defect nor a test gap; but a harness that auto-reads "survived = gap" has a defect in that rule. (`measured`, 2026-08-13–15)
117
+
118
+ ### Decision ontology (an ontology a model decides from)
119
+
120
+ - **Observer**: a model or agent deciding from the ontology alone.
121
+ - **Defect**: a representation that produces a wrong decision or blocks a right one: overlapping concept boundaries, a missing distinction the decision needs, an instance contradicting reality, a wrong relation direction or cardinality, a name implying what the definition does not say.
122
+ - **Classes**: `decision_defect` (stop-relevant) · `representation_note` · `out_of_scope_gap`.
123
+ - **Evidence**: a **decision scenario** — "answered from the ontology alone, this question yields X; reality is Y" — with the question, the path taken, and the ground truth.
124
+ - **Non-defects**: representation format, completeness as such, a question the model would get wrong without the ontology too.
125
+ - **Stop**: zero wrong decisions over the agreed scenario set. The set is the scope — fix it first, or this criterion self-refills like the library one.
126
+ - **Misclassification cost**: situational — feeding a hard gate makes false negatives expensive; exploratory aid makes false positives expensive. Filling this cell is mandatory at declaration.
127
+ - **Goldens** (all `constructed`, 2026-08-19, authored to pin boundaries the source loop's decision-scenario framing left open):
128
+ - **+** `Customer` and `Account` both define "the paying party"; a refund-routing question resolves through both paths — overlap is a decision defect even when each definition is individually correct.
129
+ - **+** `Order —hasOne→ Payment` while split payments exist — cardinality is a claim, and a false claim misleads the deciding model.
130
+ - **−** a verbose concept description — no decision changes; form is outside this observer's sight.
131
+ - **−** a domain absent that no scenario in the agreed set needs — completeness is scoped by the set, not the world.
132
+ - **±** the distinction exists but the model cannot find it (name or link missing) — with the observer fixed as "a model seeing only the ontology," unreachable is a representation defect, not a search defect. The observer clause decides the class.
133
+
134
+ ## Before any review
135
+
136
+ 1. Write one sentence: the system type and this work's goal. ("API surface — make the responses clients receive today match the contract." / "Harness — make this run's PASS signals trustworthy.")
137
+ 2. Pick a criterion from the catalog, or fill the schema fresh. A cell you cannot fill means the review does not start.
138
+ 3. Paste the goldens into the packet verbatim. Never the definition alone.
139
+ 4. Declare the stop condition and why it is reachable — or how the scope was pinned to make it so.
140
+ 5. Put the classification on the accepting channel (next section).
141
+ 6. Audit every round: reported class vs the class confirmed after measurement. A repeated disagreement is the next golden.
142
+
143
+ ## Enforcing the enum, by channel
144
+
145
+ Ranked by how much the channel refuses for you:
146
+
147
+ 1. **A route with a submit schema**: classification is a required enum field. The
148
+ measured precedent is anchors — 2,466 of 2,466 findings carried one, because the
149
+ schema refuses output without it. Where this channel exists, use it.
150
+ 2. **Prose-packet routes** (the shipped deep-review methods): the packet header
151
+ declares `Criterion: <name>` with the goldens pasted verbatim, and the dispatching
152
+ agent runs this fold procedure on what comes back:
153
+ 1. For each returned finding row, look up its class against the declared enum.
154
+ 2. A row carrying a class from the enum enters the findings ledger under that class.
155
+ 3. A row with no class, or a class outside the enum, is **not admitted**: send it
156
+ back once for classification, or record it as refused with the reason. Never
157
+ admit it unclassified, and never guess its class for it.
158
+ 4. Count the stop condition over the stop-relevant class only.
159
+ 5. Record the audit pair (reported class, confirmed class) for procedure step 6.
160
+
161
+ Stated honestly: on a prose route this is steering, not control — the fold is
162
+ performed by an agent following this guide, and nothing structural refuses a
163
+ class-less row for it. That is the known weaker result of a request-only rule.
164
+ 3. A structural criterion slot in the review dispatch machinery is deliberately not
165
+ part of this guide: a criterion is per-review while dispatch config is per-launch,
166
+ and a slot with no schema-backed consumer would be an inert value. Until such a
167
+ channel exists, ranks 1–2 are the real ones.
168
+
169
+ ## Golden lifecycle
170
+
171
+ - **Admission**: only a golden that decides a boundary the definition leaves open,
172
+ evidenced by a recorded classification disagreement or an audited misclassification;
173
+ provenance labeled `measured` or `constructed`, each with its why and date.
174
+ - **Overturn**: only by a measured counterexample. Correct the golden in place — this
175
+ guide describes the present — plus a dated decision record, in whatever channel the
176
+ repo keeps decisions, naming the closed golden and the counterexample. Silent
177
+ deletion is forbidden: a wrong golden makes reviewers systematically wrong, and an
178
+ untracked fix hides that it ever did.
179
+
180
+ ## Declaring, switching, and mixed packets
181
+
182
+ - Every dispatched packet names its criterion. A harness round declares `AI harness`;
183
+ a round whose system type has no catalog entry declares a **task-local criterion**,
184
+ written in the packet itself, conforming to the schema above and labeled task-local
185
+ — repeated use is what earns a catalog entry.
186
+ - A mixed packet is split into one packet per criterion. Two observers in one
187
+ trajectory produce findings no single stop condition can count.
188
+ - When a criterion changes mid-loop: re-classify only the findings still open; rounds
189
+ already recorded are dated history. Never splice pre- and post-switch counts into
190
+ one series — they count different things.
191
+
192
+ ## Evidence base
193
+
194
+ Derived from a 39-round adversarial review loop (3 lenses × a frontier reviewer) on a
195
+ peer OAuth CLI-API adapter, 2026-08-13–15: the same codebase plateaued at
196
+ 17→19→21→23→19 findings under a self-refilling criterion and resumed falling
197
+ (behavioral 10→5) the round the classes were split and the stop counted over
198
+ `behavioral_defect` alone. The boundary evidence is from the same loop: a packet
199
+ instruction saying "do not inflate coverage gaps" still left two lenses classing one
200
+ finding `doc_gap` and `behavioral_defect`, both defensibly — one golden would have
201
+ decided it. The non-convergence golden (8→9→10→5→12, every count true) is this
202
+ environment's own launcher-review loop, 2026-08-16–17, run under an undeclared
203
+ "material" criterion; the two series are two populations under two criteria and are
204
+ cited separately for exactly the reason the migration section gives. Re-derive when
205
+ the review routes or the bound models change.
@@ -26,7 +26,11 @@ verification_focus:
26
26
  This guide is a scoped extension of the global Coding Guidelines. Use it when
27
27
  composing what you ask a reviewer for — the request, the evidence bar, the
28
28
  verdict shape. It does not cover when to review, how deep, or what counts as
29
- material (the severity ladder and review loop in `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/guides/coding-staged-workflow.md` own that),
29
+ material — that last question splits in two: the severity ladder and review loop in
30
+ `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/guides/coding-staged-workflow.md` own *how bad*,
31
+ while whether something is a defect at all, of which class, and what "zero" is
32
+ counted over is the defect criterion, owned by
33
+ `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/guides/review-defect-criteria.md`. Nor
30
34
  which reviewer kind to route to — the convergence heuristic in
31
35
  `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/guides/verification-discipline.md` owns that. Phrasing a prompt for a specific model
32
36
  family is out of scope here; where that guidance ships, the rule that needs it
@@ -38,6 +42,16 @@ is a per-model claim; these are the failures that persist regardless of who
38
42
  reviews. Every rule names the evidence behind it, because a review guide that
39
43
  asserts without evidence would fail its own bar.
40
44
 
45
+ ## Declare the criterion
46
+
47
+ Before composing anything else, name the defect criterion the review runs under —
48
+ the per-system-type definition of what a defect is, its class enum, and what "zero"
49
+ is counted over. `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/guides/review-defect-criteria.md`
50
+ owns choosing it; this guide assumes one is declared. Put `Criterion: <name>` at the
51
+ top of the packet with that criterion's goldens pasted verbatim — a definition
52
+ without goldens does not classify consistently at the boundary, and an undeclared
53
+ criterion means every reviewer substitutes its own.
54
+
41
55
  ## Demand a failure path, not a gap
42
56
 
43
57
  The dominant reviewer failure is not hallucination. Across 372 rejected
@@ -80,6 +80,7 @@ patching them one at a time is a queue that refills.
80
80
 
81
81
  - At each stage, run review loops as appropriate: self review, subagent review when available, and structured multi-lens review when the repository or domain supports one (concrete tool: Environment Binding below).
82
82
  - Iterate until material issues reach zero: review, identify material issues, fix them, and review again.
83
+ - "Material issues reach zero" is counted over the declared defect criterion's stop-relevant class; choosing and declaring that criterion is owned by `${CODEX_HOME:-$HOME/.codex}/guides/review-defect-criteria.md`.
83
84
  - Use the severity contract for materiality — the canonical definition is the ladder below; external review tools map their levels onto it: blocker, high, and medium are material; low and info are non-material.
84
85
  - Treat blocker as primary happy-path or core-contract failure.
85
86
  - Treat high as supported user, environment, data, or execution path failure.
@@ -0,0 +1,205 @@
1
+ ---
2
+ guide_id: review-defect-criteria
3
+ language: en
4
+ status: active
5
+ use_when:
6
+ - declaring what counts as a defect before dispatching any review
7
+ - choosing or writing the defect criterion for a system type × work goal
8
+ - a review loop plateaus, diverges, or its "material 0" stop never arrives
9
+ - a finding's class is contested at the boundary, or two lenses class it differently
10
+ core_rules:
11
+ - choose and declare the criterion before the review starts — undeclared, every reviewer substitutes its own
12
+ - the severity ladder answers how bad; the criterion answers whether it is a defect, of which class, and what "zero" is counted over
13
+ - a criterion with an empty cell is a hunch, not a criterion — do not start the review
14
+ - paste the goldens into the packet verbatim; a definition alone does not classify consistently at the boundary
15
+ - put the class enum on the accepting channel where one exists; on prose routes, run the fold procedure
16
+ - a mixed packet is split — never run two observers in one trajectory
17
+ verification_focus:
18
+ - every dispatched packet names its criterion and carries its goldens
19
+ - each round audits reported class against post-measurement class; repeated disagreement mints the next golden
20
+ - the stop condition is evaluated over the stop-relevant class only, never the whole finding count
21
+ ---
22
+
23
+ # Review Defect Criteria
24
+
25
+ A defect criterion is chosen, not assumed. This guide is a scoped extension of
26
+ `${CODEX_HOME:-$HOME/.codex}/guides/review-request.md`: before that guide's
27
+ request composition starts, this one decides what the review is hunting. The
28
+ evidence: one codebase, two stop criteria, opposite trajectories.
29
+
30
+ | Rounds | Criterion | Trajectory |
31
+ |---|---|---|
32
+ | early | material = contract violation **or** unprotected contract sentence, small frozen surface | 10 → 4 → 1 → 0 |
33
+ | late | same, scoped to client-observable behavior | 17 → 19 → 21 → 23 → 19 — **plateau** |
34
+ | final | split classes: behavioral_defect / coverage_gap / doc_gap; stop = behavioral 0 | behavioral 10 → 5 — falling again |
35
+
36
+ The plateau was the criterion, not the code: counting "a contract sentence no test
37
+ protects" as a defect means every fix adds contract rows, each a potential defect
38
+ next round — a self-refilling criterion cannot reach zero while its surface grows.
39
+ Yet that same criterion is exactly right for a library, where an unprotected promise
40
+ is a first-class defect. Neither is wrong; starting without choosing is.
41
+
42
+ The **severity ladder** (in `${CODEX_HOME:-$HOME/.codex}/guides/coding-staged-workflow.md`)
43
+ answers *how bad* a finding is. The **criterion** answers *whether* it is a defect at
44
+ all, of which class, and what "zero" is counted over. The plateau happened entirely
45
+ inside "material" — no severity adjustment could have ended it. Ordering: system type
46
+ + goal → defect criterion → classification → defect? → severity → materiality → stop.
47
+
48
+ ## The criterion schema
49
+
50
+ One entry carries all of these. An empty cell makes it a hunch, not a criterion — do
51
+ not start the review on it.
52
+
53
+ | Field | Meaning |
54
+ |---|---|
55
+ | observer | whose eyes judge — client, caller, operator, a model reading the output |
56
+ | defect | what the observer must experience |
57
+ | classes | the enum, exactly ONE class stop-relevant; the others are relief valves that keep the stop class honest |
58
+ | evidence | what one finding must present |
59
+ | non-defects | defect-lookalikes excluded by this criterion — named explicitly |
60
+ | stop condition | what "0" means, counted over the stop class, and WHY it is reachable — how a growing surface is pinned |
61
+ | misclassification cost | which error is expensive — the direction to tell the reviewer to lean |
62
+ | goldens | ≥2 positive, ≥2 negative, ≥1 boundary; each with why, date, and provenance: `measured` (dated incident) or `constructed` (authored to pin a boundary) |
63
+
64
+ ## The catalog
65
+
66
+ Four system types. A starting set, not a census.
67
+
68
+ ### Web service / API surface (contract compliance)
69
+
70
+ - **Observer**: the HTTP client.
71
+ - **Defect**: a client today receives a response that differs from the contract — status code, envelope, stream terminal frames, the model actually run, isolation, resource caps.
72
+ - **Classes**: `behavioral_defect` (stop-relevant) · `coverage_gap` · `doc_gap`.
73
+ - **Evidence**: the concrete request and observed vs promised response.
74
+ - **Non-defects**: internal lifecycle (unless a request sequence shows two different responses), absence of a test, documentation wording.
75
+ - **Stop**: behavioral 0 — reachable: the code surface is finite and shrinks monotonically unless a fix creates a new behavioral defect.
76
+ - **Misclassification cost**: false negatives (a client actually breaks); but inflating coverage gaps into defects destroys the stop condition — audit category creep separately.
77
+ - **Goldens** (all `measured`, 2026-08-13–15):
78
+ - **+** `GET` on a nonexistent path returned 405, not 404 — an existing-method complaint attached to a path that does not exist.
79
+ - **+** a streaming endpoint's terminal delta carried only `output_tokens`, so cache/input token counts never reach a streaming client — non-streaming was correct, which is why nobody saw it.
80
+ - **−** "no test catches a one-line flip of this default": today's behavior is correct — `coverage_gap` here, a defect only under the library criterion.
81
+ - **−** child-process kill grace period — out of scope until a request sequence shows a client-visible difference; pins the observer to the client.
82
+ - **±** rejecting a documented-as-valid boundary value (compression 100, docs say "below 100 requires jpeg/webp") — the documentation is what makes it a defect; without that sentence it is taste.
83
+
84
+ ### Library / SDK / contract-first system
85
+
86
+ - **Observer**: the caller **and** the future maintainer.
87
+ - **Defect**: a promise violated today, or a promise unprotected — a one-line change breaks it and nothing catches it. The API criterion's `coverage_gap` is first-class here.
88
+ - **Classes**: `contract_defect` (stop-relevant) · `style_note` · `internal_change`.
89
+ - **Evidence**: for a violation, as above; for an unprotected promise, the promise sentence + the one-line change that breaks it + the absence of any catching test.
90
+ - **Non-defects**: style, internal structure.
91
+ - **Stop**: caution — this criterion self-refills while the contract grows. Declare "0" only over a surface whose growth has stopped; on a growing surface, narrow the stop to "every promise this change added is protected."
92
+ - **Misclassification cost**: balanced — miss an unprotected promise and the next refactor breaks it silently; over-report and the loop never ends.
93
+ - **Goldens**:
94
+ - **+** a streaming-image event test matched only `.completed`, so an edit stream misnamed `image_generation.completed` still passed — mutation survival showed the promise unprotected. (`measured`, 2026-08-13–15)
95
+ - **+** the contract promised "storage serves a large image at least once"; the code evicted it on the next request — resolved by fixing the **contract**: the disagreement is the defect, whichever side moves. (`measured`, 2026-08-13–15)
96
+ - **−** renaming an internal helper no document promises — the observer holds the contract; undocumented internals carry no promise. (`measured`, 2026-08-13–15)
97
+ - **−** a caller breaking on undocumented iteration order — no promise existed; the rename boundary pinned from the caller's side. (`constructed`, 2026-08-19)
98
+ - **±** an inventory-listed test turns out vacuous — asserts nothing about its promise. "A test exists" is a claim about names until the assertion is read; hand the reviewer "check the assertion, not the name." (`measured`, 2026-08-13–15)
99
+
100
+ ### AI workbench / harness (agent orchestration, review loops, verification pipelines)
101
+
102
+ - **Observer**: the operator, and any model consuming the harness's output.
103
+ - **Defect**: **a false signal that looks true** — a wrong PASS, a vacuous test, a silent fallback, a packet missing the call site, a wrong denominator. Being wrong is not the defect; being wrong while *looking right* is. Second form: a run claimed without provable dispatch — no receipt.
104
+ - **Classes**: `false_signal` (stop-relevant) · `detected_miss`. An unproven dispatch — a run or PASS claimed with no receipt — is classed `false_signal`, not given its own relief valve: a signal that cannot be shown true is counted false, or a round could declare completion while every dispatch stayed unproven.
105
+ - **Evidence**: the input on which the instrument gave a false verdict, plus the known correct answer. The standard probe: run the instrument against an input whose answer is known to be the opposite.
106
+ - **Non-defects**: one output's style; a false positive the harness itself **detected** — a caught error is the harness working.
107
+ - **Stop**: every PASS emitted this run survives a known-opposite check and evidences its own dispatch. The unit is "this run's signals are trustworthy," not a standing zero.
108
+ - **Misclassification cost**: false passes dominate. An instrument bug reporting failure dies in minutes because someone looks; one reporting success survives — a selection effect.
109
+ - **Goldens**:
110
+ - **+** a shell test runner received one nonexistent path (zsh does not word-split an unquoted variable), so every run exited 1 and every mutation reported KILLED — a green instrument that never ran its subject. (`measured`, 2026-08-13–15)
111
+ - **+** a test-inventory grep matched only `^test('...'` and missed every parameterized name; three reviewers judged a populated file empty — nothing tied the inventory's denominator to the source's own count. (`measured`, 2026-08-13–15)
112
+ - **+** a negative control kept passing after a faithful revert of the fix it was written against — a guard satisfied by an absence, a false PASS about the gate itself; found only by re-running reverts. (`measured`, 2026-08)
113
+ - **+** a review round returned "clean" with no receipt evidencing that the declared packet was dispatched on the exact seat — `false_signal` although nothing observed was wrong: what the stop refuses is absence of proof of dispatch, not proof of falsity. (`constructed`, 2026-08-19, pinned after a classification dispute at exactly this boundary)
114
+ - **−** one reviewer over-classed an item as material; another lens plus measurement filtered it — the harness caught it, so it worked: `detected_miss`, not a defect. (`measured`, 2026-08-13–15)
115
+ - **−** five review rounds returned 8 → 9 → 10 → 5 → 12 findings, refusing to converge — and every count was true. A truthful unpleasant signal is not a false signal; the defect lived in the undeclared criterion. (`measured`, 2026-08-16–17)
116
+ - **±** a surviving mutation proved equivalent — the platform already normalized what the mutated guard checked. Neither a harness defect nor a test gap; but a harness that auto-reads "survived = gap" has a defect in that rule. (`measured`, 2026-08-13–15)
117
+
118
+ ### Decision ontology (an ontology a model decides from)
119
+
120
+ - **Observer**: a model or agent deciding from the ontology alone.
121
+ - **Defect**: a representation that produces a wrong decision or blocks a right one: overlapping concept boundaries, a missing distinction the decision needs, an instance contradicting reality, a wrong relation direction or cardinality, a name implying what the definition does not say.
122
+ - **Classes**: `decision_defect` (stop-relevant) · `representation_note` · `out_of_scope_gap`.
123
+ - **Evidence**: a **decision scenario** — "answered from the ontology alone, this question yields X; reality is Y" — with the question, the path taken, and the ground truth.
124
+ - **Non-defects**: representation format, completeness as such, a question the model would get wrong without the ontology too.
125
+ - **Stop**: zero wrong decisions over the agreed scenario set. The set is the scope — fix it first, or this criterion self-refills like the library one.
126
+ - **Misclassification cost**: situational — feeding a hard gate makes false negatives expensive; exploratory aid makes false positives expensive. Filling this cell is mandatory at declaration.
127
+ - **Goldens** (all `constructed`, 2026-08-19, authored to pin boundaries the source loop's decision-scenario framing left open):
128
+ - **+** `Customer` and `Account` both define "the paying party"; a refund-routing question resolves through both paths — overlap is a decision defect even when each definition is individually correct.
129
+ - **+** `Order —hasOne→ Payment` while split payments exist — cardinality is a claim, and a false claim misleads the deciding model.
130
+ - **−** a verbose concept description — no decision changes; form is outside this observer's sight.
131
+ - **−** a domain absent that no scenario in the agreed set needs — completeness is scoped by the set, not the world.
132
+ - **±** the distinction exists but the model cannot find it (name or link missing) — with the observer fixed as "a model seeing only the ontology," unreachable is a representation defect, not a search defect. The observer clause decides the class.
133
+
134
+ ## Before any review
135
+
136
+ 1. Write one sentence: the system type and this work's goal. ("API surface — make the responses clients receive today match the contract." / "Harness — make this run's PASS signals trustworthy.")
137
+ 2. Pick a criterion from the catalog, or fill the schema fresh. A cell you cannot fill means the review does not start.
138
+ 3. Paste the goldens into the packet verbatim. Never the definition alone.
139
+ 4. Declare the stop condition and why it is reachable — or how the scope was pinned to make it so.
140
+ 5. Put the classification on the accepting channel (next section).
141
+ 6. Audit every round: reported class vs the class confirmed after measurement. A repeated disagreement is the next golden.
142
+
143
+ ## Enforcing the enum, by channel
144
+
145
+ Ranked by how much the channel refuses for you:
146
+
147
+ 1. **A route with a submit schema**: classification is a required enum field. The
148
+ measured precedent is anchors — 2,466 of 2,466 findings carried one, because the
149
+ schema refuses output without it. Where this channel exists, use it.
150
+ 2. **Prose-packet routes** (the shipped deep-review methods): the packet header
151
+ declares `Criterion: <name>` with the goldens pasted verbatim, and the dispatching
152
+ agent runs this fold procedure on what comes back:
153
+ 1. For each returned finding row, look up its class against the declared enum.
154
+ 2. A row carrying a class from the enum enters the findings ledger under that class.
155
+ 3. A row with no class, or a class outside the enum, is **not admitted**: send it
156
+ back once for classification, or record it as refused with the reason. Never
157
+ admit it unclassified, and never guess its class for it.
158
+ 4. Count the stop condition over the stop-relevant class only.
159
+ 5. Record the audit pair (reported class, confirmed class) for procedure step 6.
160
+
161
+ Stated honestly: on a prose route this is steering, not control — the fold is
162
+ performed by an agent following this guide, and nothing structural refuses a
163
+ class-less row for it. That is the known weaker result of a request-only rule.
164
+ 3. A structural criterion slot in the review dispatch machinery is deliberately not
165
+ part of this guide: a criterion is per-review while dispatch config is per-launch,
166
+ and a slot with no schema-backed consumer would be an inert value. Until such a
167
+ channel exists, ranks 1–2 are the real ones.
168
+
169
+ ## Golden lifecycle
170
+
171
+ - **Admission**: only a golden that decides a boundary the definition leaves open,
172
+ evidenced by a recorded classification disagreement or an audited misclassification;
173
+ provenance labeled `measured` or `constructed`, each with its why and date.
174
+ - **Overturn**: only by a measured counterexample. Correct the golden in place — this
175
+ guide describes the present — plus a dated decision record, in whatever channel the
176
+ repo keeps decisions, naming the closed golden and the counterexample. Silent
177
+ deletion is forbidden: a wrong golden makes reviewers systematically wrong, and an
178
+ untracked fix hides that it ever did.
179
+
180
+ ## Declaring, switching, and mixed packets
181
+
182
+ - Every dispatched packet names its criterion. A harness round declares `AI harness`;
183
+ a round whose system type has no catalog entry declares a **task-local criterion**,
184
+ written in the packet itself, conforming to the schema above and labeled task-local
185
+ — repeated use is what earns a catalog entry.
186
+ - A mixed packet is split into one packet per criterion. Two observers in one
187
+ trajectory produce findings no single stop condition can count.
188
+ - When a criterion changes mid-loop: re-classify only the findings still open; rounds
189
+ already recorded are dated history. Never splice pre- and post-switch counts into
190
+ one series — they count different things.
191
+
192
+ ## Evidence base
193
+
194
+ Derived from a 39-round adversarial review loop (3 lenses × a frontier reviewer) on a
195
+ peer OAuth CLI-API adapter, 2026-08-13–15: the same codebase plateaued at
196
+ 17→19→21→23→19 findings under a self-refilling criterion and resumed falling
197
+ (behavioral 10→5) the round the classes were split and the stop counted over
198
+ `behavioral_defect` alone. The boundary evidence is from the same loop: a packet
199
+ instruction saying "do not inflate coverage gaps" still left two lenses classing one
200
+ finding `doc_gap` and `behavioral_defect`, both defensibly — one golden would have
201
+ decided it. The non-convergence golden (8→9→10→5→12, every count true) is this
202
+ environment's own launcher-review loop, 2026-08-16–17, run under an undeclared
203
+ "material" criterion; the two series are two populations under two criteria and are
204
+ cited separately for exactly the reason the migration section gives. Re-derive when
205
+ the review routes or the bound models change.
@@ -26,7 +26,11 @@ verification_focus:
26
26
  This guide is a scoped extension of the global Coding Guidelines. Use it when
27
27
  composing what you ask a reviewer for — the request, the evidence bar, the
28
28
  verdict shape. It does not cover when to review, how deep, or what counts as
29
- material (the severity ladder and review loop in `${CODEX_HOME:-$HOME/.codex}/guides/coding-staged-workflow.md` own that),
29
+ material — that last question splits in two: the severity ladder and review loop in
30
+ `${CODEX_HOME:-$HOME/.codex}/guides/coding-staged-workflow.md` own *how bad*,
31
+ while whether something is a defect at all, of which class, and what "zero" is
32
+ counted over is the defect criterion, owned by
33
+ `${CODEX_HOME:-$HOME/.codex}/guides/review-defect-criteria.md`. Nor
30
34
  which reviewer kind to route to — the convergence heuristic in
31
35
  `${CODEX_HOME:-$HOME/.codex}/guides/verification-discipline.md` owns that. Phrasing a prompt for a specific model
32
36
  family is out of scope here; where that guidance ships, the rule that needs it
@@ -38,6 +42,16 @@ is a per-model claim; these are the failures that persist regardless of who
38
42
  reviews. Every rule names the evidence behind it, because a review guide that
39
43
  asserts without evidence would fail its own bar.
40
44
 
45
+ ## Declare the criterion
46
+
47
+ Before composing anything else, name the defect criterion the review runs under —
48
+ the per-system-type definition of what a defect is, its class enum, and what "zero"
49
+ is counted over. `${CODEX_HOME:-$HOME/.codex}/guides/review-defect-criteria.md`
50
+ owns choosing it; this guide assumes one is declared. Put `Criterion: <name>` at the
51
+ top of the packet with that criterion's goldens pasted verbatim — a definition
52
+ without goldens does not classify consistently at the boundary, and an undeclared
53
+ criterion means every reviewer substitutes its own.
54
+
41
55
  ## Demand a failure path, not a gap
42
56
 
43
57
  The dominant reviewer failure is not hallucination. Across 372 rejected
@@ -272,11 +272,18 @@ def replace_atomically(path, text):
272
272
  learn/migrate-learnings.py has had this discipline and states the reason; it now shares
273
273
  this one implementation rather than keeping a second. The temp file is a sibling so the
274
274
  replace stays on one filesystem, where os.replace is atomic.
275
+
276
+ An existing target's permission bits ride through the swap: the temp is born with
277
+ umask mode, so replacing a user-restricted file — a 0600 AGENTS.md — silently widened
278
+ it to world-readable. Ownership is not copied; this never runs with the privilege to
279
+ change it, and a same-owner rename keeps it anyway.
275
280
  """
276
281
  path.parent.mkdir(parents=True, exist_ok=True)
277
282
  tmp = path.with_name(path.name + ".tmp-agent-bios")
278
283
  try:
279
284
  tmp.write_text(text, encoding="utf-8")
285
+ if path.exists():
286
+ tmp.chmod(path.stat().st_mode & 0o777)
280
287
  os.replace(tmp, path)
281
288
  finally:
282
289
  if tmp.exists():
@@ -100,6 +100,7 @@
100
100
  "llm-capability-boundary-patterns.md": {"tier": "domain", "domains": ["llm-pipeline-dev"]},
101
101
  "llm-capability-boundary.md": {"tier": "domain", "domains": ["llm-pipeline-dev"]},
102
102
  "mock-realization-boundary.md": {"tier": "domain", "domains": ["builder-base"]},
103
+ "review-defect-criteria.md": {"tier": "domain", "domains": ["builder-base"]},
103
104
  "review-request.md": {"tier": "domain", "domains": ["builder-base"]},
104
105
  "session-distill-workflow.md": {"tier": "infra", "domains": []},
105
106
  "learning-flow.md": {"tier": "infra", "domains": []},
@@ -3021,9 +3021,10 @@ def _sha256_file(path: str) -> str:
3021
3021
  return digest.hexdigest()
3022
3022
 
3023
3023
 
3024
- def publish_atomically(target: pathlib.Path, text: str) -> None:
3025
- """Write `text` at `target` so a reader sees the old file or the complete new one, and
3026
- a failure leaves neither a partial file nor a temporary.
3024
+ def publish_atomically(target: pathlib.Path, payload: str | bytes,
3025
+ mode: int | None = None) -> None:
3026
+ """Write `payload` at `target` so a reader sees the old file or the complete new one,
3027
+ and a failure leaves neither a partial file nor a temporary.
3027
3028
 
3028
3029
  `write_text` creates the FINAL name and then fills it, so a concurrent reader — and
3029
3030
  the receipt fold globs exactly its directory — could observe a truncated record, and a
@@ -3036,10 +3037,17 @@ def publish_atomically(target: pathlib.Path, text: str) -> None:
3036
3037
  the preset save wrote its temporary and published it with no `try`, so an `os.replace`
3037
3038
  that failed left a complete temporary file beside the untouched preset file, for the
3038
3039
  next save's glob or the next reader to find (spec round 2, #6). The removal never
3039
- masks the error that caused it."""
3040
+ masks the error that caused it. The registration wizard was the third copy, with
3041
+ neither the boundary nor the cleanup (writable-scratch round, #6) — it publishes
3042
+ BYTES it snapshotted and re-applies the file's prior mode, hence the two optionals."""
3040
3043
  temporary = target.with_name(f".{target.name}.{os.getpid()}.tmp")
3041
3044
  try:
3042
- temporary.write_text(text, encoding="utf-8")
3045
+ if isinstance(payload, bytes):
3046
+ temporary.write_bytes(payload)
3047
+ else:
3048
+ temporary.write_text(payload, encoding="utf-8")
3049
+ if mode is not None:
3050
+ temporary.chmod(mode)
3043
3051
  os.replace(temporary, target)
3044
3052
  except BaseException:
3045
3053
  try:
@@ -5303,32 +5311,39 @@ def _trial_registration(
5303
5311
  through the genuine load_config — same merge order, same validation, same
5304
5312
  collision refusal. Returns None on acceptance, the reader's message on refusal."""
5305
5313
  trial = pathlib.Path(tempfile.mkdtemp(prefix="agent-launch-register-"))
5306
- shutil.copy(config_path, trial / config_path.name)
5307
- for sibling in (USER_PRESETS_NAME, USER_LAUNCHER_NAME):
5308
- source = config_path.with_name(sibling)
5309
- if source.is_file():
5310
- shutil.copy(source, trial / sibling)
5311
- methods_source = user_methods_path(config_path)
5312
- existing = methods_source.read_text(encoding="utf-8") if methods_source.is_file() else USER_METHODS_HEADER
5313
- candidate = trial / USER_METHODS_NAME
5314
- try:
5315
- candidate.write_text(existing + block, encoding="utf-8")
5316
- except OSError as exc:
5317
- # A read-only config dir or a full disk lost every answer the
5318
- # user had just typed. Reported like any other refusal instead.
5319
- raise LaunchError(f"cannot write {candidate}: {exc}") from exc
5320
- candidate.chmod(0o600)
5314
+ # One finally over EVERYTHING after the mkdtemp — the copies, the candidate
5315
+ # write, both readers: the trial is a scratch workspace, and every exit used
5316
+ # to leave it behind, one directory per attempt, for the life of the temp
5317
+ # area (writable-scratch round, #7).
5321
5318
  try:
5322
- trial_config = load_config(trial / config_path.name)
5323
- # The REAL pipeline is both readers, not the first one. load_config accepts a
5324
- # method whose instructions name a slot core does not provide; load_review_methods
5325
- # — the reader every launch actually goes through — refuses it. Trialling only the
5326
- # first wrote the block, told the user it was live, and left the refusal for the
5327
- # next launch. A trial that does not run the reader that decides is not a trial.
5328
- load_review_methods(trial_config)
5329
- except LaunchError as exc:
5330
- return str(exc)
5331
- return None
5319
+ shutil.copy(config_path, trial / config_path.name)
5320
+ for sibling in (USER_PRESETS_NAME, USER_LAUNCHER_NAME):
5321
+ source = config_path.with_name(sibling)
5322
+ if source.is_file():
5323
+ shutil.copy(source, trial / sibling)
5324
+ methods_source = user_methods_path(config_path)
5325
+ existing = methods_source.read_text(encoding="utf-8") if methods_source.is_file() else USER_METHODS_HEADER
5326
+ candidate = trial / USER_METHODS_NAME
5327
+ try:
5328
+ candidate.write_text(existing + block, encoding="utf-8")
5329
+ except OSError as exc:
5330
+ # A read-only config dir or a full disk lost every answer the
5331
+ # user had just typed. Reported like any other refusal instead.
5332
+ raise LaunchError(f"cannot write {candidate}: {exc}") from exc
5333
+ candidate.chmod(0o600)
5334
+ try:
5335
+ trial_config = load_config(trial / config_path.name)
5336
+ # The REAL pipeline is both readers, not the first one. load_config accepts a
5337
+ # method whose instructions name a slot core does not provide; load_review_methods
5338
+ # — the reader every launch actually goes through — refuses it. Trialling only the
5339
+ # first wrote the block, told the user it was live, and left the refusal for the
5340
+ # next launch. A trial that does not run the reader that decides is not a trial.
5341
+ load_review_methods(trial_config)
5342
+ except LaunchError as exc:
5343
+ return str(exc)
5344
+ return None
5345
+ finally:
5346
+ shutil.rmtree(trial, ignore_errors=True)
5332
5347
 
5333
5348
 
5334
5349
  def register_reviewer_wizard(
@@ -5550,60 +5565,67 @@ def register_reviewer_wizard(
5550
5565
  back_hint=t("corpus.back.hint"))
5551
5566
  return False
5552
5567
  mode = (write_target.stat().st_mode & 0o777) if existed else 0o600
5553
- temporary = write_target.with_name(
5554
- f".{write_target.name}.{os.getpid()}.tmp"
5555
- )
5556
- temporary.write_bytes(before + block.encode())
5557
- temporary.chmod(mode)
5558
- os.replace(temporary, write_target)
5559
- # Unreachable while the trial is the real pipeline — and
5560
- # load-bearing exactly when it is not: a post-write failure must
5561
- # restore the pre-write state (bytes, mode, or ABSENCE) and say
5562
- # so, never leave a corrupt registry behind a green screen.
5563
5568
  try:
5564
- fresh = load_config(config_path)
5565
- except LaunchError:
5566
- fresh = None
5567
- if fresh is None or answers["id"] not in load_review_methods(fresh):
5568
- # Restore ONLY when the file still holds exactly our write:
5569
- # unconditional rollback would destroy an edit that landed in
5570
- # the meantime; if the bytes moved, the human owns the merge.
5571
- current = (
5572
- write_target.read_bytes() if write_target.is_file() else b""
5573
- )
5574
- if current != before + block.encode():
5575
- # Its own sentence, because this branch deliberately does NOT
5576
- # roll back someone else's edit landed and the human owns the
5577
- # merge. Sharing the restore branch's message told the user the
5578
- # file had been restored in the one case where it was left
5579
- # exactly as the other writer wrote it.
5580
- _corpus_info(
5581
- ui, t("wizard.title"), [t("wizard.postwrite.raced.line")],
5582
- back_hint=t("corpus.back.hint"),
5583
- )
5584
- return False
5585
- if existed:
5586
- restore = write_target.with_name(
5587
- f".{write_target.name}.{os.getpid()}.restore"
5569
+ # The shared primitive owns the temporary's lifecycle: on any
5570
+ # failure it removes its own temp and re-raises. A directory at
5571
+ # the target, a permission flip, a full disk — each used to
5572
+ # escape here as a raw OSError, PAST the retained-answer loop,
5573
+ # with the completed temporary left beside the untouched file.
5574
+ # Publication failure is a refusal like any other: the reader's
5575
+ # loop keeps the answers, and the screen shows the OS's words.
5576
+ publish_atomically(write_target, before + block.encode(), mode)
5577
+ except OSError as exc:
5578
+ verdict = f"cannot write {write_target}: {exc}"
5579
+ if verdict is None:
5580
+ # Unreachable while the trial is the real pipeline — and
5581
+ # load-bearing exactly when it is not: a post-write failure must
5582
+ # restore the pre-write state (bytes, mode, or ABSENCE) and say
5583
+ # so, never leave a corrupt registry behind a green screen.
5584
+ try:
5585
+ fresh = load_config(config_path)
5586
+ except LaunchError:
5587
+ fresh = None
5588
+ if fresh is None or answers["id"] not in load_review_methods(fresh):
5589
+ # Restore ONLY when the file still holds exactly our write:
5590
+ # unconditional rollback would destroy an edit that landed in
5591
+ # the meantime; if the bytes moved, the human owns the merge.
5592
+ current = (
5593
+ write_target.read_bytes() if write_target.is_file() else b""
5588
5594
  )
5589
- restore.write_bytes(before)
5590
- restore.chmod(mode)
5591
- os.replace(restore, write_target)
5592
- else:
5593
- write_target.unlink(missing_ok=True)
5594
- _corpus_info(ui, t("wizard.title"), [t("wizard.postwrite.line")],
5595
- back_hint=t("corpus.back.hint"))
5596
- return False # pre-write state restored above
5597
- config.clear()
5598
- config.update(fresh)
5599
- registry.clear()
5600
- registry.update(load_review_methods(config))
5601
- _corpus_info(
5602
- ui, t("wizard.title"),
5603
- [t("wizard.done.line").format(method_id=answers["id"])],
5604
- back_hint=t("corpus.back.hint"),
5605
- )
5606
- return True
5595
+ if current != before + block.encode():
5596
+ # Its own sentence, because this branch deliberately does NOT
5597
+ # roll back — someone else's edit landed and the human owns the
5598
+ # merge. Sharing the restore branch's message told the user the
5599
+ # file had been restored in the one case where it was left
5600
+ # exactly as the other writer wrote it.
5601
+ _corpus_info(
5602
+ ui, t("wizard.title"), [t("wizard.postwrite.raced.line")],
5603
+ back_hint=t("corpus.back.hint"),
5604
+ )
5605
+ return False
5606
+ if existed:
5607
+ restore = write_target.with_name(
5608
+ f".{write_target.name}.{os.getpid()}.restore"
5609
+ )
5610
+ restore.write_bytes(before)
5611
+ restore.chmod(mode)
5612
+ os.replace(restore, write_target)
5613
+ else:
5614
+ write_target.unlink(missing_ok=True)
5615
+ _corpus_info(ui, t("wizard.title"), [t("wizard.postwrite.line")],
5616
+ back_hint=t("corpus.back.hint"))
5617
+ return False # pre-write state restored above
5618
+ if verdict is None:
5619
+ config.clear()
5620
+ config.update(fresh)
5621
+ registry.clear()
5622
+ registry.update(load_review_methods(config))
5623
+ _corpus_info(
5624
+ ui, t("wizard.title"),
5625
+ [t("wizard.done.line").format(method_id=answers["id"])],
5626
+ back_hint=t("corpus.back.hint"),
5627
+ )
5628
+ return True
5607
5629
  action = choose(
5608
5630
  t("wizard.refused.title"),
5609
5631
  [
@@ -6140,21 +6162,25 @@ def preset_from_plan(
6140
6162
  f"is not an effort — saving would drop it and {other} would reload at "
6141
6163
  f"its host default. Fix that entry in the launch profile and save again."
6142
6164
  )
6165
+ # A non-table where the normalized home would go — `tier_overrides.<host>` or
6166
+ # its `frontier` entry authored as a value. The serializer now carries such a
6167
+ # value VERBATIM (spec round 7, #3), so this normalizer cannot write into it
6168
+ # (`setdefault("frontier", {})` on a string was round 24, #8's raw TypeError)
6169
+ # and cannot skip past it either: skipping used to be safe only because the
6170
+ # renderer refused the block, and with that refusal gone a skip would drop
6171
+ # the authored `frontier_effort` in silence. When the authored effort is that
6172
+ # host's default there is nothing to write and the carry is faithful; when it
6173
+ # is not, the home is genuinely occupied and the save refuses naming both —
6174
+ # the one case D-20260817-105da4 keeps a named refusal for.
6143
6175
  block = scoped.get(other)
6176
+ occupied = None
6177
+ existing = None
6144
6178
  if block is not None and not isinstance(block, dict):
6145
- # Left exactly as authored; the serializer names it rather than this
6146
- # silently repairing a shape it is not the owner of.
6147
- continue
6148
- existing = (block or {}).get("frontier")
6149
- if existing is not None and not isinstance(existing, dict):
6150
- # Left exactly as authored, for the reason the host-block branch above says
6151
- # in the same words: the serializer owns unwritable shapes and names them.
6152
- # This normalizer wrote into the block regardless, so `setdefault("frontier",
6153
- # {})` handed back the STRING and `["effort"] =` left Save as a raw
6154
- # TypeError — past the named boundary its workhorse twin arrives through
6155
- # (round 24, #8). Nothing is dropped by skipping: the renderer's `_table`
6156
- # door refuses this very entry by name a moment later.
6157
- continue
6179
+ occupied = f"tier_overrides.{other}"
6180
+ else:
6181
+ existing = (block or {}).get("frontier")
6182
+ if existing is not None and not isinstance(existing, dict):
6183
+ occupied = f"tier_overrides.{other}.frontier"
6158
6184
  if isinstance(existing, dict) and existing.get("effort") not in (None, effort):
6159
6185
  # The contradiction build_plan already refuses, reached from the host that
6160
6186
  # does not launch it. Named here rather than resolved: picking one of two
@@ -6179,6 +6205,19 @@ def preset_from_plan(
6179
6205
  .get("frontier", {}).get("effort")
6180
6206
  ):
6181
6207
  continue
6208
+ if occupied is not None:
6209
+ # AFTER the default elision on purpose, unlike the contradiction above: a
6210
+ # non-table home states no effort to contradict, so a default-valued
6211
+ # authoring writes no line and reloads faithfully. A non-default one has
6212
+ # exactly one home and that home is occupied by a value the save carries
6213
+ # verbatim — writing both is impossible, and dropping either is the
6214
+ # silence S4 forbids.
6215
+ raise LaunchError(
6216
+ f"preset {name!r}: {other} authors frontier_effort={effort!r}, whose "
6217
+ f"saved home is tier_overrides.{other}.frontier.effort, and {occupied} "
6218
+ f"holds a non-table value the save carries verbatim — it cannot write "
6219
+ f"both. Remove one of them in the launch profile and save again."
6220
+ )
6182
6221
  scoped.setdefault(other, {}).setdefault("frontier", {})["effort"] = effort
6183
6222
  return fields, scoped, review_block
6184
6223
 
@@ -6270,11 +6309,11 @@ def render_preset_block(
6270
6309
  tier_overrides: dict[str, Any],
6271
6310
  review_block: dict[str, Any] | None = None,
6272
6311
  ) -> str:
6273
- # One door for every table this writes back, hoisted here from inside the review-arm
6274
- # loop so the tier overrides get the same named boundary. Save failures are contracted
6275
- # to arrive as LaunchError at the settings hub, and the inactive-host overrides had no
6276
- # boundary at all: a scalar host block was silently FILTERED OUT before it arrived, and
6277
- # a scalar tier block reached `.items()` as a raw AttributeError (round 22, #9).
6312
+ # One door for every place only a table can stand an arm's `methods`, its `base`,
6313
+ # each binding. Save failures are contracted to arrive as LaunchError at the settings
6314
+ # hub, never a raw escape or a silent filter (round 22, #9). The tier overrides and
6315
+ # whole review arms no longer pass through it: a non-table node there is not
6316
+ # malformed, it is a VALUE its parent table spells (spec round 7, #2/#3).
6278
6317
  def _table(value, where):
6279
6318
  if not isinstance(value, dict):
6280
6319
  raise LaunchError(
@@ -6287,51 +6326,58 @@ def render_preset_block(
6287
6326
  lines = [f"[presets.{_toml_key(name)}]"]
6288
6327
  for key, value in fields.items():
6289
6328
  lines.append(f"{key} = {_toml_scalar(value)}")
6290
- for host, tiers in tier_overrides.items():
6291
- for tier, override in _table(tiers, f"tier_overrides.{host}").items():
6292
- # Through the SAME recursive emitter the review arms use. This was a flat loop
6293
- # over one level, so an inactive override holding a sub-table reached
6294
- # `_toml_scalar`, which gives a bare table no spellinga profile the launcher
6295
- # accepts died at Save with `cannot serialize preset value: {'nested': 1}`,
6296
- # naming neither the host, the tier, nor the key (spec round 3, #6). Verbatim
6297
- # carry has no depth limit here for the reason it has none in an arm (round 24,
6298
- # #7), and the emitter puts every leaf through `_toml_key` round 24 #10's half
6299
- # and every leaf VALUE through `_arm_scalar`, so a genuinely unwritable entry
6300
- # is refused naming `tier_overrides.<host>.<tier>.<key>` rather than its repr.
6301
- _emit_arm_table(
6302
- lines,
6303
- f"presets.{_toml_key(name)}.tier_overrides"
6304
- f".{_toml_key(host)}.{_toml_key(tier)}",
6305
- _table(override, f"tier_overrides.{host}.{tier}"),
6306
- name, f"tier_overrides.{host}.{tier}",
6307
- )
6329
+ if tier_overrides:
6330
+ # The whole tree through the SAME recursive emitter the review arms use, from the
6331
+ # `tier_overrides` root down. Verbatim carry has no depth limit (spec round 3, #6;
6332
+ # round 24, #7) and no FLOOR either: a non-table node at any depth —
6333
+ # `tier_overrides.claude = "x"`, or `…codex.workhorse = [1, 2]`is TOML the
6334
+ # launcher accepts, since only the active host's blocks are validated at load,
6335
+ # and TOML spells it perfectly well as a value in its parent table; refusing it
6336
+ # at Save made an accepted profile unsaveable, which is S4's complement (spec
6337
+ # round 7, #3 the per-tier loop this replaces held a table door at exactly the
6338
+ # two depths the emitter now writes as values). The active host's blocks arrive
6339
+ # normalized from `preset_from_plan`, so for them this emits the same
6340
+ # `[…tier_overrides.<host>.<tier>]` tables it always did; every leaf key goes
6341
+ # through `_toml_key` and every leaf value through `_arm_scalar`, so a genuinely
6342
+ # unwritable entry is refused naming its full `tier_overrides.…` path.
6343
+ _emit_arm_table(
6344
+ lines, f"presets.{_toml_key(name)}.tier_overrides",
6345
+ tier_overrides, name, "tier_overrides",
6346
+ )
6308
6347
  if review_block is not None:
6309
6348
  # One sub-table per binding, and the method id is quoted: it is a user-chosen
6310
6349
  # name, so a bare key would break on anything a TOML bare key cannot hold.
6311
6350
  # Adding method N therefore appends exactly one table and touches nothing else.
6312
6351
  arms = review_block.get(REVIEW_ARMS_KEY)
6352
+ # A whole arm authored as a VALUE — `hosts.codex = [1, 2]` — is TOML the launcher
6353
+ # accepts, because only the launching arm is parsed, and TOML writes it back as a
6354
+ # value in the parent hosts table; so that is what this does, rather than the
6355
+ # refusal that made an accepted profile unsaveable (spec round 7, #2). Emitted
6356
+ # before the table arms for the reason every emitter here puts scalars first; a
6357
+ # value with no TOML form at all is refused through `_arm_scalar`, naming
6358
+ # `review.hosts.<host>`.
6359
+ value_arms = (
6360
+ {host: arm for host, arm in arms.items() if not isinstance(arm, dict)}
6361
+ if arms else {}
6362
+ )
6363
+ if value_arms:
6364
+ lines.append("")
6365
+ lines.append(f"[presets.{_toml_key(name)}.review.{REVIEW_ARMS_KEY}]")
6366
+ for host in sorted(value_arms, key=_toml_key):
6367
+ lines.append(
6368
+ f"{_toml_key(host)} = "
6369
+ f"{_arm_scalar(value_arms[host], name, f'review.{REVIEW_ARMS_KEY}.{host}')}"
6370
+ )
6313
6371
  for prefix, block in (
6314
6372
  sorted((f"review.{REVIEW_ARMS_KEY}.{_toml_key(host)}", arm)
6315
- for host, arm in arms.items())
6373
+ for host, arm in arms.items() if isinstance(arm, dict))
6316
6374
  if arms else [("review", review_block)]
6317
6375
  ):
6318
- # An untouched arm is written back exactly as authored, and "as authored"
6319
- # includes shapes this cannot serialize: a scalar where a table belongs made
6320
- # `"base" in block` a raw TypeError out of Save, after the launch had already
6321
- # happened. Save failures are contracted to arrive as LaunchError at the
6322
- # settings hub, so this one does too, naming the arm to look at.
6323
- if not isinstance(block, dict):
6324
- raise LaunchError(
6325
- f"preset {name!r}: the review arm at {prefix!r} is "
6326
- f"{type(block).__name__}, not a table — it cannot be written back. "
6327
- f"Fix that entry in the launch profile and save again."
6328
- )
6329
6376
  # `.get`, because an untouched arm is written back exactly as authored and
6330
6377
  # a raw block need not carry both keys — only the edited arm is re-derived.
6331
- # The nested shapes get the same door as the arm itself: `base`, `methods` and
6332
- # each binding are tables or the save names the entry (round 19, #11) the
6333
- # outer check above caught a scalar arm and let `.items()` on a scalar
6334
- # `methods` escape as AttributeError past the same boundary.
6378
+ # The nested shapes each get a door: `base`, `methods` and each binding are
6379
+ # tables or the save names the entry (round 19, #11), which once escaped as
6380
+ # a raw AttributeError from `.items()` on a scalar `methods`.
6335
6381
  methods = _table(block.get("methods", {}), f"{prefix}.methods")
6336
6382
  # Everything else the arm carries. An untouched arm is promised back VERBATIM
6337
6383
  # and this serializer knew exactly two keys, so any other top-level field the
@@ -7799,14 +7845,17 @@ def child_agent_registrations(
7799
7845
  """THE Codex child-registration projection — `(tier, description, config path, config
7800
7846
  content)` per spawnable tier, in the order the backend receives them.
7801
7847
 
7802
- Pure: it reads the authored templates and derives, it writes nothing. One value, three
7803
- consumers — `run_contract` states it, `codex_agent_configs` materialises the content at
7804
- the path, and `project_args` writes the description and that path into argv — for the
7805
- same reason `review_mcp_servers` is one value: the template a tier resolves to decides
7806
- BOTH halves of what the backend is handed, and neither reached the contract. Repointing
7807
- one tier at another tier's template therefore changed the child's description and its
7808
- config path under a byte-identical contract, which is the one thing the contract's own
7809
- tail may not be false about (spec round 6, L7).
7848
+ Pure in what it writes nothing but not in what it reads: the authored template
7849
+ files and `XDG_CACHE_HOME`. One value, three consumers — `run_contract` states it,
7850
+ `codex_agent_configs` materialises the content at the path, and `project_args` writes
7851
+ the description and that path into argv for the same reason `review_mcp_servers` is
7852
+ one value: the template a tier resolves to decides BOTH halves of what the backend is
7853
+ handed, and neither reached the contract. Repointing one tier at another tier's
7854
+ template therefore changed the child's description and its config path under a
7855
+ byte-identical contract, which is the one thing the contract's own tail may not be
7856
+ false about (spec round 6, L7). And one CALL: `project_args` computes this once and
7857
+ hands the same list to all three, because a projection that rereads its sources gives
7858
+ each extra call a chance to answer differently (spec round 7, L7).
7810
7859
 
7811
7860
  The path is the config's IDENTITY, not an incidental location: its directory is a
7812
7861
  digest of the whole rendered set, so any edit to any template moves every path — and
@@ -7883,11 +7932,16 @@ def child_agent_registrations(
7883
7932
 
7884
7933
 
7885
7934
  def codex_agent_configs(
7886
- plan: dict[str, Any], materialize: bool
7935
+ plan: dict[str, Any], materialize: bool,
7936
+ registrations: list[tuple[str, str, pathlib.Path, str]] | None = None,
7887
7937
  ) -> dict[str, tuple[pathlib.Path, str]]:
7888
7938
  """The registration projection, written to disk. Everything decided is decided above;
7889
- this adds only the bytes and the failure modes writing them has."""
7890
- registrations = child_agent_registrations(plan)
7939
+ this adds only the bytes and the failure modes writing them has. A caller that also
7940
+ renders the contract passes the projection it rendered, so what is materialised is
7941
+ the value the contract states rather than a second computation of it (spec round 7,
7942
+ L7)."""
7943
+ if registrations is None:
7944
+ registrations = child_agent_registrations(plan)
7891
7945
  cache_root = registrations[0][2].parent
7892
7946
  # Every filesystem error here becomes a LaunchError, because `materialize` is exactly
7893
7947
  # the difference between --dry-run and a real launch: the projection skips these writes
@@ -8063,7 +8117,18 @@ def _cross_review_route(
8063
8117
  return " ".join(parts)
8064
8118
 
8065
8119
 
8066
- def run_contract(plan: dict[str, Any]) -> str:
8120
+ def run_contract(
8121
+ plan: dict[str, Any],
8122
+ mcp_registrations: list[tuple[str, str, list[str]]] | None = None,
8123
+ child_registrations: list[tuple[str, str, pathlib.Path, str]] | None = None,
8124
+ ) -> str:
8125
+ """The launch contract. A caller that also builds argv — `project_args` — passes the
8126
+ MCP and child registration projections it computed, so this text and that argv are
8127
+ generated from ONE call of each producer: the producers reread template files, the
8128
+ environment and PATH, so calling them once per consumer let a mid-launch change put
8129
+ one value here and another in argv under a byte-identical contract (spec round 7,
8130
+ L7). A caller that renders only the contract omits them and the projections are
8131
+ computed here — there is no argv beside the text to diverge from."""
8067
8132
  main_tier = plan["main_tier"]
8068
8133
  inactive = inactive_tiers(plan)
8069
8134
  if plan["delegation"]:
@@ -8152,7 +8217,9 @@ def run_contract(plan: dict[str, Any]) -> str:
8152
8217
  # Rendered from the whole triple, quoted as JSON, because reconciling this against argv
8153
8218
  # is the point: a command or an argument carrying a space would otherwise read as two.
8154
8219
  # Empty on every route that registers nothing, which includes every legacy one.
8155
- registrations = review_mcp_servers(plan)
8220
+ registrations = (
8221
+ review_mcp_servers(plan) if mcp_registrations is None else mcp_registrations
8222
+ )
8156
8223
  mcp_clause = ""
8157
8224
  if registrations:
8158
8225
  rendered = "; ".join(
@@ -8181,7 +8248,11 @@ def run_contract(plan: dict[str, Any]) -> str:
8181
8248
  if plan["delegation"] and plan["host"] == "codex":
8182
8249
  rendered_children = "; ".join(
8183
8250
  f"{tier} = {json.dumps(description)} at {json.dumps(str(path))}"
8184
- for tier, description, path, _ in child_agent_registrations(plan)
8251
+ for tier, description, path, _ in (
8252
+ child_agent_registrations(plan)
8253
+ if child_registrations is None
8254
+ else child_registrations
8255
+ )
8185
8256
  )
8186
8257
  child_clause = (
8187
8258
  f"Codex child agent registrations, in the order the backend receives them "
@@ -8340,9 +8411,12 @@ def review_mcp_servers(plan: dict[str, Any]) -> list[tuple[str, str, list[str]]]
8340
8411
 
8341
8412
  THE registration projection, and the only one: `run_contract` states this value and
8342
8413
  both argv builders write it, so the contract and the args cannot describe different
8343
- servers. It carries the args as well as the name and command because a triple is what
8344
- each backend receives leaving them to a constant read at the argv site put a third
8345
- of the registration outside the value the contract is generated from.
8414
+ servers and `project_args` computes it ONCE, handing the same list to the contract
8415
+ and to argv, because command resolution rereads PATH and a second call is a second
8416
+ answer (spec round 7, L7). It carries the args as well as the name and command
8417
+ because a triple is what each backend receives — leaving them to a constant read at
8418
+ the argv site put a third of the registration outside the value the contract is
8419
+ generated from.
8346
8420
 
8347
8421
  A rename here is a real change to what runs: the name below becomes the backend's
8348
8422
  `mcp_servers.<name>` namespace, which is how a tool call addresses the server."""
@@ -8565,7 +8639,21 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
8565
8639
  return []
8566
8640
  main = plan["tiers"][plan["main_tier"]]
8567
8641
  main_effort = tier_effort(plan, plan["main_tier"])
8568
- contract = run_contract(plan)
8642
+ # Each registration projection is computed ONCE and every consumer below receives the
8643
+ # same value. One producer was not enough: the contract render and the argv builders
8644
+ # each CALLED it, and the producers reread template files, `XDG_CACHE_HOME` and PATH —
8645
+ # so a change landing between the two calls put one value in the contract and another
8646
+ # in argv, under a tail that promises what is described is what runs (spec round 7,
8647
+ # L7). The child projection is computed exactly where both of its consumers live:
8648
+ # codex argv with delegation on, and the contract's child clause, which renders under
8649
+ # the same condition.
8650
+ mcp_registrations = review_mcp_servers(plan)
8651
+ child_registrations = (
8652
+ child_agent_registrations(plan)
8653
+ if plan["delegation"] and host == "codex"
8654
+ else None
8655
+ )
8656
+ contract = run_contract(plan, mcp_registrations, child_registrations)
8569
8657
  if host == "codex":
8570
8658
  args = [
8571
8659
  "--model", main["model"],
@@ -8582,12 +8670,14 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
8582
8670
  policy_args = ["--sandbox", policy]
8583
8671
  args += policy_args
8584
8672
  if plan["delegation"]:
8585
- for tier, (path, description) in codex_agent_configs(plan, materialize_agents).items():
8673
+ for tier, (path, description) in codex_agent_configs(
8674
+ plan, materialize_agents, child_registrations
8675
+ ).items():
8586
8676
  args += [
8587
8677
  "-c", f"agents.{tier}.description={json.dumps(description)}",
8588
8678
  "-c", f"agents.{tier}.config_file={json.dumps(str(path))}",
8589
8679
  ]
8590
- for name, command, server_args in review_mcp_servers(plan):
8680
+ for name, command, server_args in mcp_registrations:
8591
8681
  args += [
8592
8682
  "-c", f"mcp_servers.{name}.enabled=true",
8593
8683
  "-c", f"mcp_servers.{name}.command={json.dumps(command)}",
@@ -8609,7 +8699,7 @@ def project_args(plan: dict[str, Any], materialize_agents: bool = True) -> list[
8609
8699
  else:
8610
8700
  policy_args = ["--permission-mode", policy]
8611
8701
  args += policy_args
8612
- servers = review_mcp_servers(plan)
8702
+ servers = mcp_registrations
8613
8703
  if servers:
8614
8704
  mcp_config = {
8615
8705
  "mcpServers": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-bios",
3
- "version": "0.12.0",
4
- "releaseDate": "2026-08-18",
3
+ "version": "0.12.1",
4
+ "releaseDate": "2026-08-19",
5
5
  "description": "A thin, low-level instruction layer for LLM CLI agents: one set of principles and behavior whichever model you run. Deploys into $HOME by copy via an explicit `agent-bios install`.",
6
6
  "bin": {
7
7
  "agent-bios": "install.sh"
package/provenance.json CHANGED
@@ -1 +1 @@
1
- {"commit":"bfcc56715fb031564e370ce236739e4208b7c513","committedAt":"2026-08-18T17:23:41+09:00","dirty":false}
1
+ {"commit":"471ff43022a60490b5e0e788f5ba6dec3a89e992","committedAt":"2026-08-19T07:44:53+09:00","dirty":false}