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.
- package/claude/guides/coding-staged-workflow.md +1 -0
- package/claude/guides/review-defect-criteria.md +205 -0
- package/claude/guides/review-request.md +15 -1
- package/codex/guides/coding-staged-workflow.md +1 -0
- package/codex/guides/review-defect-criteria.md +205 -0
- package/codex/guides/review-request.md +15 -1
- package/compose/assemble.py +7 -0
- package/compose/domains.json +1 -0
- package/launch/agent-launch.py +245 -155
- package/package.json +2 -2
- package/provenance.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
package/compose/assemble.py
CHANGED
|
@@ -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():
|
package/compose/domains.json
CHANGED
|
@@ -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": []},
|
package/launch/agent-launch.py
CHANGED
|
@@ -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,
|
|
3025
|
-
|
|
3026
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5307
|
-
|
|
5308
|
-
|
|
5309
|
-
|
|
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
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
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
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
#
|
|
5569
|
-
#
|
|
5570
|
-
#
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
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
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
5598
|
-
|
|
5599
|
-
|
|
5600
|
-
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
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
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
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
|
|
6274
|
-
#
|
|
6275
|
-
#
|
|
6276
|
-
#
|
|
6277
|
-
#
|
|
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
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
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
|
|
6332
|
-
#
|
|
6333
|
-
#
|
|
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
|
|
7803
|
-
consumers — `run_contract` states it,
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
one tier at another tier's
|
|
7808
|
-
|
|
7809
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
8344
|
-
|
|
8345
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
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.
|
|
4
|
-
"releaseDate": "2026-08-
|
|
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":"
|
|
1
|
+
{"commit":"471ff43022a60490b5e0e788f5ba6dec3a89e992","committedAt":"2026-08-19T07:44:53+09:00","dirty":false}
|