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.
- package/DEPENDENCIES.md +19 -19
- package/README.md +43 -12
- package/claude/CLAUDE.md +5 -41
- package/claude/guides/claude-prompting.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +22 -4
- package/claude/guides/coding-staged-workflow.md +49 -15
- package/claude/guides/concept-economy.md +187 -0
- package/claude/guides/documentation-hygiene.md +112 -0
- package/claude/guides/gpt-prompting.md +1 -1
- package/claude/guides/learning-flow.md +5 -5
- package/claude/guides/llm-capability-boundary.md +6 -1
- package/claude/guides/review-request.md +9 -7
- package/claude/guides/session-distill-workflow.md +19 -9
- package/claude/guides/tooling-gotchas.md +26 -0
- package/claude/guides/verification-discipline.md +166 -0
- package/claude/hooks/tooling-gotchas-hook.py +329 -12
- package/codex/AGENTS.md +5 -41
- package/codex/guides/claude-prompting.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +22 -4
- package/codex/guides/coding-staged-workflow.md +49 -15
- package/codex/guides/concept-economy.md +187 -0
- package/codex/guides/documentation-hygiene.md +112 -0
- package/codex/guides/gpt-prompting.md +1 -1
- package/codex/guides/learning-flow.md +5 -5
- package/codex/guides/llm-capability-boundary.md +6 -1
- package/codex/guides/review-request.md +9 -7
- package/codex/guides/session-distill-workflow.md +19 -9
- package/codex/guides/tooling-gotchas.md +26 -0
- package/codex/guides/verification-discipline.md +166 -0
- package/{scripts → compose}/assemble.py +194 -17
- package/{scripts → compose}/canary.sh +14 -5
- package/compose/check-domains.py +1178 -0
- package/{config → compose}/domains.json +11 -44
- package/{scripts → compose}/pkgid.py +8 -1
- package/compose/prune-backups.py +204 -0
- package/{scripts → compose}/register-hooks.py +3 -3
- package/install.sh +1233 -0
- package/launch/agent-launch.py +5294 -0
- package/launch/agent-launch.toml +376 -0
- package/{scripts → launch}/check-prompting-targets.sh +1 -1
- package/{scripts → launch}/provision-venv.sh +1 -1
- package/{scripts → learn}/check-learning.py +7 -7
- package/{scripts → learn}/collect-learning.py +10 -10
- package/{config → learn}/learning.schema.json +3 -3
- package/{scripts → learn}/migrate-learnings.py +95 -54
- package/{scripts → learn}/redact.py +4 -4
- package/package.json +32 -27
- package/provenance.json +1 -0
- package/wrappers/claude-run.sh +162 -0
- package/{scripts → wrappers}/codex-run.sh +62 -6
- package/config/agent-launch.toml +0 -143
- package/scripts/agent-launch.py +0 -2350
- package/scripts/check-domains.py +0 -296
- package/scripts/check-parity.sh +0 -2003
- package/scripts/install.sh +0 -819
- /package/{shell → launch}/agent-launch.zsh +0 -0
- /package/{config → learn}/promotions.json +0 -0
- /package/{scripts/session-cost.py → session-cost.py} +0 -0
- /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())
|