agent-bios 0.11.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/README.md +4 -4
- package/claude/guides/cli-multi-model-workflow.md +46 -0
- 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/claude/hooks/tooling-gotchas-hook.py +37 -2
- package/claude/skills/repo-charter/SKILL.md +149 -0
- package/codex/guides/cli-multi-model-workflow.md +46 -0
- 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 +191 -21
- package/compose/canary.sh +13 -0
- package/compose/check-domains.py +144 -20
- package/compose/domains.json +4 -0
- package/compose/prune-backups.py +57 -7
- package/install.sh +396 -27
- package/launch/agent-launch.py +3976 -595
- package/launch/agent-launch.toml +10 -4
- package/launch/agent-launch.zsh +7 -2
- package/launch/i18n/en.toml +127 -7
- package/launch/i18n/ja.toml +126 -7
- package/launch/i18n/ko.toml +126 -7
- package/learn/check-learning.py +19 -1
- package/learn/collect-learning.py +57 -2
- package/learn/migrate-learnings.py +140 -16
- package/learn/redact.py +14 -5
- package/package.json +3 -2
- package/provenance.json +1 -1
- package/session-cost.py +402 -33
- package/wrappers/claude-run.sh +49 -4
|
@@ -154,6 +154,50 @@ How much independence a review actually bought, as an ordinal grade per reviewer
|
|
|
154
154
|
- After resume/clear/relocation, verify pwd, branch, and HEAD against the pinned handoff before acting.
|
|
155
155
|
- Attribute a parallel session's action (commit, branch, resource) by execution evidence in that session's own transcript, never by token mentions — shared handoff/memory files inject the same tokens into every session's context.
|
|
156
156
|
|
|
157
|
+
## Context Budget And Reset
|
|
158
|
+
|
|
159
|
+
Context growth is a property of the work, not of the host: over 1,075 sessions of 50+
|
|
160
|
+
requests across both CLIs it runs ~2,400 tokens per request (IQR 1,850-2,950), the two
|
|
161
|
+
hosts within 7% of each other (2,280 Claude, 2,450 Codex); the longest sessions (400+
|
|
162
|
+
requests) run lower, ~1,800. One budget therefore serves both, and what differs per host
|
|
163
|
+
is the price of ignoring it. The figure is a prior; the live session is measured below.
|
|
164
|
+
|
|
165
|
+
- **Automatic compaction fires only when the window is nearly full** — Claude at 84-87%
|
|
166
|
+
(windows cluster at 200K and 1M), Codex at ~95% of the window its transcript records as
|
|
167
|
+
`model_context_window` (258,400 on the sessions measured; a later Codex/model pair
|
|
168
|
+
records 353,400 — read the value, never assume it). Cache read is charged per
|
|
169
|
+
request against the whole loaded context, so leaving the reset to the host pays the
|
|
170
|
+
maximum on every request before it. Measured: input is 92-94% of session cost and
|
|
171
|
+
output 6-8%, at a 95-97% cache hit rate — the context is the bill, and uncached input
|
|
172
|
+
is 0.0% of it.
|
|
173
|
+
- Reset deliberately instead. Cost per request falls ~4x from an 867K auto-compact point
|
|
174
|
+
to 200K. The cost-theoretic optimum is ~65K, but it buys a compaction every ~32
|
|
175
|
+
requests at 2-3 minutes each, so 150-250K is the working range and the tail below it is
|
|
176
|
+
not worth chasing.
|
|
177
|
+
- **What bounds the budget is what survives the reset, not the token count.** A compaction
|
|
178
|
+
keeps a ~14K summary plus 3-4 recent messages and discards the rest — unguided when it
|
|
179
|
+
fires on its own. Anything already written to a file survives every reset, so the
|
|
180
|
+
earliest safe threshold is the one where durable state is already on disk. That is what
|
|
181
|
+
decouples cost from loss: without it, resetting more often loses proportionally more.
|
|
182
|
+
- Choose the mechanism by what is known, not by how large the context grew:
|
|
183
|
+
|
|
184
|
+
| Situation | Mechanism |
|
|
185
|
+
|---|---|
|
|
186
|
+
| Stage finished, what to keep is known | clear + dated handoff file — cheapest, and the loss is not a loss |
|
|
187
|
+
| Mid-stage, what to keep is known | write the handoff first, then compact with explicit instructions |
|
|
188
|
+
| Mid-stage, the needed detail is not yet identifiable | compact with instructions; a summary spans what a file cannot yet name |
|
|
189
|
+
| Original detail likely wanted later | clear, and record the transcript path in the handoff — transcripts persist on disk |
|
|
190
|
+
| The next question is unknown | new session plus messaging; only this keeps a round trip available |
|
|
191
|
+
| Growth is tool output | offload to subagents instead — measured ~20x cheaper (tier ~5x, context isolation ~4x) |
|
|
192
|
+
| The judgement trail itself is load-bearing | keep the session and pay the 2-5x resume |
|
|
193
|
+
|
|
194
|
+
- Measure rather than estimate: `agent-bios cost --context <transcript>` (the installed
|
|
195
|
+
entry to `session-cost.py`) reads either
|
|
196
|
+
host's transcript and reports current context, growth rate, compactions, and requests
|
|
197
|
+
remaining against a budget. Codex records `model_context_window` directly; Claude does
|
|
198
|
+
not, so the tool reports its observed auto-compaction point instead of assuming a
|
|
199
|
+
window.
|
|
200
|
+
|
|
157
201
|
## Handoff Contract
|
|
158
202
|
|
|
159
203
|
Write for the next agent and re-verification, not narrative. Required content:
|
|
@@ -223,3 +267,5 @@ Single owner of numeric defaults; one production environment, 2026-07. Recalibra
|
|
|
223
267
|
| two live delegation sessions | tiering saved ~3.3×; discarded prefixes made fresh respawn cheaper; unpinned reviewers inherited FRONTIER |
|
|
224
268
|
| Codex reach contrast | inherit ~16.5K vs hermetic ~8.7K tokens; schema and stdout/stderr contract verified |
|
|
225
269
|
| Codex native-spawn probe + HELM E2E | requested max/Ultra native children recorded xhigh/role null; separate read-only roots recorded max and Ultra successfully |
|
|
270
|
+
| 1,075 sessions of 50+ requests, both hosts (2026-08-16; the earlier 62-session top-by-size sample gave ~1,800-2,000) | context grows ~2,400 tok/request (IQR 1,850-2,950), hosts within 7%, ~1,800 in 400+-request sessions; auto-compaction fires at 84-95% of window, never earlier |
|
|
271
|
+
| 2 sessions decomposed by cost component (2026-08) | input 92-94% of cost, output 6-8%, cache hit 95-97%, uncached input 0.0%; an 867K→200K budget cuts cost per request ~4x |
|
|
@@ -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
|
@@ -22,8 +22,10 @@ env-personal is never assembled. The domains gate must be green first.
|
|
|
22
22
|
"""
|
|
23
23
|
import argparse
|
|
24
24
|
import json
|
|
25
|
+
import os
|
|
25
26
|
import pathlib
|
|
26
27
|
import re
|
|
28
|
+
import shlex
|
|
27
29
|
import shutil
|
|
28
30
|
import subprocess
|
|
29
31
|
import sys
|
|
@@ -37,6 +39,14 @@ PERSONAL_IMPORT_LINE = "@personal/learnings.md"
|
|
|
37
39
|
CLAUDE_VAR = "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
38
40
|
CODEX_VAR = "${CODEX_HOME:-$HOME/.codex}"
|
|
39
41
|
CODEX_ONLY_PREFIX = "- Codex-only standing authorization:"
|
|
42
|
+
# Where that bullet belongs. Owned here because the payload cannot import from gates/,
|
|
43
|
+
# and `gates/emit-mirrors.py` — which pins the same position in the STATIC projection —
|
|
44
|
+
# imports it from this module instead, the same direction check-package.sh already takes
|
|
45
|
+
# for `author_only`. The two were independent before, and they disagreed: the projection
|
|
46
|
+
# put the bullet under this heading while the assembler appended it to the end of the
|
|
47
|
+
# bundle, so the deployed AGENTS.md filed a multi-model rule under whatever section
|
|
48
|
+
# happened to come last.
|
|
49
|
+
CODEX_ONLY_ANCHOR = "## Multi-Model Workflow"
|
|
40
50
|
|
|
41
51
|
ENTRY_SEED = f"""# CLAUDE.md
|
|
42
52
|
|
|
@@ -99,6 +109,30 @@ def parse_monolith(text):
|
|
|
99
109
|
return title, sections
|
|
100
110
|
|
|
101
111
|
|
|
112
|
+
def place_codex_only(bundle, bullet):
|
|
113
|
+
"""Put the Codex-only bullet first under CODEX_ONLY_ANCHOR, or refuse.
|
|
114
|
+
|
|
115
|
+
Appending was the old behaviour and it is what put a multi-model rule under Session
|
|
116
|
+
Learning in every deployed AGENTS.md: the bundle's sections are whatever the selection
|
|
117
|
+
kept, so "the end" is a different heading depending on what the user installed.
|
|
118
|
+
|
|
119
|
+
Refuses rather than falling back to appending. The bullet is only added when
|
|
120
|
+
`multi-agent-orchestration` is selected, and that domain is what carries the anchor
|
|
121
|
+
heading, so a missing anchor means the bundle is not the shape this rule assumes —
|
|
122
|
+
quietly filing the rule somewhere else is how the defect looked in the first place.
|
|
123
|
+
"""
|
|
124
|
+
lines = bundle.split("\n")
|
|
125
|
+
hits = [i for i, line in enumerate(lines) if line == CODEX_ONLY_ANCHOR]
|
|
126
|
+
if len(hits) != 1:
|
|
127
|
+
die(f"codex bundle must hold exactly one {CODEX_ONLY_ANCHOR!r} to place "
|
|
128
|
+
f"{CODEX_ONLY_PREFIX!r} under; found {len(hits)}")
|
|
129
|
+
index = hits[0] + 1
|
|
130
|
+
while index < len(lines) and not lines[index].strip():
|
|
131
|
+
index += 1
|
|
132
|
+
lines.insert(index, bullet)
|
|
133
|
+
return "\n".join(lines)
|
|
134
|
+
|
|
135
|
+
|
|
102
136
|
def build_bundle(monolith_text, manifest, selection, tool):
|
|
103
137
|
entry_of = {}
|
|
104
138
|
for e in manifest["bullets"]:
|
|
@@ -226,12 +260,58 @@ def copy_filtered(src_dir, names, dest, rewrite=None, dry=False, backup=None):
|
|
|
226
260
|
target.write_text(body, encoding="utf-8")
|
|
227
261
|
|
|
228
262
|
|
|
263
|
+
def replace_atomically(path, text):
|
|
264
|
+
"""Write via a temp file + os.replace, so a write that fails partway cannot truncate.
|
|
265
|
+
|
|
266
|
+
Every file this is used on is one the USER owns and edits — their settings.json, their
|
|
267
|
+
AGENTS.md — and a plain write_text truncates first and fills after. A short write (a full
|
|
268
|
+
disk, a crash) left AGENTS.md holding a fragment of our marker and none of their text,
|
|
269
|
+
with no copy in reach: the backup taken beside these calls is of the PREVIOUS content,
|
|
270
|
+
which is exactly what a half-written file destroys the value of.
|
|
271
|
+
|
|
272
|
+
learn/migrate-learnings.py has had this discipline and states the reason; it now shares
|
|
273
|
+
this one implementation rather than keeping a second. The temp file is a sibling so the
|
|
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.
|
|
280
|
+
"""
|
|
281
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
282
|
+
tmp = path.with_name(path.name + ".tmp-agent-bios")
|
|
283
|
+
try:
|
|
284
|
+
tmp.write_text(text, encoding="utf-8")
|
|
285
|
+
if path.exists():
|
|
286
|
+
tmp.chmod(path.stat().st_mode & 0o777)
|
|
287
|
+
os.replace(tmp, path)
|
|
288
|
+
finally:
|
|
289
|
+
if tmp.exists():
|
|
290
|
+
tmp.unlink()
|
|
291
|
+
|
|
292
|
+
|
|
229
293
|
def hook_command_matches(command, name):
|
|
230
|
-
"""
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
294
|
+
"""Does an argv TOKEN of this command end in `/hooks/<name>`?
|
|
295
|
+
|
|
296
|
+
One matcher for the merge below AND the domain gate's settings leg — the gate used a
|
|
297
|
+
substring test, so `central/hooks/<name>.disabled` passed the gate while this merge
|
|
298
|
+
skipped it, and the install carried no hook under a green gate.
|
|
299
|
+
|
|
300
|
+
Split as a shell would, not delimited by spaces. The registration this file writes is
|
|
301
|
+
`shlex.quote`d so a config home containing a space works, and the quoting puts a `'`
|
|
302
|
+
right after the filename — so a matcher wanting end-of-string or a following space
|
|
303
|
+
could not recognize the line it had just written, and every reinstall on a
|
|
304
|
+
`/Users/First Last` home appended another copy of the same hook. Splitting the way the
|
|
305
|
+
shell will is the only reading that survives its own quoting.
|
|
306
|
+
|
|
307
|
+
Still a PATH match and never a bare name: `wrap.py --inner <name>` passes the name as
|
|
308
|
+
an argument to somebody else's wrapper, and deleting a stranger's hook is not a right
|
|
309
|
+
this ownership rule ever claimed."""
|
|
310
|
+
try:
|
|
311
|
+
tokens = shlex.split(command)
|
|
312
|
+
except ValueError: # unbalanced quotes: not a command we wrote
|
|
313
|
+
tokens = command.split()
|
|
314
|
+
return any(token.endswith("/hooks/" + name) for token in tokens)
|
|
235
315
|
|
|
236
316
|
|
|
237
317
|
def merge_settings(claude_dir, hook_names, template_path, dry=False, owned_names=None):
|
|
@@ -250,15 +330,41 @@ def merge_settings(claude_dir, hook_names, template_path, dry=False, owned_names
|
|
|
250
330
|
"""
|
|
251
331
|
owned = set(owned_names if owned_names is not None else hook_names)
|
|
252
332
|
spath = claude_dir / "settings.json"
|
|
253
|
-
|
|
333
|
+
existed = spath.exists()
|
|
334
|
+
settings = json.loads(spath.read_text(encoding="utf-8")) if existed else {}
|
|
254
335
|
template = json.loads(template_path.read_text(encoding="utf-8")) if template_path.exists() else {}
|
|
255
336
|
hooks = settings.setdefault("hooks", {})
|
|
256
337
|
|
|
257
|
-
def ours(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
338
|
+
def ours(hook):
|
|
339
|
+
# The same matcher the re-add below uses, which `hook_command_matches` already
|
|
340
|
+
# describes itself as being ("one matcher for the merge below AND the domain
|
|
341
|
+
# gate"). Only the ADD half honoured that; removal asked whether the name appeared
|
|
342
|
+
# anywhere in the command, so a user's own entry was deleted for mentioning ours —
|
|
343
|
+
# `wrap.py --inner tooling-gotchas-hook.py`, a `.bak` copy, a directory named after
|
|
344
|
+
# it. Deleting a stranger's hook out of their settings is not something the
|
|
345
|
+
# ownership rule above ever claimed the right to do.
|
|
346
|
+
return any(hook_command_matches(hook.get("command", ""), n) for n in owned)
|
|
347
|
+
|
|
348
|
+
# Pruned per HOOK, not per entry. Ownership is a name on one command, and the entry is
|
|
349
|
+
# a matcher that can hold several — so `{"matcher": "Bash", "hooks": [ours, theirs]}`
|
|
350
|
+
# answered "ours" and took the stranger's hook with it. That is the same overreach the
|
|
351
|
+
# matcher above was narrowed to stop, one level up: the granularity of the removal has
|
|
352
|
+
# to match the granularity of the claim. An entry left with no hooks was only ever ours,
|
|
353
|
+
# so it still goes whole.
|
|
354
|
+
for event, entries in list(hooks.items()):
|
|
355
|
+
pruned = []
|
|
356
|
+
for en in entries:
|
|
357
|
+
children = en.get("hooks") if isinstance(en, dict) else None
|
|
358
|
+
if not isinstance(children, list):
|
|
359
|
+
pruned.append(en) # not a shape we wrote; not ours to judge
|
|
360
|
+
continue
|
|
361
|
+
keep = [h for h in children if not (isinstance(h, dict) and ours(h))]
|
|
362
|
+
if len(keep) == len(children):
|
|
363
|
+
pruned.append(en) # nothing of ours in it — untouched
|
|
364
|
+
elif keep:
|
|
365
|
+
en["hooks"] = keep # ours dropped, their siblings stay where they are
|
|
366
|
+
pruned.append(en)
|
|
367
|
+
hooks[event] = pruned
|
|
262
368
|
for event, entries in template.get("hooks", {}).items(): # re-add per selection
|
|
263
369
|
for en in entries:
|
|
264
370
|
cmds = [h.get("command", "") for h in en.get("hooks", [])]
|
|
@@ -268,16 +374,32 @@ def merge_settings(claude_dir, hook_names, template_path, dry=False, owned_names
|
|
|
268
374
|
continue
|
|
269
375
|
clone = json.loads(json.dumps(en))
|
|
270
376
|
for h in clone.get("hooks", []):
|
|
377
|
+
# Quoted, because this lands in a shell command line. A config home with a
|
|
378
|
+
# space in it — `/Users/First Last/.claude` — split into separate argv
|
|
379
|
+
# words, so the shell tried to run `/Users/First` and the deployed hook
|
|
380
|
+
# simply never fired. Nothing reported it: a hook that does not run looks
|
|
381
|
+
# exactly like a hook with nothing to say.
|
|
382
|
+
#
|
|
383
|
+
# A function replacement rather than a string one: shlex.quote emits
|
|
384
|
+
# backslashes for some paths and re.sub reads those as group references in
|
|
385
|
+
# a replacement string.
|
|
386
|
+
target = shlex.quote(str(claude_dir / "central" / "hooks" / owner))
|
|
271
387
|
h["command"] = re.sub(r"\S*/hooks/" + re.escape(owner),
|
|
272
|
-
|
|
388
|
+
lambda _match, value=target: value,
|
|
273
389
|
h["command"])
|
|
274
390
|
hooks.setdefault(event, []).append(clone)
|
|
275
391
|
if dry:
|
|
276
392
|
print(f" [dry] merge settings.json ({len(hook_names)} central hooks)")
|
|
277
393
|
return
|
|
278
|
-
if
|
|
394
|
+
if not existed and not any(hooks.values()):
|
|
395
|
+
# Nothing of ours to register and no file to preserve. Writing one would create a
|
|
396
|
+
# settings.json the user did not have — and this branch is reached by uninstall on a
|
|
397
|
+
# machine where the claude dir is gone, where creating the directory to hold it
|
|
398
|
+
# crashed on a missing parent instead.
|
|
399
|
+
return
|
|
400
|
+
if existed:
|
|
279
401
|
shutil.copy2(spath, spath.with_suffix(f".json.bak-{time.strftime('%Y%m%d-%H%M%S')}"))
|
|
280
|
-
spath
|
|
402
|
+
replace_atomically(spath, json.dumps(settings, indent=2, ensure_ascii=False) + "\n")
|
|
281
403
|
|
|
282
404
|
|
|
283
405
|
def seed_personal_learnings(claude_dir, dry=False):
|
|
@@ -335,7 +457,21 @@ def seed_entry(claude_dir, legacy_monolith, prior_deployed=(), dry=False):
|
|
|
335
457
|
return "needs-action" # user content without the import line: report, never rewrite
|
|
336
458
|
|
|
337
459
|
|
|
338
|
-
def merge_codex(codex_dir, central_text, dry=False):
|
|
460
|
+
def merge_codex(codex_dir, central_text, prior_deployed=(), dry=False):
|
|
461
|
+
"""Write the central region into AGENTS.md, and never silence a file the user wrote.
|
|
462
|
+
|
|
463
|
+
A missing marker pair used to mean "legacy whole-file deploy", so any AGENTS.md without
|
|
464
|
+
them had its active body replaced by the region plus an empty `## Personal`. Every Codex
|
|
465
|
+
user who wrote an AGENTS.md before installing has exactly that file, and their
|
|
466
|
+
instructions stopped loading on the first install — the backup made it recoverable, not
|
|
467
|
+
noticed. Absence of a marker is absence of evidence, in both directions.
|
|
468
|
+
|
|
469
|
+
So the same evidence seed_entry uses on the Claude side decides it here: `prior_deployed`
|
|
470
|
+
is the previous install's manifest, and a path in it is ours by record. Anything else is
|
|
471
|
+
theirs, and the markers are adopted ABOVE their text rather than over it — which is what
|
|
472
|
+
the marker pair is for, and it leaves nothing of theirs unloaded. Nothing is lost that
|
|
473
|
+
way, so that branch needs no backup; the by-record branch keeps the one it always had.
|
|
474
|
+
"""
|
|
339
475
|
agents = codex_dir / "AGENTS.md"
|
|
340
476
|
region = f"{MARK_START}\n{central_text}{MARK_END}\n"
|
|
341
477
|
if agents.exists():
|
|
@@ -344,17 +480,21 @@ def merge_codex(codex_dir, central_text, dry=False):
|
|
|
344
480
|
pre, rest = body.split(MARK_START, 1)
|
|
345
481
|
_, post = rest.split(MARK_END, 1)
|
|
346
482
|
new = pre + region + post
|
|
347
|
-
|
|
483
|
+
elif str(pathlib.Path(agents)) in prior_deployed:
|
|
484
|
+
# Ours by record: an earlier release deployed this file whole, so replacing it
|
|
485
|
+
# with the marked shape is the upgrade, not a loss.
|
|
348
486
|
if not dry:
|
|
349
487
|
shutil.copy2(agents, agents.with_suffix(f".md.bak-legacy-{time.strftime('%Y%m%d-%H%M%S')}"))
|
|
350
488
|
new = region + "\n## Personal\n"
|
|
489
|
+
else:
|
|
490
|
+
new = region + "\n" + body.lstrip("\n")
|
|
351
491
|
else:
|
|
352
492
|
new = region + "\n## Personal\n"
|
|
353
493
|
if dry:
|
|
354
494
|
print(f" [dry] write AGENTS.md central region ({len(central_text)} bytes)")
|
|
355
495
|
return
|
|
356
496
|
codex_dir.mkdir(parents=True, exist_ok=True)
|
|
357
|
-
agents
|
|
497
|
+
replace_atomically(agents, new)
|
|
358
498
|
|
|
359
499
|
|
|
360
500
|
def remove_owned(claude_dir, codex_dir, manifest, dry=False):
|
|
@@ -385,7 +525,7 @@ def remove_owned(claude_dir, codex_dir, manifest, dry=False):
|
|
|
385
525
|
if dry:
|
|
386
526
|
print(" [dry] strip AGENTS.md central region, keep everything outside the markers")
|
|
387
527
|
return
|
|
388
|
-
agents
|
|
528
|
+
replace_atomically(agents, (pre + post).lstrip("\n"))
|
|
389
529
|
|
|
390
530
|
|
|
391
531
|
def main():
|
|
@@ -402,6 +542,11 @@ def main():
|
|
|
402
542
|
"from it, so an earlier release's deployed CLAUDE.md is recognized as "
|
|
403
543
|
"ours instead of being reported as the user's.")
|
|
404
544
|
ap.add_argument("--dry-run", action="store_true")
|
|
545
|
+
ap.add_argument("--selected-skills", action="store_true",
|
|
546
|
+
help="print the names of the shipped skills the selection delivers, one "
|
|
547
|
+
"per line, and write nothing. install.sh deploys skills itself (a "
|
|
548
|
+
"skill is a tree the HOST scans, not a file under central/) and asks "
|
|
549
|
+
"here which ones, so the selection rule keeps one owner")
|
|
405
550
|
args = ap.parse_args()
|
|
406
551
|
|
|
407
552
|
import os
|
|
@@ -409,13 +554,28 @@ def main():
|
|
|
409
554
|
codex_dir = pathlib.Path(args.codex_dir or os.environ.get("CODEX_HOME") or pathlib.Path.home() / ".codex")
|
|
410
555
|
state_dir = pathlib.Path(args.state_dir or pathlib.Path.home() / ".local/share/agent-bios")
|
|
411
556
|
|
|
412
|
-
manifest = json.loads((REPO / "compose" / "domains.json").read_text(encoding="utf-8"))
|
|
413
557
|
if args.remove_owned:
|
|
414
558
|
# No domains gate: removal does not depend on the manifest being well-formed, and an
|
|
415
559
|
# uninstall that refuses to run because the corpus is mid-edit would strand the user.
|
|
560
|
+
# That was the claim; the parse sat ABOVE this branch and ran first, so a malformed
|
|
561
|
+
# domains.json raised out of uninstall before the branch that does not need it. The
|
|
562
|
+
# two halves of the removal need it differently: the Codex region is bounded by our
|
|
563
|
+
# markers and needs nothing, while the settings registrations are owned BY NAME and
|
|
564
|
+
# cannot be found without it. So the region goes either way, and a manifest we cannot
|
|
565
|
+
# read makes the registration half impossible rather than skippable — the caller has
|
|
566
|
+
# to hear that, not read a clean summary over it.
|
|
567
|
+
try:
|
|
568
|
+
manifest = json.loads((REPO / "compose" / "domains.json").read_text(encoding="utf-8"))
|
|
569
|
+
except (OSError, ValueError) as exc:
|
|
570
|
+
remove_owned(claude_dir, codex_dir, {"hooks": {}}, dry=args.dry_run)
|
|
571
|
+
die(f"the AGENTS.md central region was removed, but {REPO / 'compose' / 'domains.json'} "
|
|
572
|
+
f"could not be read ({exc}) — the hook registrations it names are STILL in "
|
|
573
|
+
f"settings.json. Restore that file and re-run uninstall.")
|
|
416
574
|
remove_owned(claude_dir, codex_dir, manifest, dry=args.dry_run)
|
|
417
575
|
return
|
|
418
576
|
|
|
577
|
+
manifest = json.loads((REPO / "compose" / "domains.json").read_text(encoding="utf-8"))
|
|
578
|
+
|
|
419
579
|
gate = subprocess.run([sys.executable, str(REPO / "compose" / "check-domains.py")],
|
|
420
580
|
capture_output=True, text=True)
|
|
421
581
|
if gate.returncode != 0:
|
|
@@ -431,6 +591,16 @@ def main():
|
|
|
431
591
|
if unknown:
|
|
432
592
|
die(f"unknown domains: {sorted(unknown)} (known: {sorted(manifest['domains'])})")
|
|
433
593
|
|
|
594
|
+
if args.selected_skills:
|
|
595
|
+
# The same two rules the guides get: the manifest's audience, then the file's own
|
|
596
|
+
# `audience: author` declaration — the gate that tolerates author-side paths in a
|
|
597
|
+
# declared file assumes the assembler withholds it, and a skill deployed by
|
|
598
|
+
# install.sh from this list must keep that assumption true.
|
|
599
|
+
for name in filtered_files(manifest, "skills", selection):
|
|
600
|
+
if not author_only(REPO / "claude" / "skills" / name / "SKILL.md"):
|
|
601
|
+
print(name)
|
|
602
|
+
return
|
|
603
|
+
|
|
434
604
|
monolith = (REPO / "claude" / "CLAUDE.md").read_text(encoding="utf-8")
|
|
435
605
|
bundle, n_bullets = build_bundle(monolith, manifest, selection, "claude")
|
|
436
606
|
codex_src = (REPO / "codex" / "AGENTS.md").read_text(encoding="utf-8")
|
|
@@ -438,7 +608,7 @@ def main():
|
|
|
438
608
|
if "multi-agent-orchestration" in selection:
|
|
439
609
|
codex_only = next((ln for ln in codex_src.splitlines() if ln.startswith(CODEX_ONLY_PREFIX)), None)
|
|
440
610
|
if codex_only:
|
|
441
|
-
codex_bundle
|
|
611
|
+
codex_bundle = place_codex_only(codex_bundle, codex_only)
|
|
442
612
|
|
|
443
613
|
guides = filtered_files(manifest, "guides", selection)
|
|
444
614
|
withheld = [n for n in guides if author_only(REPO / "claude" / "guides" / n)]
|
|
@@ -492,7 +662,7 @@ def main():
|
|
|
492
662
|
prior.read_text(encoding="utf-8").splitlines() if ln.strip()}
|
|
493
663
|
entry_state = seed_entry(claude_dir, monolith, prior_deployed, dry=dry)
|
|
494
664
|
|
|
495
|
-
merge_codex(codex_dir, codex_bundle, dry=dry)
|
|
665
|
+
merge_codex(codex_dir, codex_bundle, prior_deployed, dry=dry)
|
|
496
666
|
copy_filtered(REPO / "codex" / "guides", guides, codex_dir / "guides", dry=dry,
|
|
497
667
|
backup=run_backup)
|
|
498
668
|
|