agent-bios 0.9.8 → 0.10.0

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.
Files changed (59) hide show
  1. package/DEPENDENCIES.md +19 -19
  2. package/README.md +43 -12
  3. package/claude/CLAUDE.md +5 -41
  4. package/claude/guides/claude-prompting.md +1 -1
  5. package/claude/guides/cli-multi-model-workflow.md +22 -4
  6. package/claude/guides/coding-staged-workflow.md +49 -15
  7. package/claude/guides/concept-economy.md +187 -0
  8. package/claude/guides/documentation-hygiene.md +112 -0
  9. package/claude/guides/gpt-prompting.md +1 -1
  10. package/claude/guides/learning-flow.md +5 -5
  11. package/claude/guides/llm-capability-boundary.md +6 -1
  12. package/claude/guides/review-request.md +9 -7
  13. package/claude/guides/session-distill-workflow.md +19 -9
  14. package/claude/guides/tooling-gotchas.md +26 -0
  15. package/claude/guides/verification-discipline.md +166 -0
  16. package/claude/hooks/tooling-gotchas-hook.py +329 -12
  17. package/codex/AGENTS.md +5 -41
  18. package/codex/guides/claude-prompting.md +1 -1
  19. package/codex/guides/cli-multi-model-workflow.md +22 -4
  20. package/codex/guides/coding-staged-workflow.md +49 -15
  21. package/codex/guides/concept-economy.md +187 -0
  22. package/codex/guides/documentation-hygiene.md +112 -0
  23. package/codex/guides/gpt-prompting.md +1 -1
  24. package/codex/guides/learning-flow.md +5 -5
  25. package/codex/guides/llm-capability-boundary.md +6 -1
  26. package/codex/guides/review-request.md +9 -7
  27. package/codex/guides/session-distill-workflow.md +19 -9
  28. package/codex/guides/tooling-gotchas.md +26 -0
  29. package/codex/guides/verification-discipline.md +166 -0
  30. package/{scripts → compose}/assemble.py +194 -17
  31. package/{scripts → compose}/canary.sh +14 -5
  32. package/compose/check-domains.py +1178 -0
  33. package/{config → compose}/domains.json +11 -44
  34. package/{scripts → compose}/pkgid.py +8 -1
  35. package/compose/prune-backups.py +204 -0
  36. package/{scripts → compose}/register-hooks.py +3 -3
  37. package/install.sh +1233 -0
  38. package/launch/agent-launch.py +5294 -0
  39. package/launch/agent-launch.toml +376 -0
  40. package/{scripts → launch}/check-prompting-targets.sh +1 -1
  41. package/{scripts → launch}/provision-venv.sh +1 -1
  42. package/{scripts → learn}/check-learning.py +7 -7
  43. package/{scripts → learn}/collect-learning.py +10 -10
  44. package/{config → learn}/learning.schema.json +3 -3
  45. package/{scripts → learn}/migrate-learnings.py +95 -54
  46. package/{scripts → learn}/redact.py +4 -4
  47. package/package.json +32 -27
  48. package/provenance.json +1 -0
  49. package/wrappers/claude-run.sh +162 -0
  50. package/{scripts → wrappers}/codex-run.sh +62 -6
  51. package/config/agent-launch.toml +0 -143
  52. package/scripts/agent-launch.py +0 -2350
  53. package/scripts/check-domains.py +0 -296
  54. package/scripts/check-parity.sh +0 -2003
  55. package/scripts/install.sh +0 -819
  56. /package/{shell → launch}/agent-launch.zsh +0 -0
  57. /package/{config → learn}/promotions.json +0 -0
  58. /package/{scripts/session-cost.py → session-cost.py} +0 -0
  59. /package/{scripts → wrappers}/codex-helm.sh +0 -0
