agent-bios 0.9.9 → 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.
@@ -13,9 +13,13 @@ Blocking checks (any violation exits 1, all violations listed):
13
13
  in claude/CLAUDE.md, and every bullet is claimed by exactly ONE entry
14
14
  3. file coverage: every file in claude/guides|hooks|agents claimed exactly
15
15
  once; every claimed file exists on disk
16
- 4. router-guide co-package: a bullet referencing guides/<g>.md must have an
17
- audience covered by that guide's audience (universal bullet -> universal
18
- guide; domain bullet -> guide covering all its domains)
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
19
23
  5. hook source_guide: a hook naming its source guide must carry the same
20
24
  tier + domain set as that guide
21
25
  6. non-vacuity: every subject set this gate judges is non-empty, so a green
@@ -34,6 +38,7 @@ import sys
34
38
 
35
39
  sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
36
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
37
42
 
38
43
  REPO = pathlib.Path(__file__).resolve().parent.parent
39
44
  MANIFEST = REPO / "compose" / "domains.json"
@@ -75,6 +80,20 @@ def audience(entry, errors, ctx):
75
80
  return "NEVER"
76
81
 
77
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
+
78
97
  def covers(guide_aud, bullet_aud):
79
98
  if guide_aud == "UNIVERSAL":
80
99
  return True
@@ -83,6 +102,75 @@ def covers(guide_aud, bullet_aud):
83
102
  return isinstance(guide_aud, frozenset) and guide_aud >= bullet_aud
84
103
 
85
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
+
86
174
  def run_gate(manifest, bullets, repo=REPO):
87
175
  errors = []
88
176
  tiers = set(manifest.get("tiers", []))
@@ -154,18 +242,69 @@ def run_gate(manifest, bullets, repo=REPO):
154
242
  errors.append(f"{key}: file {name} in {rel} not claimed by the manifest")
155
243
 
156
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.
157
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
158
286
  anchor_of = {id(e): e.get("anchor", "?") for e in mb}
159
287
  ref_checks = 0
288
+ undelivered = set()
160
289
  for entry in mb:
161
290
  hits = [b for b in bullets if entry.get("anchor", "\0") in b]
162
291
  if len(hits) != 1:
163
292
  continue # already reported by bijection
164
293
  b_aud = audience(entry, errors, f"bullet {entry.get('anchor', '')!r:.40}")
165
- for g in re.findall(r"guides/([a-z0-9-]+\.md)", hits[0]):
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"]):
166
300
  ref_checks += 1
167
301
  if g not in guide_aud:
168
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
+ )
169
308
  elif not covers(guide_aud[g], b_aud):
170
309
  errors.append(
171
310
  f"router: bullet {entry['anchor']!r:.40} (audience {b_aud}) references guide {g} "
@@ -174,6 +313,233 @@ def run_gate(manifest, bullets, repo=REPO):
174
313
  if ref_checks == 0:
175
314
  errors.append("non-vacuity: no router->guide references were checked")
176
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
+
177
543
  # -- 5. hook source_guide -----------------------------------------------
178
544
  for name, entry in entries["hooks"].items():
179
545
  src = entry.get("source_guide")
@@ -219,7 +585,10 @@ def check_settings_template(manifest, repo=REPO):
219
585
  if not entries:
220
586
  errors.append("non-vacuity: settings template registers no hooks")
221
587
  for ev, cmd in entries:
222
- if not any(("/hooks/" + n) in cmd for n in names):
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):
223
592
  errors.append(f"settings: {ev} hook {cmd!r} names no manifest hook "
224
593
  f"(known: {sorted(names)}) — it would never deploy")
225
594
  return errors
@@ -252,6 +621,173 @@ def self_test(manifest, bullets):
252
621
  m7["hooks"] = {"renamed-hook.py": {"tier": "core", "domains": []}}
253
622
  muts.append(("settings registers a hook the manifest does not declare", m7, bullets))
254
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
+
255
791
  # Targeted: the settings rule itself must speak, not just some neighbouring
256
792
  # check tripping on the same mutation.
257
793
  import copy as _c
@@ -260,7 +796,347 @@ def self_test(manifest, bullets):
260
796
  settings_errs = [e for e in check_settings_template(m_off) if e.startswith("settings:")]
261
797
  print(f"self-test [{'CAUGHT' if settings_errs else 'MISSED'}] settings rule fires on its own")
262
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
+
263
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")
264
1140
  for name, mm, bb in muts:
265
1141
  errs, _ = run_gate(mm, bb)
266
1142
  if not errs:
@@ -294,7 +1170,7 @@ def main():
294
1170
  if errors:
295
1171
  print(f"DOMAINS GATE FAIL: {len(errors)} violation(s)")
296
1172
  return 1
297
- print(f"DOMAINS GATE OK: {len(bullets)} bullets bijective, all files claimed, routers co-packaged")
1173
+ print(f"DOMAINS GATE OK: {len(bullets)} bullets bijective, all files claimed, routers co-packaged and delivered, no orphan guides")
298
1174
  return 0
299
1175
 
300
1176