@@ -0,0 +1,1178 @@
1
+ #!/usr/bin/env python3
2
+ """Bijection/coverage gate: compose/domains.json vs the canonical corpus.
3
+
4
+ The manifest is the sole classification authority (tagged-monolith layout);
5
+ claude/CLAUDE.md stays the sole text authority. This gate closes the
6
+ silent-drop window: a bullet reworded without a manifest update, or a
7
+ manifest entry whose anchor no longer matches, fails HERE, before landing.
8
+
9
+ Blocking checks (any violation exits 1, all violations listed):
10
+ 1. schema shape + registry legality (tiers/domains from the manifest's own
11
+ registry; domains list non-empty iff tier == "domain")
12
+ 2. bullet bijection: every manifest anchor matches exactly ONE `- ` bullet
13
+ in claude/CLAUDE.md, and every bullet is claimed by exactly ONE entry
14
+ 3. file coverage: every file in claude/guides|hooks|agents claimed exactly
15
+ once; every claimed file exists on disk
16
+ 4. router-guide co-package, both directions: a bullet or guide referencing a
17
+ guide must have an audience covered by that guide's audience, and the target
18
+ must be one an install actually receives; every guide must be pointed at by
19
+ something — a bullet, a parent guide, or a launch preset — or it ships and is
20
+ read by nobody. This gate assumes references take PATH form; the rule that
21
+ makes that true is author-side, in gates/check-lexicon.py — it needs LEXICON
22
+ and the ko/ tree, neither of which a packaged install has
23
+ 5. hook source_guide: a hook naming its source guide must carry the same
24
+ tier + domain set as that guide
25
+ 6. non-vacuity: every subject set this gate judges is non-empty, so a green
26
+ run cannot be vacuous
27
+
28
+ Informational (non-blocking until budgets are set in the manifest): estimated
29
+ token size per package.
30
+
31
+ --self-test: runs negative controls (mutated manifests that MUST fail) and
32
+ exits 0 only if every mutation is caught. Proves the gate can fail.
33
+ """
34
+ import json
35
+ import pathlib
36
+ import re
37
+ import sys
38
+
39
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
40
+ import pkgid # noqa: E402 (sibling module in compose/)
41
+ from assemble import author_only, hook_command_matches # noqa: E402 THE readers: audience frontmatter, and the settings-merge token rule the gate must agree with
42
+
43
+ REPO = pathlib.Path(__file__).resolve().parent.parent
44
+ MANIFEST = REPO / "compose" / "domains.json"
45
+ MONOLITH = REPO / "claude" / "CLAUDE.md"
46
+ FILE_SECTIONS = { # manifest key -> corpus dir, glob
47
+ "guides": ("claude/guides", "*.md"),
48
+ "hooks": ("claude/hooks", "*"),
49
+ "agents": ("claude/agents", "*.md"),
50
+ }
51
+
52
+
53
+ def load_manifest(path=MANIFEST):
54
+ with open(path, encoding="utf-8") as f:
55
+ return json.load(f)
56
+
57
+
58
+ def corpus_bullets(path=MONOLITH):
59
+ text = path.read_text(encoding="utf-8")
60
+ return [ln for ln in text.splitlines() if ln.startswith("- ")]
61
+
62
+
63
+ def audience(entry, errors, ctx):
64
+ """Universal set marker, or the frozen domain set; records legality errors."""
65
+ tier = entry.get("tier")
66
+ domains = entry.get("domains", [])
67
+ if tier in ("core", "infra"):
68
+ if domains:
69
+ errors.append(f"{ctx}: tier {tier} must not list domains, got {domains}")
70
+ return "UNIVERSAL"
71
+ if tier == "env-personal":
72
+ if domains:
73
+ errors.append(f"{ctx}: env-personal must not list domains")
74
+ return "NEVER"
75
+ if tier == "domain":
76
+ if not domains:
77
+ errors.append(f"{ctx}: tier domain requires >=1 domain")
78
+ return frozenset(domains)
79
+ errors.append(f"{ctx}: illegal tier {tier!r}")
80
+ return "NEVER"
81
+
82
+
83
+ def delivered(name, repo=None):
84
+ """Does an install actually receive this guide?
85
+
86
+ Manifest audience says who NEEDS it; `audience: author` says who can act on it, and
87
+ compose/assemble.py withholds the second from every packaged install. A reference whose
88
+ target is withheld is unresolvable for every installed reader no matter how universal its
89
+ tier looks, so audience coverage alone is the wrong question to ask about it."""
90
+ base = (repo or REPO) / "claude" / "guides" / name
91
+ try:
92
+ return not author_only(base)
93
+ except Exception:
94
+ return True # unreadable frontmatter is check-package's call, not this one
95
+
96
+
97
+ def covers(guide_aud, bullet_aud):
98
+ if guide_aud == "UNIVERSAL":
99
+ return True
100
+ if bullet_aud in ("UNIVERSAL", "NEVER"):
101
+ return False # universal bullet needs universal guide; NEVER refs nothing
102
+ return isinstance(guide_aud, frozenset) and guide_aud >= bullet_aud
103
+
104
+
105
+ def prose_handles(name):
106
+ """Prose forms of a guide name, DERIVED from the filename rather than declared.
107
+
108
+ Multi-token runs of the stem only — `coding-staged-workflow` yields "staged-workflow"
109
+ and "coding-staged", never the bare "workflow" or "coding", because a single generic
110
+ token matches unrelated sentences and a gate that cries wolf gets routed around. The
111
+ hyphen is what makes a run distinctive enough to mean the guide.
112
+
113
+ Derivation rather than declaration because declaring is a step someone forgets: two
114
+ prose references were already caught this way after their instances were patched, and
115
+ a third and fourth were found by the derivation itself. `handles` stays for a phrasing
116
+ that shares no token run with the filename."""
117
+ toks = name[:-3].split("-")
118
+ return sorted({"-".join(toks[i:j]) for i in range(len(toks))
119
+ for j in range(i + 2, len(toks) + 1)}, key=len, reverse=True)
120
+
121
+
122
+ # The words that make a prose mention a REFERENCE ("read the X guide/doc/..."). One list,
123
+ # owned here because this file ships and gates/ may import shipped, never the reverse.
124
+ # refs_from hard-coded "guide" while check-lexicon knew four words, so "read the
125
+ # staged-workflow document" resolved for the author-side gate and not for this one — a
126
+ # universal bullet could carry that pointer past rule 4 undetected.
127
+ REFERRING_WORDS = ("guide", "guidance", "document", "doc")
128
+
129
+
130
+ def referring_alternation():
131
+ """The regex alternation for referring words, plurals included — "read the concept
132
+ economy guideS" was a reference the singular-only pattern let through. One builder,
133
+ because two gates consume this and a plural added in one drifted from the other."""
134
+ return "|".join(w if w == "guidance" else w + "s?" for w in REFERRING_WORDS)
135
+
136
+
137
+ def refs_from(text, guides):
138
+ """Which guides this text sends the reader to — by path, by declared handle, or by a
139
+ derived prose form. Stable order, so one guide named twice is judged once.
140
+
141
+ A derived run counts only when it identifies ONE guide — or is a guide's entire stem,
142
+ since naming the whole filename is not ambiguous even where it prefixes a sibling's.
143
+ Without that, "the llm-capability-boundary guide" marked the base AND both of its
144
+ children as consumed, so an unrelated mention of the parent kept an unreachable child
145
+ out of the orphan report."""
146
+ found = list(dict.fromkeys(re.findall(r"guides/([a-z0-9-]+\.md)", text)))
147
+ lowered = text.lower()
148
+
149
+ def loose(run):
150
+ # Prose renders filenames as plain words: a bullet saying "the concept economy
151
+ # guide" is a promise refs_from could not see while it required the hyphen, so a
152
+ # universal bullet could point at a domain guide undetected. Safe to relax here
153
+ # because this branch already requires the word "guide" after the run — a bare
154
+ # dehyphenated mention never reaches it.
155
+ return r"[-\s]+".join(re.escape(tok) for tok in run.split("-"))
156
+
157
+ owner = {}
158
+ for name in guides:
159
+ for r in prose_handles(name):
160
+ owner.setdefault(r, set()).add(name)
161
+ for name, entry in guides.items():
162
+ if name in found:
163
+ continue
164
+ stem = name[:-3]
165
+ runs = [h for h in prose_handles(name) if owner.get(h) == {name} or h == stem]
166
+ if any(re.search(rf"(?<![0-9A-Za-z_]){re.escape(h.lower())}(?![0-9A-Za-z_])",
167
+ lowered) for h in entry.get("handles", [])) or \
168
+ any(re.search(rf"\b{loose(h)}\s+(?:{referring_alternation()})\b", lowered)
169
+ for h in runs):
170
+ found.append(name)
171
+ return found
172
+
173
+
174
+ def run_gate(manifest, bullets, repo=REPO):
175
+ errors = []
176
+ tiers = set(manifest.get("tiers", []))
177
+ domains_reg = set(manifest.get("domains", {}))
178
+ if not tiers or not domains_reg:
179
+ errors.append("registry: tiers/domains registry empty")
180
+
181
+ errors += check_settings_template(manifest, repo)
182
+
183
+ # -- 0. package identity ------------------------------------------------
184
+ # Absent means core (the reservation that keeps pre-v2 artifacts valid), but
185
+ # a PRESENT malformed id must fail rather than fall back — silently treating
186
+ # a typo as core would hand it the engine's unconditional-injection audience.
187
+ pid = pkgid.resolve(manifest)
188
+ if not pkgid.is_valid(pid):
189
+ errors.append(f"package_id: {pid!r} is not @scope/name (lowercase, hyphen-separated)")
190
+
191
+ def check_registry(entry, ctx):
192
+ if entry.get("tier") not in tiers:
193
+ errors.append(f"{ctx}: tier {entry.get('tier')!r} not in registry")
194
+ for d in entry.get("domains", []):
195
+ if d not in domains_reg:
196
+ errors.append(f"{ctx}: domain {d!r} not in registry")
197
+
198
+ # -- 2. bullet bijection ------------------------------------------------
199
+ mb = manifest.get("bullets", [])
200
+ if not bullets:
201
+ errors.append("non-vacuity: no bullets found in monolith")
202
+ if not mb:
203
+ errors.append("non-vacuity: manifest has no bullet entries")
204
+ if len(mb) != len(bullets):
205
+ errors.append(f"bijection: manifest has {len(mb)} bullet entries, monolith has {len(bullets)} bullets")
206
+ claimed = [0] * len(bullets)
207
+ seen_anchors = set()
208
+ for i, entry in enumerate(mb):
209
+ ctx = f"bullets[{i}] anchor={entry.get('anchor', '')!r:.60}"
210
+ check_registry(entry, ctx)
211
+ anchor = entry.get("anchor", "")
212
+ if not anchor:
213
+ errors.append(f"{ctx}: empty anchor")
214
+ continue
215
+ if anchor in seen_anchors:
216
+ errors.append(f"{ctx}: duplicate anchor")
217
+ seen_anchors.add(anchor)
218
+ hits = [j for j, b in enumerate(bullets) if anchor in b]
219
+ if len(hits) != 1:
220
+ errors.append(f"{ctx}: anchor matches {len(hits)} bullets (need exactly 1)")
221
+ for j in hits:
222
+ claimed[j] += 1
223
+ for j, n in enumerate(claimed):
224
+ if n != 1:
225
+ errors.append(f"bijection: bullet line {bullets[j][:70]!r} claimed {n} times (need exactly 1)")
226
+
227
+ # -- 3. file coverage ---------------------------------------------------
228
+ entries = {}
229
+ for key, (rel, glob) in FILE_SECTIONS.items():
230
+ section = manifest.get(key, {})
231
+ entries[key] = section
232
+ if not section:
233
+ errors.append(f"non-vacuity: manifest section {key!r} empty")
234
+ on_disk = {p.name for p in (repo / rel).glob(glob) if p.is_file()}
235
+ if not on_disk:
236
+ errors.append(f"non-vacuity: no files on disk under {rel}")
237
+ for name, entry in section.items():
238
+ check_registry(entry, f"{key}/{name}")
239
+ if name not in on_disk:
240
+ errors.append(f"{key}: claimed file {name} does not exist in {rel}")
241
+ for name in sorted(on_disk - set(section)):
242
+ errors.append(f"{key}: file {name} in {rel} not claimed by the manifest")
243
+
244
+ # -- 4. router-guide co-package ----------------------------------------
245
+ # Path form AND declared prose handles. Naming a guide in prose is still a router — the
246
+ # reader is sent somewhere — and a path-only scan reports clean on it, so a rule can ship to
247
+ # an audience that cannot hold what it names. Prose is decidable once the mapping exists, so
248
+ # the mapping is DATA on the guide entry (`handles`) and the check stays structural. A
249
+ # handle is only as good as its declaration: this catches phrasings someone wrote down, not
250
+ # every paraphrase.
251
+ guide_aud = {n: audience(e, errors, f"guides/{n}") for n, e in entries["guides"].items()}
252
+ # A declared handle is a NAME, and a name resolving to two guides routes the reader
253
+ # nowhere: refs_from() would credit both targets from one prose router, falsely
254
+ # rooting and co-packaging a guide the sentence never meant. Duplicates fail here
255
+ # so resolution below can trust that a declared handle has one owner.
256
+ hdl_owner = {}
257
+ for n, e in entries["guides"].items():
258
+ for h in e.get("handles", []):
259
+ # Indexed by the SAME normalized form resolution uses — refs_from
260
+ # lowercases before matching, so "Case Alias" and "case alias" are one
261
+ # name, and indexing raw text passed the pair as distinct.
262
+ hdl_owner.setdefault(h.lower(), []).append(n)
263
+ for h, owners in sorted(hdl_owner.items()):
264
+ if len(owners) > 1:
265
+ errors.append(f"guides: handle {h!r} is declared by {', '.join(sorted(owners))}"
266
+ f" — one name, one target; make the handle unique")
267
+ # Declared×derived is the remaining collision pair (declared×declared above,
268
+ # derived×derived resolves to nobody inside refs_from): a handle like "concept
269
+ # economy guide" declared on another guide double-resolves with concept-economy.md's
270
+ # DERIVED form, one prose pointer rooting and co-packaging two targets. Normalized —
271
+ # lowercased, one trailing referring word stripped, spaces to hyphens — a declared
272
+ # handle may not carry a different guide's run or stem at a hyphen boundary.
273
+ ref_tail = re.compile(rf"[-\s]+(?:{referring_alternation()})$")
274
+ for n, e in entries["guides"].items():
275
+ for h in e.get("handles", []):
276
+ norm = re.sub(r"\s+", "-", ref_tail.sub("", h.lower()).strip())
277
+ for other in entries["guides"]:
278
+ if other == n:
279
+ continue
280
+ runs = set(prose_handles(other)) | {other[:-3]}
281
+ if any(re.search(rf"(?:^|-){re.escape(r)}(?:-|$)", norm) for r in runs):
282
+ errors.append(
283
+ f"guides: handle {h!r} on {n} collides with {other}'s derived "
284
+ f"name — one prose pointer would resolve both; reword the handle")
285
+ break
286
+ anchor_of = {id(e): e.get("anchor", "?") for e in mb}
287
+ ref_checks = 0
288
+ undelivered = set()
289
+ for entry in mb:
290
+ hits = [b for b in bullets if entry.get("anchor", "\0") in b]
291
+ if len(hits) != 1:
292
+ continue # already reported by bijection
293
+ b_aud = audience(entry, errors, f"bullet {entry.get('anchor', '')!r:.40}")
294
+ if b_aud == "NEVER":
295
+ # env-personal is never assembled (assemble.py maps the tier to NEVER),
296
+ # so this bullet's references reach no reader — remembered here so the
297
+ # orphan check below does not let it root a guide.
298
+ undelivered.add(hits[0])
299
+ for g in refs_from(hits[0], entries["guides"]):
300
+ ref_checks += 1
301
+ if g not in guide_aud:
302
+ errors.append(f"router: bullet {entry['anchor']!r:.40} references unclaimed guide {g}")
303
+ elif not delivered(g, repo):
304
+ errors.append(
305
+ f"router: bullet {entry['anchor']!r:.40} references {g}, which declares "
306
+ f"audience: author and is withheld from every install — no reader can open it"
307
+ )
308
+ elif not covers(guide_aud[g], b_aud):
309
+ errors.append(
310
+ f"router: bullet {entry['anchor']!r:.40} (audience {b_aud}) references guide {g} "
311
+ f"(audience {guide_aud[g]}) — user can hold the router without the guide"
312
+ )
313
+ if ref_checks == 0:
314
+ errors.append("non-vacuity: no router->guide references were checked")
315
+
316
+ # The same relation, read the other way. Rule 4 asks whether the reader of a pointer has
317
+ # the guide; this asks whether a guide has a pointer at all. Without it a guide can be
318
+ # written, packaged, and delivered while nothing reaches it, and every other check is green.
319
+ #
320
+ # Three consumer kinds exist and each is a real delivery path, so none of them is an
321
+ # exemption: a corpus bullet (by path or declared handle), a parent guide citing a child
322
+ # as a depth chain, and a launch preset's mission. An exemption list would be the fourth,
323
+ # and it is the one that lets a genuinely orphaned guide through.
324
+ guide_dir = repo / "claude" / "guides"
325
+ launch = repo / "launch" / "agent-launch.toml"
326
+ launch_text, launch_missions = "", []
327
+ if launch.is_file():
328
+ # PARSED string values, not the raw file: a guide named in a TOML comment reaches
329
+ # no agent, but refs_from over raw text credited it as a consumer and an orphan
330
+ # slipped past. The parser drops comments and keys; what remains is exactly the
331
+ # text a mission can put in front of a model. A config that does not parse is a
332
+ # loud error — silently falling back to raw text would re-open the comment hole.
333
+ import tomllib
334
+ # Instruction-bearing fields ONLY — `mission` is what the launcher appends to the
335
+ # system prompt, so it is the one field whose text reaches a model. Walking every
336
+ # string value credited a guide named in an unused key as consumed, hiding an
337
+ # orphan; and classified-or-loud closes the other direction too: a guides/
338
+ # reference in any NON-instruction field fails outright, because text no agent
339
+ # reads is either a mistake or belongs in a mission.
340
+ def _fields(v, path=()):
341
+ if isinstance(v, str):
342
+ yield path, v
343
+ elif isinstance(v, dict):
344
+ for k, x in v.items():
345
+ yield from _fields(x, path + (k,))
346
+ elif isinstance(v, list):
347
+ for x in v:
348
+ yield from _fields(x, path)
349
+
350
+ def _is_mission(path):
351
+ # presets.*.mission ONLY — the launcher reads mission from presets alone
352
+ # (launch/agent-launch.py, load_config -> presets), so a `mission` key under
353
+ # any other table is inert text no model receives. Filtering by leaf key
354
+ # credited exactly such a value as a consumer.
355
+ return len(path) == 3 and path[0] == "presets" and path[2] == "mission"
356
+ try:
357
+ pairs = list(_fields(tomllib.loads(launch.read_text(encoding="utf-8"))))
358
+ launch_missions = [v for k, v in pairs if _is_mission(k)]
359
+ launch_text = "\n".join(launch_missions)
360
+ for k, v in pairs:
361
+ # Path form OR prose form — refs_from covers both (its path regex is
362
+ # this test's old one): a `mission` under the wrong table saying
363
+ # "read the concept economy guide first" is the same inert text as a
364
+ # guides/ path there, and the path-only test let the prose form
365
+ # masquerade as instructions no launcher delivers.
366
+ if not _is_mission(k) and refs_from(v, entries["guides"]):
367
+ errors.append(
368
+ f"launch: field {'.'.join(k)!r} names a guide but is not "
369
+ f"instruction-bearing — no agent reads it there; move it into a "
370
+ f"preset mission or drop it")
371
+ except tomllib.TOMLDecodeError as exc:
372
+ errors.append(f"launch: agent-launch.toml does not parse ({exc}) — mission "
373
+ f"consumers cannot be judged")
374
+ # install.sh deploys the launch config regardless of domain selection, so a mission's
375
+ # DEPLOYED-form reference (a CLAUDE_CONFIG_DIR / ~/.claude path) must resolve for every
376
+ # user: universal tier and delivered. A CHECKOUT-form reference (bare claude/guides/...,
377
+ # as the distill mission uses on purpose) names the author's repo; its guard is
378
+ # check-package's RUNTIME_GUARDED leg, not audience math — but it still counts as a
379
+ # consumer below, or the guide it points at reads as an orphan.
380
+ # Judged PER MISSION, never over the concatenated text: one preset's guarded
381
+ # checkout reference must not exempt another preset's prose reference to the same
382
+ # guide — collapsing by name did exactly that, and review reproduced it with both
383
+ # forms of concept-economy in one config.
384
+ launch_flagged = set()
385
+ for mtext in launch_missions:
386
+ # Occurrences are classified separately even inside ONE mission: refs_from
387
+ # collapses a checkout path and a prose pointer to the same guide, and the
388
+ # mission-wide checkout match exempted the prose — review reproduced both forms
389
+ # in a single mission. Stripping every path first leaves exactly the prose
390
+ # occurrences, so what still resolves afterwards was said in words.
391
+ pathless_m = re.sub(r"\S*guides/[a-z0-9-]+\.md\S*", "", mtext)
392
+ prose_refs = set(refs_from(pathless_m, entries["guides"]))
393
+ for lg in refs_from(mtext, entries["guides"]):
394
+ if lg in launch_flagged:
395
+ continue
396
+ # The EXEMPTION is the narrow case: a checkout-form path (bare
397
+ # claude/guides/..., the distill mission's deliberate shape, guarded by
398
+ # check-package's RUNTIME_GUARDED) names the author's repo. A deployed-home
399
+ # path or a PROSE occurrence reaches every selection, because presets deploy
400
+ # unconditionally.
401
+ checkout_form = re.search(
402
+ r"(?<![\w$}])claude/guides/" + re.escape(lg), mtext)
403
+ deployed_form = re.search(
404
+ r"(?:CLAUDE_CONFIG_DIR|CODEX_HOME|\.claude|\.codex)[^\n\"']*guides/"
405
+ + re.escape(lg), mtext)
406
+ if (deployed_form or lg in prose_refs or not checkout_form) and (
407
+ not delivered(lg, repo) or guide_aud.get(lg) != "UNIVERSAL"):
408
+ launch_flagged.add(lg)
409
+ errors.append(
410
+ f"launch: a mission references {lg}, the launch config deploys to "
411
+ f"every selection, and {lg} is "
412
+ f"{'withheld (audience: author)' if not delivered(lg, repo) else 'domain-scoped'}"
413
+ f" — a non-matching selection receives the mission without the guide")
414
+ def _defenced(text):
415
+ # Fenced code blocks are EXAMPLES: a fenced `guides/x.md` shows the reader what a
416
+ # reference looks like without sending anyone anywhere, and counting it as a
417
+ # consumer let a fenced sample conceal an orphan after its real router was
418
+ # removed. ALL Markdown fence forms — ``` and ~~~ at any length >= 3, closed by
419
+ # the same character at >= the opening length — because the first version knew
420
+ # only triple backticks and review moved the sample to ~~~. Inline code spans
421
+ # stay: real references are written as backticked deploy paths.
422
+ out, fence = [], None
423
+ prev_blank, last_nonblank, in_indent = True, "", False
424
+ for line in text.splitlines():
425
+ # Blockquote markers are containers, not content: `> ```` opens a fence as
426
+ # surely as ``` does, and the prefixed form escaped the opener match. The
427
+ # marker is normalized off for the whole pipeline — blockquoted PROSE still
428
+ # renders and its real references still count.
429
+ # A list marker is a container the same way when a blockquote rides it:
430
+ # `- > ```` renders as code inside a list-carried blockquote, and the
431
+ # leading marker hid the `>` from the normalization — the fence never
432
+ # opened and the quoted example read as prose. Stripped only when a
433
+ # blockquote follows; a plain list item is content, not a wrapper.
434
+ line = re.sub(r"^\s*(?:[-*+]|\d+\.)\s+(?=>)", "", line)
435
+ line = re.sub(r"^(\s*>)+ ?", "", line)
436
+ s = line.strip()
437
+ m = re.match(r"(`{3,}|~{3,})", s)
438
+ if fence is not None:
439
+ if m and m.group(1)[0] == fence[0] and len(m.group(1)) >= fence[1] \
440
+ and not s[len(m.group(1)):].strip():
441
+ # A CLOSING fence carries nothing but whitespace — ```python inside
442
+ # an open block is a nested opener in real Markdown, and treating it
443
+ # as the close exposed the rest of the example as prose.
444
+ fence = None
445
+ continue
446
+ if m:
447
+ fence = (m.group(1)[0], len(m.group(1)))
448
+ continue
449
+ # Markdown's INDENTED code form: four spaces (or a tab) after a blank line
450
+ # opens a code block unless the preceding block is a list item, whose
451
+ # continuation lines are legitimately indented prose. The fenced repairs
452
+ # left this fourth example syntax crediting consumers.
453
+ # The code threshold is RELATIVE to the enclosing block: four columns past
454
+ # the text of a list item (whose continuations are legitimately indented
455
+ # prose), four from the margin otherwise. A blanket list exemption kept an
456
+ # eight-space sample nested under a list item as prose.
457
+ lm = re.match(r"(\s*(?:[-*+]|\d+\.)\s+)", last_nonblank.expandtabs(4))
458
+ base = len(lm.group(1)) if lm else 0
459
+ # Tabs expand to columns (CommonMark: a tab advances to the next 4-column
460
+ # stop); counting a tab as one character kept tab-indented code as prose.
461
+ exp = line.expandtabs(4)
462
+ ind = len(exp) - len(exp.lstrip())
463
+ indented = bool(s) and ind >= base + 4
464
+ if in_indent:
465
+ if (bool(s) and ind >= base + 4) or not s:
466
+ continue
467
+ in_indent = False
468
+ elif indented and prev_blank:
469
+ in_indent = True
470
+ continue
471
+ if s:
472
+ last_nonblank = line
473
+ prev_blank = not s
474
+ out.append(line)
475
+ prose = "\n".join(out)
476
+ # HTML comments are stripped from RENDERED PROSE, after the fence pass: a literal
477
+ # <!-- inside a fenced sample is code, and subbing comments first let it swallow
478
+ # everything through EOF — including a real router after the fence, which then
479
+ # read as an orphan. Closed comments first, then an unterminated one through EOF
480
+ # (rendered Markdown hides the remainder either way).
481
+ prose = re.sub(r"<!--.*?-->", "", prose, flags=re.S)
482
+ return re.sub(r"<!--.*", "", prose, flags=re.S)
483
+
484
+ bodies = {p.name: _defenced(p.read_text(encoding="utf-8"))
485
+ for p in sorted(guide_dir.glob("*.md"))}
486
+ if not bodies:
487
+ errors.append("non-vacuity: no guide bodies read, so consumers were not checked")
488
+ # Reachability is TRANSITIVE FROM ROOTS (bullets and launch missions), not "any
489
+ # incoming edge": two unrooted guides citing each other kept each other alive as an
490
+ # inert island — the same lesson the pre-commit reach scan learned about seeding.
491
+ # And a root must be DELIVERED: an undelivered (env-personal) bullet exists in the
492
+ # monolith but ships to nobody, and routing a guide's only pointer through one
493
+ # concealed the orphan behind a tier change.
494
+ reachable, frontier = set(), [
495
+ g for g in entries["guides"]
496
+ if any(g in refs_from(b, entries["guides"])
497
+ for b in bullets if b not in undelivered)
498
+ or g in refs_from(launch_text, entries["guides"])]
499
+ while frontier:
500
+ g = frontier.pop()
501
+ if g in reachable:
502
+ continue
503
+ reachable.add(g)
504
+ frontier.extend(n for n in refs_from(bodies.get(g, ""), entries["guides"])
505
+ if n != g and n not in reachable)
506
+ for name in entries["guides"]:
507
+ if name not in reachable:
508
+ errors.append(
509
+ f"orphan: guide {name} is claimed by the manifest and delivered, but no bullet, "
510
+ f"no other guide, and no launch preset points at it — it is inert"
511
+ )
512
+
513
+ # A guide citing another guide is a router with the same failure mode, so it needs the
514
+ # same audience check. Miss it and an install can deliver the citing guide while withholding
515
+ # the cited one, which reads to the gate as two independently legal packages.
516
+ body_checks = 0
517
+ for name, body in bodies.items():
518
+ if name not in entries["guides"]:
519
+ continue # unclaimed file: rule 3 owns that
520
+ src_aud = audience(entries["guides"][name], errors, f"guides/{name}")
521
+ for cited in refs_from(body, entries["guides"]):
522
+ if cited == name:
523
+ continue
524
+ body_checks += 1
525
+ if delivered(name, repo) and not delivered(cited, repo):
526
+ errors.append(
527
+ f"router: guide {name} is delivered but points at {cited}, which declares "
528
+ f"audience: author and is withheld — the citation cannot resolve for a reader"
529
+ )
530
+ elif delivered(name, repo) and not covers(guide_aud.get(cited, "NEVER"), src_aud):
531
+ # Coverage is asked only of citations a packaged reader can follow. An
532
+ # audience:author guide never reaches an install, and a checkout has the whole
533
+ # tree regardless of domains — so its citation of a narrow-domain child is a
534
+ # gap for nobody, and failing it would force author docs to carry universal
535
+ # audiences they do not have.
536
+ errors.append(
537
+ f"router: guide {name} (audience {src_aud}) points at {cited} "
538
+ f"(audience {guide_aud.get(cited)}) — reader can hold {name} without it"
539
+ )
540
+ if body_checks == 0:
541
+ errors.append("non-vacuity: no guide->guide references were checked")
542
+
543
+ # -- 5. hook source_guide -----------------------------------------------
544
+ for name, entry in entries["hooks"].items():
545
+ src = entry.get("source_guide")
546
+ if src:
547
+ g = entries["guides"].get(src)
548
+ if g is None:
549
+ errors.append(f"hooks/{name}: source_guide {src} not in manifest guides")
550
+ elif (entry.get("tier"), sorted(entry.get("domains", []))) != (g.get("tier"), sorted(g.get("domains", []))):
551
+ errors.append(f"hooks/{name}: tier/domains differ from source guide {src}")
552
+
553
+ # -- informational token report ------------------------------------------
554
+ sizes = {}
555
+ for entry in mb:
556
+ key = entry.get("tier") if entry.get("tier") != "domain" else ",".join(entry.get("domains", ["?"])[:1])
557
+ hit = next((b for b in bullets if entry.get("anchor", "\0") in b), "")
558
+ sizes[key] = sizes.get(key, 0) + len(hit) // 4
559
+ for name, entry in entries["guides"].items():
560
+ key = entry.get("tier") if entry.get("tier") != "domain" else ",".join(entry.get("domains", ["?"])[:1])
561
+ p = repo / FILE_SECTIONS["guides"][0] / name
562
+ if p.is_file():
563
+ sizes[key] = sizes.get(key, 0) + len(p.read_text(encoding="utf-8")) // 4
564
+ return errors, sizes
565
+
566
+
567
+ def check_settings_template(manifest, repo=REPO):
568
+ """Every hook registered in the shipped settings template must be a hook the
569
+ manifest declares.
570
+
571
+ `merge_settings` only re-adds template entries whose command names a
572
+ manifest-declared hook (assemble.py:167-169), so an entry for anything else
573
+ is silently inert — it looks registered, ships to every user, and never runs.
574
+ That is how a machine-local registration ends up committed in a
575
+ deploy-managed file. Make it invalid instead of asking people to remember.
576
+ """
577
+ errors = []
578
+ spath = repo / "claude" / "settings.template.json"
579
+ if not spath.is_file():
580
+ return ["settings: claude/settings.template.json missing"]
581
+ names = set(manifest.get("hooks", {}))
582
+ entries = [(ev, h.get("command", ""))
583
+ for ev, evs in json.loads(spath.read_text(encoding="utf-8")).get("hooks", {}).items()
584
+ for en in evs for h in en.get("hooks", [])]
585
+ if not entries:
586
+ errors.append("non-vacuity: settings template registers no hooks")
587
+ for ev, cmd in entries:
588
+ # The assembler's matcher, not a substring: `.disabled` after the name satisfied
589
+ # `in` while merge_settings skipped the entry, so the gate blessed a template the
590
+ # install silently dropped. One matcher, imported, so they cannot drift apart.
591
+ if not any(hook_command_matches(cmd, n) for n in names):
592
+ errors.append(f"settings: {ev} hook {cmd!r} names no manifest hook "
593
+ f"(known: {sorted(names)}) — it would never deploy")
594
+ return errors
595
+
596
+
597
+ def self_test(manifest, bullets):
598
+ """Negative controls: each mutation MUST make the gate fail."""
599
+ import copy
600
+
601
+ muts = []
602
+ m1 = copy.deepcopy(manifest)
603
+ m1["bullets"] = m1["bullets"][1:]
604
+ muts.append(("dropped bullet entry", m1, bullets))
605
+ m2 = copy.deepcopy(manifest)
606
+ m2["bullets"][0]["anchor"] = "zz-no-such-phrase-zz"
607
+ muts.append(("anchor matches nothing (reword drift)", m2, bullets))
608
+ m3 = copy.deepcopy(manifest)
609
+ m3["bullets"][1]["anchor"] = m3["bullets"][0]["anchor"]
610
+ muts.append(("duplicate anchor claim", m3, bullets))
611
+ m4 = copy.deepcopy(manifest)
612
+ first_guide = next(iter(m4["guides"]))
613
+ del m4["guides"][first_guide]
614
+ muts.append((f"unclaimed guide {first_guide}", m4, bullets))
615
+ b5 = bullets + ["- a brand new bullet the manifest never heard of"]
616
+ muts.append(("bullet added without manifest entry", copy.deepcopy(manifest), b5))
617
+ m6 = copy.deepcopy(manifest)
618
+ m6["package_id"] = "@Acme/Builder" # uppercase is not a legal segment
619
+ muts.append(("malformed package_id", m6, bullets))
620
+ m7 = copy.deepcopy(manifest)
621
+ m7["hooks"] = {"renamed-hook.py": {"tier": "core", "domains": []}}
622
+ muts.append(("settings registers a hook the manifest does not declare", m7, bullets))
623
+
624
+ # A bullet that names a cross-domain guide in PROSE. This shipped for real: the reader
625
+ # held the rule and could not hold the guide, and a path-only scan reported clean. The
626
+ # control plants the handle rather than a path, so it fails only while handle detection
627
+ # is live — deleting `handles` or reverting refs_from() to the path regex brings it back.
628
+ handled = [(n, e) for n, e in manifest["guides"].items()
629
+ if e.get("handles") and e.get("tier") == "domain"]
630
+ if not handled:
631
+ raise SystemExit("self-test: no guide declares `handles` — the prose-router control "
632
+ "has no subject and would pass vacuously")
633
+ gname, gentry = handled[0]
634
+ victim = next(b for b in manifest["bullets"]
635
+ if b.get("tier") == "domain"
636
+ and not set(b.get("domains") or []) & set(gentry["domains"]))
637
+ hits = [b for b in bullets if victim["anchor"] in b]
638
+ assert len(hits) == 1, "self-test: prose-router control could not locate its bullet"
639
+ b8 = [b + f" (see the {gentry['handles'][0]})" if b is hits[0] else b for b in bullets]
640
+ muts.append((f"bullet names {gname} in prose across a domain boundary",
641
+ copy.deepcopy(manifest), b8))
642
+ # The dehyphenated PLURAL of the same promise: "the concept economy guides" resolved
643
+ # to nothing while the singular was caught — the referring pattern must own plurals.
644
+ spoken8 = gname[:-3].replace("-", " ")
645
+ b8p = [b + f" — read the {spoken8} guides first" if b is hits[0] else b for b in bullets]
646
+ muts.append((f"bullet names {gname} as a dehyphenated PLURAL prose reference",
647
+ copy.deepcopy(manifest), b8p))
648
+
649
+ # The orphan control removes a guide's ONLY pointer. It picks that guide by looking, not
650
+ # from a name typed here: a typed name stops being the right subject the moment that guide
651
+ # gains a second consumer, and the control would go quiet without saying so.
652
+ only_bullet = []
653
+ for name in manifest["guides"]:
654
+ cited = f"guides/{name}"
655
+ handles = manifest["guides"][name].get("handles", [])
656
+ refs = [b for b in bullets
657
+ if cited in b or any(h.lower() in b.lower() for h in handles)]
658
+ elsewhere = any(cited in (REPO / "claude" / "guides" / o).read_text(encoding="utf-8")
659
+ for o in manifest["guides"] if o != name)
660
+ if len(refs) == 1 and not elsewhere:
661
+ only_bullet.append((name, refs[0]))
662
+ if not only_bullet:
663
+ raise SystemExit("self-test: no guide is reachable through exactly one bullet, so the "
664
+ "orphan control has no subject and would pass vacuously")
665
+ o_name, o_line = only_bullet[0]
666
+ if len(only_bullet) < 2:
667
+ raise SystemExit("self-test: fewer than two single-pointer guides, so the island "
668
+ "control has no subject and would pass vacuously")
669
+ i_name, i_line = only_bullet[1]
670
+ # The manifest entry goes with the line. Dropping the line alone breaks the bijection in
671
+ # rule 2, which fails first — the control would then pass without the orphan check being
672
+ # consulted at all.
673
+ m_orphan = copy.deepcopy(manifest)
674
+ m_orphan["bullets"] = [e for e in m_orphan["bullets"] if e["anchor"] not in o_line]
675
+ if len(m_orphan["bullets"]) != len(manifest["bullets"]) - 1:
676
+ raise SystemExit("self-test: the orphan control could not drop exactly one manifest "
677
+ "entry with its bullet, so the mutation is not the one it claims")
678
+ muts.append((f"guide {o_name} left with no pointer at all",
679
+ m_orphan, [b for b in bullets if b != o_line]))
680
+
681
+ # A bullet that exists but ships to NOBODY cannot root a guide: reclassify the
682
+ # orphan guide's only router bullet as env-personal (assemble maps the tier to
683
+ # NEVER) and the guide must be reported as an orphan. The monolith line survives,
684
+ # so the bijection stays intact and the orphan leg itself is the one judged —
685
+ # which is why this control requires the orphan error by name instead of joining
686
+ # the generic any-error mutation list.
687
+ m_never = copy.deepcopy(manifest)
688
+ tgt = [e for e in m_never["bullets"] if e["anchor"] in o_line]
689
+ if len(tgt) != 1:
690
+ raise SystemExit("self-test: the never-delivered control could not target exactly "
691
+ "one manifest entry, so the mutation is not the one it claims")
692
+ tgt[0]["tier"] = "env-personal"
693
+ tgt[0].pop("domains", None)
694
+ errs_nv, _ = run_gate(m_never, bullets)
695
+ never_ok = any(o_name in e and "orphan" in e for e in errs_nv)
696
+ print(f"self-test [{'CAUGHT' if never_ok else 'MISSED'}] an env-personal (never-"
697
+ f"delivered) router bullet leaves {o_name} an orphan")
698
+
699
+ # One handle, one guide: copying a declared handle onto a second guide must fail as
700
+ # a duplicate declaration, not silently resolve one prose router to both targets.
701
+ m_dup = copy.deepcopy(manifest)
702
+ donor = next((n for n in m_dup["guides"] if m_dup["guides"][n].get("handles")), None)
703
+ if donor is None:
704
+ raise SystemExit("self-test: no guide declares a handle, so the duplicate-handle "
705
+ "control has no subject and would pass vacuously")
706
+ other = next(n for n in m_dup["guides"] if n != donor)
707
+ m_dup["guides"][other].setdefault("handles", []).append(
708
+ m_dup["guides"][donor]["handles"][0])
709
+ errs_dup, _ = run_gate(m_dup, bullets)
710
+ dup_ok = any("handle" in e and "declared by" in e for e in errs_dup)
711
+ print(f"self-test [{'CAUGHT' if dup_ok else 'MISSED'}] a handle declared by two "
712
+ f"guides fails as a duplicate declaration")
713
+
714
+ # Case is not identity: resolution lowercases, so the uniqueness index must too —
715
+ # a case variant of a declared handle is the same name and must fail the same way.
716
+ m_case = copy.deepcopy(manifest)
717
+ variant = m_case["guides"][donor]["handles"][0].upper()
718
+ if variant == m_case["guides"][donor]["handles"][0]:
719
+ raise SystemExit("self-test: the case-variant control is vacuous — the donor "
720
+ "handle has no case to vary")
721
+ other2 = next(n for n in m_case["guides"] if n != donor)
722
+ m_case["guides"][other2].setdefault("handles", []).append(variant)
723
+ errs_case, _ = run_gate(m_case, bullets)
724
+ case_ok = any("handle" in e and "declared by" in e for e in errs_case)
725
+ print(f"self-test [{'CAUGHT' if case_ok else 'MISSED'}] a case-variant duplicate "
726
+ f"handle fails as the same name")
727
+
728
+ # Declared×derived: a handle spelling ANOTHER guide's spoken name (plus a referring
729
+ # word) must fail as a collision — refs_from would resolve both targets from one
730
+ # prose pointer, the declared branch for the handle's owner and the derived branch
731
+ # for the guide the words actually name.
732
+ m_xd = copy.deepcopy(manifest)
733
+ target_g = next((n for n in m_xd["guides"] if "-" in n[:-3]), None)
734
+ if target_g is None:
735
+ raise SystemExit("self-test: no guide has a hyphenated stem, so the declared× "
736
+ "derived collision control has no subject and would pass vacuously")
737
+ victim_g = next(n for n in m_xd["guides"] if n != target_g)
738
+ m_xd["guides"][victim_g].setdefault("handles", []).append(
739
+ target_g[:-3].replace("-", " ") + " guide")
740
+ errs_xd, _ = run_gate(m_xd, bullets)
741
+ xd_ok = any("collides with" in e and target_g in e for e in errs_xd)
742
+ print(f"self-test [{'CAUGHT' if xd_ok else 'MISSED'}] a declared handle spelling "
743
+ f"another guide's derived name fails as a collision")
744
+
745
+ # Guide-to-guide audience. The mutation is a manifest narrowing, not a corpus edit, so it
746
+ # cannot trip the bijection the way the orphan control first did: take a guide that some
747
+ # OTHER guide cites and shrink its domain set to one the citing guide does not hold.
748
+ # The cited guide must be one NO bullet points at — a depth-chain child. Pick one a bullet
749
+ # also names and the forward leg fails on the same mutation, so the control would pass
750
+ # without this leg being consulted.
751
+ citers = []
752
+ for name in manifest["guides"]:
753
+ body = (REPO / "claude" / "guides" / name).read_text(encoding="utf-8")
754
+ for cited in re.findall(r"guides/([a-z0-9-]+\.md)", body):
755
+ if cited == name or cited not in manifest["guides"]:
756
+ continue
757
+ handles = manifest["guides"][cited].get("handles", [])
758
+ if any(f"guides/{cited}" in b or any(h.lower() in b.lower() for h in handles)
759
+ for b in bullets):
760
+ continue # a bullet names it too; forward leg would fire
761
+ citers.append((name, cited))
762
+ if not citers:
763
+ raise SystemExit("self-test: no guide cites another that no bullet names, so the "
764
+ "guide->guide control cannot be isolated from the forward leg")
765
+ c_from, c_to = citers[0]
766
+ m_cross = copy.deepcopy(manifest)
767
+ m_cross["guides"][c_to] = {"tier": "domain", "domains": ["office-work"]}
768
+ muts.append((f"guide {c_from} cites {c_to} after {c_to} moves to a domain it does not hold",
769
+ m_cross, bullets))
770
+
771
+ # Targeted: the settings leg must use the assembler's TOKEN rule, not a substring.
772
+ # `.disabled` appended after the hook name is the reviewer-verified shape: substring
773
+ # said deployed, merge_settings skipped it, the install carried no hook. Planted in a
774
+ # copy because the leg reads the template from disk.
775
+ import shutil as _sh, tempfile as _tf, json as _json
776
+ with _tf.TemporaryDirectory() as _td:
777
+ tmpl_tmp = pathlib.Path(_td)
778
+ _sh.copytree(REPO / "claude", tmpl_tmp / "claude")
779
+ sp = tmpl_tmp / "claude" / "settings.template.json"
780
+ data = _json.loads(sp.read_text(encoding="utf-8"))
781
+ for evs in data.get("hooks", {}).values():
782
+ for en in evs:
783
+ for h in en.get("hooks", []):
784
+ h["command"] = h.get("command", "") + ".disabled"
785
+ sp.write_text(_json.dumps(data), encoding="utf-8")
786
+ dis_errs = [e for e in check_settings_template(manifest, tmpl_tmp)
787
+ if e.startswith("settings:")]
788
+ print(f"self-test [{'CAUGHT' if dis_errs else 'MISSED'}] a '.disabled'-suffixed hook "
789
+ f"command fails the settings leg instead of passing as a substring")
790
+
791
+ # Targeted: the settings rule itself must speak, not just some neighbouring
792
+ # check tripping on the same mutation.
793
+ import copy as _c
794
+ m_off = _c.deepcopy(manifest)
795
+ m_off["hooks"] = {"renamed-hook.py": {"tier": "core", "domains": []}}
796
+ settings_errs = [e for e in check_settings_template(m_off) if e.startswith("settings:")]
797
+ print(f"self-test [{'CAUGHT' if settings_errs else 'MISSED'}] settings rule fires on its own")
798
+
799
+ # Targeted: an ambiguous derived run must resolve to nothing, and a full stem to exactly
800
+ # its own guide. Without the restriction, "the llm-capability-boundary guide" marked the
801
+ # base and both children consumed, so an unrelated mention of a parent could keep an
802
+ # unreachable child out of the orphan report.
803
+ owner_st = {}
804
+ for n in manifest["guides"]:
805
+ for r in prose_handles(n):
806
+ owner_st.setdefault(r, set()).add(n)
807
+ shared = next((r for r, o in sorted(owner_st.items())
808
+ if len(o) > 1 and all(r != g[:-3] for g in o)), None)
809
+ if shared is None:
810
+ raise SystemExit("self-test: no derived run is shared between guides, so the "
811
+ "ambiguity control has no subject and would pass vacuously")
812
+ amb = refs_from(f"see the {shared} guide", manifest["guides"])
813
+ print(f"self-test [{'CAUGHT' if not amb else 'MISSED'}] shared run {shared!r} "
814
+ f"resolves to no guide (got {amb})")
815
+ stem_owner = next((g for r, o in owner_st.items() if len(o) > 1
816
+ for g in o if r == g[:-3]), None)
817
+ stem_ok = True
818
+ if stem_owner is not None:
819
+ got = refs_from(f"see the {stem_owner[:-3]} guide", manifest["guides"])
820
+ stem_ok = got == [stem_owner]
821
+ print(f"self-test [{'CAUGHT' if stem_ok else 'MISSED'}] full stem resolves to "
822
+ f"{stem_owner} alone (got {got})")
823
+
824
+ # Targeted: a withheld guide's citation of a narrower child is a gap for nobody — the
825
+ # packaged reader never holds the citing guide, and a checkout has the whole tree. The
826
+ # firing side of this leg is the m_cross mutation above; this is the exemption side,
827
+ # planted in a throwaway copy because the leg reads bodies from disk.
828
+ import shutil, tempfile
829
+ withheld = next((n for n in manifest["guides"] if not delivered(n)), None)
830
+ narrow = next((n for n, e in manifest["guides"].items() if e.get("tier") == "domain"), None)
831
+ if withheld is None or narrow is None:
832
+ raise SystemExit("self-test: no withheld guide or no domain guide, so the "
833
+ "author-citation exemption has no subject and would pass vacuously")
834
+ with tempfile.TemporaryDirectory() as td:
835
+ tmp2 = pathlib.Path(td)
836
+ for sub in ("claude", "ko", "launch"):
837
+ if (REPO / sub).is_dir():
838
+ shutil.copytree(REPO / sub, tmp2 / sub)
839
+ gf = tmp2 / "claude" / "guides" / withheld
840
+ gf.write_text(gf.read_text(encoding="utf-8")
841
+ + f"\n\nSee `guides/{narrow}` for the workflow.\n", encoding="utf-8")
842
+ errs2, _ = run_gate(manifest, bullets, repo=tmp2)
843
+ false_hits = [e for e in errs2 if withheld in e and narrow in e]
844
+ print(f"self-test [{'CAUGHT' if not false_hits else 'MISSED'}] withheld {withheld} citing "
845
+ f"{narrow} is exempt from audience coverage")
846
+
847
+ # Launch missions: a deployed-form reference to a non-universal or withheld guide must
848
+ # fail; the real checkout-form reference must stay clean (it is the distill mission's
849
+ # deliberate shape, guarded by check-package instead).
850
+ with tempfile.TemporaryDirectory() as td:
851
+ tmp3 = pathlib.Path(td)
852
+ for sub in ("claude", "ko", "launch"):
853
+ if (REPO / sub).is_dir():
854
+ shutil.copytree(REPO / sub, tmp3 / sub)
855
+ lt = tmp3 / "launch" / "agent-launch.toml"
856
+ lt.write_text(lt.read_text(encoding="utf-8")
857
+ + f'\n[presets.zz-probe]\nmission = "read '
858
+ f'${{CLAUDE_CONFIG_DIR:-$HOME/.claude}}/guides/{narrow} first"\n',
859
+ encoding="utf-8")
860
+ errs3, _ = run_gate(manifest, bullets, repo=tmp3)
861
+ launch_hits = [e for e in errs3 if e.startswith("launch:") and narrow in e]
862
+ # The same plant in Codex-home form: the config drives both hosts, and the regex
863
+ # knowing only Claude homes was review's next find.
864
+ lt.write_text(lt.read_text(encoding="utf-8")
865
+ + f'\n[presets.zz-probe-cx]\nmission = "read '
866
+ f'${{CODEX_HOME}}/guides/{narrow} first"\n',
867
+ encoding="utf-8")
868
+ errs3c, _ = run_gate(manifest, bullets, repo=tmp3)
869
+ launch_hits_cx = [e for e in errs3c if e.startswith("launch:") and narrow in e]
870
+ print(f"self-test [{'CAUGHT' if launch_hits else 'MISSED'}] a deployed-form mission "
871
+ f"reference to domain-scoped {narrow} fails the launch leg")
872
+ print(f"self-test [{'CAUGHT' if launch_hits_cx else 'MISSED'}] the CODEX_HOME form "
873
+ f"of the same reference fails too")
874
+
875
+ # A TOML comment naming a guide reaches no agent: planting the orphaned guide's ONLY
876
+ # pointer as a comment must leave the orphan error standing.
877
+ with tempfile.TemporaryDirectory() as td:
878
+ tmp4 = pathlib.Path(td)
879
+ for sub in ("claude", "ko", "launch"):
880
+ if (REPO / sub).is_dir():
881
+ shutil.copytree(REPO / sub, tmp4 / sub)
882
+ lt4 = tmp4 / "launch" / "agent-launch.toml"
883
+ lt4.write_text(lt4.read_text(encoding="utf-8")
884
+ + f'\n# a comment mentioning guides/{o_name} reaches nobody\n',
885
+ encoding="utf-8")
886
+ errs4, _ = run_gate(m_orphan, [b for b in bullets if b != o_line], repo=tmp4)
887
+ still_orphan = any(o_name in e for e in errs4)
888
+ print(f"self-test [{'CAUGHT' if still_orphan else 'MISSED'}] a TOML comment naming "
889
+ f"{o_name} does not resurrect it as consumed")
890
+
891
+ # A fenced code sample in another guide is an example, not a router: with the real
892
+ # pointer dropped, the sample alone must leave the orphan error standing.
893
+ with tempfile.TemporaryDirectory() as td:
894
+ tmp7 = pathlib.Path(td)
895
+ for sub in ("claude", "ko", "launch"):
896
+ if (REPO / sub).is_dir():
897
+ shutil.copytree(REPO / sub, tmp7 / sub)
898
+ host7 = next(n for n in manifest["guides"] if n != o_name)
899
+ g7 = tmp7 / "claude" / "guides" / host7
900
+ fenced_orphan = True
901
+ for fence_open, fence_close, label in (
902
+ ("```", "```", "triple backtick"),
903
+ ("~~~", "~~~", "tilde"),
904
+ ("````", "````", "four backtick"),
905
+ ("```", "```python\nstill inside\n```", "info-string non-close"),
906
+ ("<!--", "-->", "HTML comment"),
907
+ ("<!--", "", "unclosed HTML comment"),
908
+ ("", "", "four-space indented"),
909
+ ("", "", "list-nested indented"),
910
+ ("", "", "tab indented"),
911
+ ("> ```", "> ```", "blockquoted fence"),
912
+ ("- > ```", " > ```", "list-blockquoted fence")):
913
+ base7 = g7.read_text(encoding="utf-8")
914
+ if label == "four-space indented":
915
+ sample7 = f"\n\n read guides/{o_name} for the flow\n"
916
+ elif label == "list-nested indented":
917
+ sample7 = (f"\n\n- a list item\n\n"
918
+ f" read guides/{o_name} for the flow\n")
919
+ elif label == "tab indented":
920
+ sample7 = f"\n\n\tread guides/{o_name} for the flow\n"
921
+ elif label == "blockquoted fence":
922
+ sample7 = (f"\n\n> ```\n> read guides/{o_name} for the flow\n> ```\n")
923
+ elif label == "list-blockquoted fence":
924
+ sample7 = (f"\n\n- > ```\n > read guides/{o_name} for the "
925
+ f"flow\n > ```\n")
926
+ else:
927
+ sample7 = (f"\n\n{fence_open}\nread guides/{o_name} for the "
928
+ f"flow\n{fence_close}\n")
929
+ g7.write_text(base7 + sample7, encoding="utf-8")
930
+ errs7, _ = run_gate(m_orphan, [b for b in bullets if b != o_line], repo=tmp7)
931
+ ok7 = any(o_name in e for e in errs7)
932
+ fenced_orphan = fenced_orphan and ok7
933
+ print(f"self-test [{'CAUGHT' if ok7 else 'MISSED'}] a {label} fenced sample "
934
+ f"naming {o_name} does not resurrect it as consumed")
935
+ g7.write_text(base7, encoding="utf-8")
936
+
937
+ # Two unrooted guides citing each other must BOTH stay orphans: reciprocal
938
+ # references are edges, not roots.
939
+ m_isl = copy.deepcopy(manifest)
940
+ m_isl["bullets"] = [e for e in m_isl["bullets"]
941
+ if e["anchor"] not in o_line and e["anchor"] not in i_line]
942
+ b_isl = [b for b in bullets if b not in (o_line, i_line)]
943
+ ga = tmp7 / "claude" / "guides" / o_name
944
+ gb = tmp7 / "claude" / "guides" / i_name
945
+ base_a, base_b = ga.read_text(encoding="utf-8"), gb.read_text(encoding="utf-8")
946
+ ga.write_text(base_a + f"\n\nSee `guides/{i_name}` too.\n", encoding="utf-8")
947
+ gb.write_text(base_b + f"\n\nSee `guides/{o_name}` too.\n", encoding="utf-8")
948
+ errs_isl, _ = run_gate(m_isl, b_isl, repo=tmp7)
949
+ island_ok = (any(o_name in e and "orphan" in e for e in errs_isl)
950
+ and any(i_name in e and "orphan" in e for e in errs_isl))
951
+ print(f"self-test [{'CAUGHT' if island_ok else 'MISSED'}] a reciprocal island "
952
+ f"({o_name} <-> {i_name}) is still orphaned — edges are not roots")
953
+ ga.write_text(base_a, encoding="utf-8")
954
+ gb.write_text(base_b, encoding="utf-8")
955
+
956
+ # The other direction: a REAL router placed after a fence whose sample contains
957
+ # a literal <!-- must still count — comments-before-fences deleted it through
958
+ # EOF and reported a false orphan.
959
+ g7.write_text(base7 + f"\n\n```\n<!-- a literal in an example\n```\n\n"
960
+ f"See `guides/{o_name}` for the flow.\n", encoding="utf-8")
961
+ errs7b, _ = run_gate(m_orphan, [b for b in bullets if b != o_line], repo=tmp7)
962
+ post_fence_ok = not any(o_name in e for e in errs7b)
963
+ print(f"self-test [{'CAUGHT' if post_fence_ok else 'MISSED'}] a real router after "
964
+ f"a fence containing a literal <!-- still counts as a consumer")
965
+
966
+ # And blockquoted prose riding a list marker still RENDERS: stripping the
967
+ # combined container must not delete a real router written as `- > see ...`.
968
+ g7.write_text(base7 + f"\n\n- > See `guides/{o_name}` for the flow.\n",
969
+ encoding="utf-8")
970
+ errs7c, _ = run_gate(m_orphan, [b for b in bullets if b != o_line], repo=tmp7)
971
+ listquote_prose_ok = not any(o_name in e for e in errs7c)
972
+ print(f"self-test [{'CAUGHT' if listquote_prose_ok else 'MISSED'}] a real router "
973
+ f"in list-blockquoted prose still counts as a consumer")
974
+ g7.write_text(base7, encoding="utf-8")
975
+
976
+ # A guides/ reference in a non-instruction field must fail loudly, not quietly count.
977
+ with tempfile.TemporaryDirectory() as td:
978
+ tmp5 = pathlib.Path(td)
979
+ for sub in ("claude", "ko", "launch"):
980
+ if (REPO / sub).is_dir():
981
+ shutil.copytree(REPO / sub, tmp5 / sub)
982
+ lt5 = tmp5 / "launch" / "agent-launch.toml"
983
+ lt5.write_text(lt5.read_text(encoding="utf-8")
984
+ + f'\nzz_inert = "see guides/{narrow} for details"\n',
985
+ encoding="utf-8")
986
+ # And the reviewer's sharper shape: a `mission` key OUTSIDE presets is equally
987
+ # inert — the leaf name does not make it instruction-bearing.
988
+ lt5.write_text(lt5.read_text(encoding="utf-8")
989
+ + f'\n[capabilities.zz-cap]\nmission = "read guides/{narrow}"\n',
990
+ encoding="utf-8")
991
+ # And the PROSE form of that shape: an inert field needs no guides/ path to
992
+ # send the reader somewhere — the spoken name is the same masquerade.
993
+ spoken5 = narrow[:-3].replace("-", " ")
994
+ lt5.write_text(lt5.read_text(encoding="utf-8")
995
+ + f'\n[capabilities.zz-cap2]\nmission = "Read the {spoken5} guide '
996
+ f'first."\n',
997
+ encoding="utf-8")
998
+ errs5, _ = run_gate(manifest, bullets, repo=tmp5)
999
+ inert_hits = [e for e in errs5 if "not instruction-bearing" in e]
1000
+ cap_hits = [e for e in errs5 if "capabilities.zz-cap.mission" in e]
1001
+ cap2_hits = [e for e in errs5 if "capabilities.zz-cap2.mission" in e]
1002
+ print(f"self-test [{'CAUGHT' if inert_hits else 'MISSED'}] a guide named in a "
1003
+ f"non-instruction TOML field fails loudly")
1004
+ print(f"self-test [{'CAUGHT' if cap_hits else 'MISSED'}] a mission key outside "
1005
+ f"presets is inert and fails loudly too")
1006
+ print(f"self-test [{'CAUGHT' if cap2_hits else 'MISSED'}] a PROSE reference in an "
1007
+ f"inert field fails loudly without a path form")
1008
+
1009
+ # PROSE form of the same promise: "Read the <spoken> guide" in a mission must fail
1010
+ # for a domain guide exactly as the deployed path does — form must not be a bypass.
1011
+ with tempfile.TemporaryDirectory() as td:
1012
+ tmp6 = pathlib.Path(td)
1013
+ for sub in ("claude", "ko", "launch"):
1014
+ if (REPO / sub).is_dir():
1015
+ shutil.copytree(REPO / sub, tmp6 / sub)
1016
+ lt6 = tmp6 / "launch" / "agent-launch.toml"
1017
+ spoken6 = narrow[:-3].replace("-", " ")
1018
+ lt6.write_text(lt6.read_text(encoding="utf-8")
1019
+ + f'\n[presets.zz-prose]\nmission = "Read the {spoken6} guide first."\n',
1020
+ encoding="utf-8")
1021
+ errs6, _ = run_gate(manifest, bullets, repo=tmp6)
1022
+ prose_hits = [e for e in errs6 if e.startswith("launch:") and narrow in e]
1023
+ print(f"self-test [{'CAUGHT' if prose_hits else 'MISSED'}] a PROSE mission reference "
1024
+ f"to domain-scoped {narrow} fails without needing a path form")
1025
+
1026
+ # One preset's guarded checkout reference must not launder another preset's prose
1027
+ # reference to the same guide.
1028
+ with tempfile.TemporaryDirectory() as td:
1029
+ tmp8 = pathlib.Path(td)
1030
+ for sub in ("claude", "ko", "launch"):
1031
+ if (REPO / sub).is_dir():
1032
+ shutil.copytree(REPO / sub, tmp8 / sub)
1033
+ lt8 = tmp8 / "launch" / "agent-launch.toml"
1034
+ spoken8b = narrow[:-3].replace("-", " ")
1035
+ lt8.write_text(lt8.read_text(encoding="utf-8")
1036
+ + f'\n[presets.zz-ck]\nmission = "read claude/guides/{narrow} in '
1037
+ f'the agent-bios checkout"\n'
1038
+ f'[presets.zz-pr]\nmission = "Read the {spoken8b} guide first."\n',
1039
+ encoding="utf-8")
1040
+ errs8, _ = run_gate(manifest, bullets, repo=tmp8)
1041
+ mixed_hits = [e for e in errs8 if e.startswith("launch:") and narrow in e]
1042
+ print(f"self-test [{'CAUGHT' if mixed_hits else 'MISSED'}] a checkout reference in one "
1043
+ f"preset does not exempt a prose reference in another")
1044
+
1045
+ # Both forms in ONE mission: the checkout path must not exempt the prose pointer
1046
+ # standing beside it.
1047
+ with tempfile.TemporaryDirectory() as td:
1048
+ tmp9 = pathlib.Path(td)
1049
+ for sub in ("claude", "ko", "launch"):
1050
+ if (REPO / sub).is_dir():
1051
+ shutil.copytree(REPO / sub, tmp9 / sub)
1052
+ lt9 = tmp9 / "launch" / "agent-launch.toml"
1053
+ spoken9 = narrow[:-3].replace("-", " ")
1054
+ lt9.write_text(lt9.read_text(encoding="utf-8")
1055
+ + f'\n[presets.zz-both]\nmission = "read claude/guides/{narrow} in '
1056
+ f'the agent-bios checkout, or just read the {spoken9} guide."\n',
1057
+ encoding="utf-8")
1058
+ errs9, _ = run_gate(manifest, bullets, repo=tmp9)
1059
+ same_hits = [e for e in errs9 if e.startswith("launch:") and narrow in e]
1060
+ print(f"self-test [{'CAUGHT' if same_hits else 'MISSED'}] a checkout path does not "
1061
+ f"exempt a prose pointer in the SAME mission")
1062
+
1063
+ # Targeted: the consumer legs resolve handles, not only paths. All three call refs_from,
1064
+ # so proving the resolver sees a handle-only mention proves the legs do. Without it a child
1065
+ # cited only in prose reads as inert while rule 4 validates that same reference.
1066
+ handled = next((n for n, e in manifest["guides"].items() if e.get("handles")), None)
1067
+ if handled is None:
1068
+ raise SystemExit("self-test: no guide declares handles, so the handle-resolution "
1069
+ "assertion has no subject and would pass vacuously")
1070
+ probe = f"see the {manifest['guides'][handled]['handles'][0]} for the rest"
1071
+ handle_ok = handled in refs_from(probe, manifest["guides"])
1072
+ # A handle must match as a complete phrase: "multi-model guidelines" contains the
1073
+ # handle "multi-model guide" and is ordinary prose, not a router.
1074
+ hprefix = f"note the {manifest['guides'][handled]['handles'][0]}lines here"
1075
+ handle_bounded = handled not in refs_from(hprefix, manifest["guides"])
1076
+ print(f"self-test [{'CAUGHT' if handle_bounded else 'MISSED'}] a handle embedded in a "
1077
+ f"longer word does not resolve")
1078
+ dehy = next((n for n in manifest["guides"] if "-" in n[:-3]
1079
+ and prose_handles(n) and n[:-3] in
1080
+ [r for r in prose_handles(n)]), None)
1081
+ dehy_ok = True
1082
+ if dehy is not None:
1083
+ spoken = dehy[:-3].replace("-", " ")
1084
+ dehy_ok = (dehy in refs_from(f"read the {spoken} guide", manifest["guides"])
1085
+ and dehy in refs_from(f"read the {spoken} document", manifest["guides"]))
1086
+ print(f"self-test [{'CAUGHT' if dehy_ok else 'MISSED'}] a dehyphenated reference "
1087
+ f"('the {spoken} guide') resolves to {dehy}")
1088
+ print(f"self-test [{'CAUGHT' if handle_ok else 'MISSED'}] a handle-only reference "
1089
+ f"resolves to {handled}")
1090
+
1091
+ failed = [] if settings_errs else ["settings rule fires on its own"]
1092
+ if amb:
1093
+ failed.append("shared run resolves to no guide")
1094
+ if not stem_ok:
1095
+ failed.append("full stem resolves uniquely")
1096
+ if false_hits:
1097
+ failed.append("withheld-guide citation exempt from coverage")
1098
+ if not dehy_ok:
1099
+ failed.append("dehyphenated reference resolves")
1100
+ if not dis_errs:
1101
+ failed.append("disabled-suffixed hook command fails the settings leg")
1102
+ if not launch_hits:
1103
+ failed.append("deployed-form launch reference to a domain guide fails")
1104
+ if not launch_hits_cx:
1105
+ failed.append("CODEX_HOME-form launch reference fails")
1106
+ if not still_orphan:
1107
+ failed.append("comment mention does not count as a consumer")
1108
+ if not fenced_orphan:
1109
+ failed.append("fenced sample does not count as a consumer")
1110
+ if not post_fence_ok:
1111
+ failed.append("real router after a <!--bearing fence still counts")
1112
+ if not listquote_prose_ok:
1113
+ failed.append("real router in list-blockquoted prose still counts")
1114
+ if not never_ok:
1115
+ failed.append("never-delivered bullet does not root a guide")
1116
+ if not island_ok:
1117
+ failed.append("reciprocal island stays orphaned")
1118
+ if not inert_hits:
1119
+ failed.append("non-instruction field naming a guide fails loudly")
1120
+ if not cap_hits:
1121
+ failed.append("mission outside presets is inert")
1122
+ if not cap2_hits:
1123
+ failed.append("prose reference in an inert field fails loudly")
1124
+ if not dup_ok:
1125
+ failed.append("duplicate declared handle fails as a duplicate declaration")
1126
+ if not xd_ok:
1127
+ failed.append("declared handle colliding with a derived name fails")
1128
+ if not case_ok:
1129
+ failed.append("case-variant duplicate handle fails as the same name")
1130
+ if not prose_hits:
1131
+ failed.append("prose mission reference validated like a path")
1132
+ if not mixed_hits:
1133
+ failed.append("checkout occurrence does not launder a prose occurrence")
1134
+ if not same_hits:
1135
+ failed.append("checkout path does not exempt prose in the same mission")
1136
+ if not handle_ok:
1137
+ failed.append("handle-only reference resolves")
1138
+ if not handle_bounded:
1139
+ failed.append("handle embedded in a longer word must not resolve")
1140
+ for name, mm, bb in muts:
1141
+ errs, _ = run_gate(mm, bb)
1142
+ if not errs:
1143
+ failed.append(name)
1144
+ print(f"self-test [{'CAUGHT' if errs else 'MISSED'}] {name}")
1145
+ return failed
1146
+
1147
+
1148
+ def main():
1149
+ manifest = load_manifest()
1150
+ bullets = corpus_bullets()
1151
+ extra = [a for a in sys.argv[1:] if a != "--self-test"]
1152
+ if extra:
1153
+ sys.exit(f"check-domains: refusing unknown argument(s) {extra!r}. This gate "
1154
+ f"validates the core manifest only; a package manifest path would be "
1155
+ f"silently ignored, reporting a check that never ran. Parameterizing "
1156
+ f"it is contract v2 §3.")
1157
+ if "--self-test" in sys.argv:
1158
+ missed = self_test(manifest, bullets)
1159
+ if missed:
1160
+ print(f"SELF-TEST FAIL: gate missed: {missed}")
1161
+ return 1
1162
+ print("SELF-TEST OK: every negative control was caught")
1163
+ return 0
1164
+ errors, sizes = run_gate(manifest, bullets)
1165
+ for e in errors:
1166
+ print(f"FAIL: {e}")
1167
+ print("-- package token estimate (informational) --")
1168
+ for k in sorted(sizes):
1169
+ print(f" {k}: ~{sizes[k]} tokens")
1170
+ if errors:
1171
+ print(f"DOMAINS GATE FAIL: {len(errors)} violation(s)")
1172
+ return 1
1173
+ print(f"DOMAINS GATE OK: {len(bullets)} bullets bijective, all files claimed, routers co-packaged and delivered, no orphan guides")
1174
+ return 0
1175
+
1176
+
1177
+ if __name__ == "__main__":
1178
+ sys.exit(main())