agent-bios 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/DEPENDENCIES.md +37 -13
  2. package/README.md +421 -32
  3. package/claude/CLAUDE.md +3 -3
  4. package/claude/agents/frontier.md +1 -1
  5. package/claude/agents/sweep.md +3 -3
  6. package/claude/agents/workhorse.md +2 -2
  7. package/claude/guides/claude-prompting.md +72 -39
  8. package/claude/guides/cli-multi-model-workflow.md +33 -15
  9. package/claude/guides/gpt-prompting.md +103 -39
  10. package/claude/guides/review-request.md +27 -0
  11. package/claude/guides/session-distill-workflow.md +54 -2
  12. package/claude/guides/slide-writing/RUNBOOK.md +137 -0
  13. package/claude/guides/slide-writing/scripts/pair.py +979 -0
  14. package/claude/guides/slide-writing/scripts/render.mjs +82 -0
  15. package/claude/guides/slide-writing.md +195 -0
  16. package/claude/guides/svg-visualization-guide.md +9 -0
  17. package/claude/guides/tooling-gotchas.md +1 -1
  18. package/claude/guides/verification-discipline.md +5 -1
  19. package/claude/hooks/tooling-gotchas-hook.py +9 -14
  20. package/claude/skills/understand/SKILL.md +83 -0
  21. package/codex/AGENTS.md +3 -3
  22. package/codex/agents/frontier.toml +2 -1
  23. package/codex/agents/reviewer.toml +1 -1
  24. package/codex/agents/sweep.toml +3 -3
  25. package/codex/agents/workhorse.toml +1 -1
  26. package/codex/config-additions.toml +1 -1
  27. package/codex/guides/claude-prompting.md +72 -39
  28. package/codex/guides/cli-multi-model-workflow.md +33 -15
  29. package/codex/guides/gpt-prompting.md +103 -39
  30. package/codex/guides/review-request.md +27 -0
  31. package/codex/guides/session-distill-workflow.md +54 -2
  32. package/codex/guides/slide-writing/RUNBOOK.md +137 -0
  33. package/codex/guides/slide-writing/scripts/pair.py +979 -0
  34. package/codex/guides/slide-writing/scripts/render.mjs +82 -0
  35. package/codex/guides/slide-writing.md +195 -0
  36. package/codex/guides/svg-visualization-guide.md +9 -0
  37. package/codex/guides/tooling-gotchas.md +1 -1
  38. package/codex/guides/verification-discipline.md +5 -1
  39. package/compose/assemble.py +290 -14
  40. package/compose/bootstrap/SKILL.md +129 -0
  41. package/compose/check-domains.py +102 -9
  42. package/compose/corpus-state.py +4 -0
  43. package/compose/corpus.py +387 -0
  44. package/compose/corpus_catalog.py +931 -0
  45. package/compose/corpus_install.py +1670 -0
  46. package/compose/corpus_session.py +825 -0
  47. package/compose/corpus_store.py +1423 -0
  48. package/compose/corpus_transaction.py +256 -0
  49. package/compose/corpus_ui.py +655 -0
  50. package/compose/corpus_understand.py +522 -0
  51. package/compose/domains.json +102 -100
  52. package/compose/register-hooks.py +6 -8
  53. package/install.sh +84 -18
  54. package/launch/agent-launch.py +1413 -193
  55. package/launch/agent-launch.toml +12 -16
  56. package/launch/agent-launch.zsh +16 -2
  57. package/launch/i18n/en.toml +135 -7
  58. package/launch/i18n/ja.toml +135 -7
  59. package/launch/i18n/ko.toml +135 -7
  60. package/launch/shell_integration.py +267 -0
  61. package/learn/collect-learning.py +46 -19
  62. package/learn/migrate-learnings.py +10 -1
  63. package/package.json +13 -3
  64. package/provenance.json +1 -1
  65. package/session-cost.py +22 -2
  66. package/wrappers/codex-helm.sh +3 -3
@@ -0,0 +1,931 @@
1
+ #!/usr/bin/env python3
2
+ """Canonical corpus inventory and private snapshot compiler.
3
+
4
+ The catalog is intentionally a read-only view over package manifests and the
5
+ canonical Claude source tree. It does not know configuration-home paths and
6
+ never writes outside the destination handed to :func:`compile_items`.
7
+
8
+ The module is the first consumer of the stable ``item_id`` values in
9
+ ``domains.json``. Those ids identify authored items; emitted paths and content
10
+ are deliberately derived separately so a body or surface edit cannot change a
11
+ CorpusRef.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import ast
16
+ import json
17
+ import hashlib
18
+ import os
19
+ import pathlib
20
+ import re
21
+ import shlex
22
+ import sys
23
+ import tempfile
24
+ from collections.abc import Iterable
25
+ from typing import Any
26
+
27
+ try:
28
+ from pkgid import CORE, is_valid as valid_package_id
29
+ except ImportError: # pragma: no cover - package import from repository root
30
+ from compose.pkgid import CORE, is_valid as valid_package_id
31
+
32
+
33
+ SCHEMA_VERSION = 1
34
+ SURFACES = frozenset(("always", "relevant", "requested", "event", "delegated"))
35
+ KINDS = frozenset(("rule", "guide", "skill", "hook", "agent"))
36
+ TIERS = frozenset(("core", "domain", "env-personal", "infra"))
37
+ _SAFE_COMPONENT = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
38
+ CLAUDE_HOOK_EVENTS = frozenset((
39
+ "PreToolUse", "PostToolUse", "PostToolUseFailure", "Notification",
40
+ "UserPromptSubmit", "SessionStart", "SessionEnd", "Stop", "SubagentStart",
41
+ "SubagentStop", "PreCompact", "PermissionRequest", "TeammateIdle",
42
+ "TaskCompleted", "ConfigChange", "WorktreeCreate", "WorktreeRemove",
43
+ "Setup", "InstructionsLoaded", "UserPromptExpansion", "MessageDisplay",
44
+ "PermissionDenied", "PostToolBatch", "TaskCreated", "StopFailure",
45
+ "CwdChanged", "DirectoryAdded", "FileChanged", "PostCompact",
46
+ "PreModelSwitch", "PostModelSwitch", "Elicitation", "ElicitationResult",
47
+ ))
48
+ # Command-event vocabulary from the two hosts' hook references (2026-09-11).
49
+ # Authoring accepts the union; compilation checks the selected host. A host-only
50
+ # event stays authored and becomes unavailable on the other host, never renamed.
51
+ CODEX_HOOK_EVENTS = frozenset((
52
+ "PreToolUse", "PermissionRequest", "PostToolUse", "PreCompact", "PostCompact",
53
+ "SessionStart", "SessionEnd", "UserPromptSubmit", "SubagentStart",
54
+ "SubagentStop", "Stop", "Interrupt",
55
+ ))
56
+ HOOK_EVENTS_BY_HOST = {"claude": CLAUDE_HOOK_EVENTS, "codex": CODEX_HOOK_EVENTS}
57
+ HOOK_EVENTS = CLAUDE_HOOK_EVENTS | CODEX_HOOK_EVENTS
58
+
59
+
60
+ class CatalogError(ValueError):
61
+ """A catalog cannot be compiled safely."""
62
+
63
+
64
+ def validate_hook_binding(hook: Any, host: str | None = None) -> None:
65
+ if not isinstance(hook, dict) or set(hook) != {"event", "matcher"}:
66
+ raise CatalogError("hook binding must contain only event and matcher")
67
+ event, matcher = hook.get("event"), hook.get("matcher")
68
+ events = HOOK_EVENTS if host is None else HOOK_EVENTS_BY_HOST[host]
69
+ if not isinstance(event, str) or event not in events:
70
+ raise CatalogError(f"unsupported {host + ' ' if host else ''}hook event {event!r}")
71
+ if not isinstance(matcher, str) or not matcher or any(char in matcher for char in "\n\r\x00"):
72
+ raise CatalogError("hook matcher must be one non-empty line")
73
+
74
+
75
+ def validate_native_hook_config(value: Any, host: str) -> None:
76
+ """Validate compiler-owned command registrations, not arbitrary host config."""
77
+ if not isinstance(value, dict):
78
+ raise CatalogError("native hooks must be an event mapping")
79
+ for event, groups in value.items():
80
+ if not isinstance(groups, list) or not groups:
81
+ raise CatalogError("native hook event must contain matcher groups")
82
+ for group in groups:
83
+ if not isinstance(group, dict) or set(group) != {"matcher", "hooks"}:
84
+ raise CatalogError("invalid native hook matcher group")
85
+ validate_hook_binding({"event": event, "matcher": group["matcher"]}, host)
86
+ handlers = group["hooks"]
87
+ if not isinstance(handlers, list) or not handlers:
88
+ raise CatalogError("native hook group must contain commands")
89
+ for handler in handlers:
90
+ if (not isinstance(handler, dict) or set(handler) != {"type", "command"}
91
+ or handler["type"] != "command" or not isinstance(handler["command"], str)
92
+ or not handler["command"] or "\x00" in handler["command"]):
93
+ raise CatalogError("invalid native hook command")
94
+
95
+
96
+ def _read_utf8(path: pathlib.Path) -> str:
97
+ if path.is_symlink():
98
+ raise CatalogError(f"symlink input is not a corpus member: {path}")
99
+ try:
100
+ return path.read_text(encoding="utf-8")
101
+ except UnicodeDecodeError as exc:
102
+ raise CatalogError(f"corpus member is not UTF-8: {path}") from exc
103
+ except OSError as exc:
104
+ raise CatalogError(f"cannot read corpus member: {path}") from exc
105
+
106
+
107
+ def _relative_path(value: str, context: str) -> str:
108
+ if not isinstance(value, str) or not value:
109
+ raise CatalogError(f"{context}: member path must be a non-empty string")
110
+ pure = pathlib.PurePosixPath(value)
111
+ if pure.is_absolute() or ".." in pure.parts or "." in pure.parts:
112
+ raise CatalogError(f"{context}: unsafe member path {value!r}")
113
+ if any(not _SAFE_COMPONENT.match(part) for part in pure.parts):
114
+ raise CatalogError(f"{context}: unsafe member path {value!r}")
115
+ return pure.as_posix()
116
+
117
+
118
+ def _item_ref(package_id: str, item_id: str) -> str:
119
+ return f"{package_id}:{item_id}"
120
+
121
+
122
+ def _public_item(item: dict[str, Any]) -> dict[str, Any]:
123
+ """Deep-copy through JSON to make the return value safe for UI callers."""
124
+ return json.loads(json.dumps(item, ensure_ascii=False))
125
+
126
+
127
+ def _content_members(item: dict[str, Any]) -> dict[str, str]:
128
+ """Validate and copy the member map used as the content authority."""
129
+ members = item.get("members")
130
+ if not isinstance(members, dict) or not members:
131
+ raise CatalogError("content members must be a non-empty object")
132
+ context = str(item.get("ref", "content"))
133
+ normalized: dict[str, str] = {}
134
+ for path, content in members.items():
135
+ path = _relative_path(path, context)
136
+ if path in normalized:
137
+ raise CatalogError(f"{context}: duplicate member path {path!r}")
138
+ if not isinstance(content, str):
139
+ raise CatalogError(f"{context}: member {path!r} must be UTF-8 text")
140
+ normalized[path] = content
141
+ return normalized
142
+
143
+
144
+ def _legacy_primary_member(body: str, members: dict[str, str]) -> str | None:
145
+ """Resolve only deterministic legacy representations; never guess intent."""
146
+ matching = [path for path, content in members.items() if content == body]
147
+ if len(matching) == 1:
148
+ return matching[0]
149
+ skills = [path for path in members if path == "SKILL.md" or path.endswith("/SKILL.md")]
150
+ if len(skills) == 1:
151
+ return skills[0]
152
+ # Generated rule entries historically exposed the line without the file's
153
+ # one trailing newline. It is a known serialization difference, not intent.
154
+ rules = [path for path, content in members.items() if path == "rule.md" and content == body + "\n"]
155
+ if len(rules) == 1:
156
+ return rules[0]
157
+ return None
158
+
159
+
160
+ def _deterministic_primary_path(members: dict[str, str]) -> str | None:
161
+ """Find a legacy path only where the path itself is unambiguous."""
162
+ skills = [path for path in members if path == "SKILL.md" or path.endswith("/SKILL.md")]
163
+ if len(skills) == 1:
164
+ return skills[0]
165
+ if len(members) == 1:
166
+ return next(iter(members))
167
+ return None
168
+
169
+
170
+ def _content_conflict(body: str, members: dict[str, str]) -> dict[str, Any]:
171
+ return {
172
+ "reason": "legacy body has no unambiguous matching primary_member",
173
+ "members": sorted(members),
174
+ }
175
+
176
+
177
+ def normalize_content(item: dict[str, Any], *, allow_legacy: bool = False) -> dict[str, Any]:
178
+ """Return a copied item whose ``body`` is derived from ``primary_member``.
179
+
180
+ Old snapshots may lack ``primary_member``. They are only migrated in
181
+ memory when a deterministic source member exists. An unresolved snapshot
182
+ remains readable with ``content_conflict`` and its original body/members;
183
+ callers must not compile it until an explicit update reconciles it.
184
+ """
185
+ if not isinstance(item, dict):
186
+ raise CatalogError("content item must be an object")
187
+ result = _public_item(item)
188
+ members = _content_members(result)
189
+ result["members"] = members
190
+ primary = result.get("primary_member")
191
+ if primary is not None:
192
+ if not isinstance(primary, str) or primary not in members:
193
+ raise CatalogError("primary_member must name an existing member")
194
+ body = result.get("body")
195
+ if body is not None and not isinstance(body, str):
196
+ raise CatalogError("content body must be UTF-8 text")
197
+ generated_rule_newline = isinstance(body, str) and primary == "rule.md" and members[primary] == body + "\n"
198
+ if body is not None and body != members[primary] and not generated_rule_newline:
199
+ if not allow_legacy:
200
+ raise CatalogError("body conflicts with primary_member member content")
201
+ result.pop("primary_member", None)
202
+ result["content_conflict"] = _content_conflict(body, members)
203
+ return result
204
+ result["primary_member"] = primary
205
+ result["body"] = members[primary]
206
+ result.pop("content_conflict", None)
207
+ return result
208
+ if not allow_legacy:
209
+ raise CatalogError("primary_member is required")
210
+ body = result.get("body")
211
+ if not isinstance(body, str):
212
+ raise CatalogError("content body must be UTF-8 text")
213
+ primary = _legacy_primary_member(body, members)
214
+ if primary is not None:
215
+ result["primary_member"] = primary
216
+ result["body"] = members[primary]
217
+ result.pop("content_conflict", None)
218
+ return result
219
+ result.pop("primary_member", None)
220
+ result["content_conflict"] = _content_conflict(body, members)
221
+ return result
222
+
223
+
224
+ def update_content(item: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
225
+ """Apply a content patch while keeping member content the only authority.
226
+
227
+ A body-only edit writes the established primary member. Member-only and
228
+ primary-member-only edits derive the returned body. Concurrent body and
229
+ primary-member edits must agree, so two representations cannot silently
230
+ select different text.
231
+ """
232
+ if not isinstance(patch, dict):
233
+ raise CatalogError("content patch must be an object")
234
+ base = normalize_content(item, allow_legacy=True)
235
+ result = _public_item(base)
236
+ copied_patch = _public_item(patch)
237
+ result.update(copied_patch)
238
+ body_supplied = "body" in copied_patch
239
+ members_supplied = "members" in copied_patch
240
+ primary_supplied = "primary_member" in copied_patch
241
+ members = _content_members(result)
242
+ result["members"] = members
243
+
244
+ primary = result.get("primary_member")
245
+ if primary is not None and (not isinstance(primary, str) or primary not in members):
246
+ raise CatalogError("primary_member must name an existing member")
247
+ if body_supplied and not isinstance(copied_patch["body"], str):
248
+ raise CatalogError("content body must be UTF-8 text")
249
+ if primary is None and body_supplied:
250
+ matches = [path for path, content in members.items() if content == copied_patch["body"]]
251
+ primary = matches[0] if len(matches) == 1 else _deterministic_primary_path(members)
252
+ if primary is None:
253
+ raise CatalogError("content_conflict: body edit needs primary_member or one matching member")
254
+ if primary is None and members_supplied:
255
+ primary = _deterministic_primary_path(members)
256
+ if body_supplied and primary is not None:
257
+ member_body = members[primary]
258
+ if members_supplied and member_body != copied_patch["body"]:
259
+ raise CatalogError("body conflicts with primary_member member content")
260
+ if not members_supplied:
261
+ members[primary] = copied_patch["body"]
262
+
263
+ if primary is not None:
264
+ result["primary_member"] = primary
265
+ result["body"] = members[primary]
266
+ result.pop("content_conflict", None)
267
+ return result
268
+ if primary_supplied:
269
+ # The explicit key was present but invalid; keep this error distinct
270
+ # from a legacy conflict so callers can repair the named path.
271
+ raise CatalogError("primary_member must name an existing member")
272
+ return normalize_content(result, allow_legacy=True)
273
+
274
+
275
+ def _validate_item(item: dict[str, Any], package_id: str, seen: set[str]) -> dict[str, Any]:
276
+ if not isinstance(item, dict):
277
+ raise CatalogError("item must be an object")
278
+ if not valid_package_id(package_id):
279
+ raise CatalogError(f"invalid package_id for item: {package_id!r}")
280
+ result = _public_item(item)
281
+ item_id = result.get("item_id")
282
+ if not isinstance(item_id, str) or not _SAFE_COMPONENT.match(item_id):
283
+ raise CatalogError(f"{package_id}: item_id must be a safe stable identifier")
284
+ ref = _item_ref(package_id, item_id)
285
+ if ref in seen:
286
+ raise CatalogError(f"duplicate corpus ref: {ref}")
287
+ seen.add(ref)
288
+ result["package_id"] = package_id
289
+ result["ref"] = ref
290
+ if not isinstance(result.get("title"), str) or not result["title"].strip():
291
+ raise CatalogError(f"{ref}: title is required")
292
+ if not isinstance(result.get("body"), str):
293
+ raise CatalogError(f"{ref}: body must be UTF-8 text")
294
+ if result.get("surface") not in SURFACES:
295
+ raise CatalogError(f"{ref}: invalid surface {result.get('surface')!r}")
296
+ if result.get("tier") not in TIERS:
297
+ raise CatalogError(f"{ref}: invalid tier {result.get('tier')!r}")
298
+ if result.get("kind") not in KINDS:
299
+ raise CatalogError(f"{ref}: invalid kind {result.get('kind')!r}")
300
+ domains = result.get("domains", [])
301
+ if not isinstance(domains, list) or any(not isinstance(d, str) or not d for d in domains):
302
+ raise CatalogError(f"{ref}: domains must be a list of names")
303
+ result["domains"] = list(dict.fromkeys(domains))
304
+ try:
305
+ result = normalize_content(result, allow_legacy=True)
306
+ except CatalogError as exc:
307
+ raise CatalogError(f"{ref}: {exc}") from exc
308
+ for key in ("routes", "dependencies"):
309
+ values = result.get(key, [])
310
+ if not isinstance(values, list) or any(not isinstance(v, str) for v in values):
311
+ raise CatalogError(f"{ref}: {key} must be a list of refs")
312
+ result[key] = list(dict.fromkeys(values))
313
+ origin = result.get("origin", {})
314
+ if not isinstance(origin, dict):
315
+ raise CatalogError(f"{ref}: origin must be an object")
316
+ result["origin"] = origin
317
+ if "hook" in result:
318
+ try:
319
+ validate_hook_binding(result["hook"])
320
+ except CatalogError as exc:
321
+ raise CatalogError(f"{ref}: {exc}") from exc
322
+ return result
323
+
324
+
325
+ def _bullet_bodies(monolith: str, manifest: dict[str, Any]) -> dict[str, str]:
326
+ bullets = [line for line in monolith.splitlines() if line.startswith("- ")]
327
+ result: dict[str, str] = {}
328
+ for entry in manifest.get("bullets", []):
329
+ anchor = entry.get("anchor")
330
+ if not isinstance(anchor, str):
331
+ raise CatalogError("bullet without anchor")
332
+ matches = [line for line in bullets if anchor in line]
333
+ if len(matches) != 1:
334
+ raise CatalogError(f"bullet anchor {anchor!r} matches {len(matches)} source lines")
335
+ result[anchor] = matches[0]
336
+ return result
337
+
338
+
339
+ def _manifest_item(
340
+ *, package_id: str, item_id: str, title: str, body: str, surface: str,
341
+ tier: str, domains: list[str], kind: str, members: dict[str, str],
342
+ source_path: str, primary_member: str, anchor: str | None = None, dependencies: Iterable[str] = (),
343
+ ) -> dict[str, Any]:
344
+ origin: dict[str, Any] = {"source_path": source_path}
345
+ if anchor is not None:
346
+ origin["anchor"] = anchor
347
+ return {
348
+ "ref": _item_ref(package_id, item_id),
349
+ "package_id": package_id,
350
+ "item_id": item_id,
351
+ "title": title,
352
+ "body": body,
353
+ "surface": surface,
354
+ "tier": tier,
355
+ "domains": list(domains),
356
+ "kind": kind,
357
+ "members": members,
358
+ "primary_member": primary_member,
359
+ "origin": origin,
360
+ "routes": list(dependencies),
361
+ "dependencies": list(dependencies),
362
+ }
363
+
364
+
365
+ def _surface_for(kind: str) -> str:
366
+ return {
367
+ "rule": "always",
368
+ "guide": "relevant",
369
+ "skill": "requested",
370
+ "hook": "event",
371
+ "agent": "delegated",
372
+ }[kind]
373
+
374
+
375
+ def _template_hook_bindings(repo: pathlib.Path) -> dict[str, dict[str, str]]:
376
+ """Hook bindings are read from the actual Claude registration template.
377
+
378
+ The template is the registration authority. A hook source path appearing in
379
+ a manifest description is not a registration and must never make executable
380
+ behavior appear in a private snapshot.
381
+ """
382
+ path = repo / "claude" / "settings.template.json"
383
+ try:
384
+ settings = json.loads(_read_utf8(path))
385
+ except json.JSONDecodeError as exc:
386
+ raise CatalogError(f"invalid Claude hook settings template: {path}") from exc
387
+ hooks = settings.get("hooks") if isinstance(settings, dict) else None
388
+ if not isinstance(hooks, dict):
389
+ raise CatalogError("Claude hook settings template has no hooks object")
390
+ result: dict[str, dict[str, str]] = {}
391
+ for event, entries in hooks.items():
392
+ if event not in CLAUDE_HOOK_EVENTS or not isinstance(entries, list):
393
+ continue
394
+ for entry in entries:
395
+ if not isinstance(entry, dict) or not isinstance(entry.get("matcher"), str):
396
+ continue
397
+ for hook in entry.get("hooks", []):
398
+ if not isinstance(hook, dict) or hook.get("type") != "command":
399
+ continue
400
+ command = hook.get("command")
401
+ if not isinstance(command, str):
402
+ continue
403
+ try:
404
+ tokens = shlex.split(command)
405
+ except ValueError:
406
+ continue
407
+ for token in tokens:
408
+ match = re.fullmatch(r"(?:\.?/)?central/hooks/([A-Za-z0-9][A-Za-z0-9._-]*\.py)", token)
409
+ if not match:
410
+ continue
411
+ name = match.group(1)
412
+ if name in result:
413
+ raise CatalogError(f"Claude hook script registered more than once: {name}")
414
+ result[name] = {"event": event, "matcher": entry["matcher"]}
415
+ return result
416
+
417
+
418
+ def _core_catalog(repo: pathlib.Path) -> dict[str, Any]:
419
+ manifest_path = repo / "compose" / "domains.json"
420
+ manifest = json.loads(_read_utf8(manifest_path))
421
+ package_id = manifest.get("package_id", CORE)
422
+ if not valid_package_id(package_id):
423
+ raise CatalogError(f"invalid core package id: {package_id!r}")
424
+ if manifest.get("version") != 1:
425
+ raise CatalogError("unsupported core manifest version")
426
+ domains = manifest.get("domains")
427
+ if not isinstance(domains, dict) or not domains:
428
+ raise CatalogError("core manifest domains must be non-empty")
429
+ if any(not isinstance(k, str) or not isinstance(v, str) for k, v in domains.items()):
430
+ raise CatalogError("core manifest domains must map names to descriptions")
431
+ monolith_path = repo / "claude" / "CLAUDE.md"
432
+ monolith = _read_utf8(monolith_path)
433
+ bullet_bodies = _bullet_bodies(monolith, manifest)
434
+ items: list[dict[str, Any]] = []
435
+ seen: set[str] = set()
436
+ hook_bindings = _template_hook_bindings(repo)
437
+ for entry in manifest.get("bullets", []):
438
+ item_id = entry.get("item_id")
439
+ anchor = entry.get("anchor")
440
+ if not isinstance(item_id, str):
441
+ raise CatalogError(f"bullet {anchor!r} lacks persistent item_id")
442
+ body = bullet_bodies[anchor]
443
+ items.append(_validate_item(_manifest_item(
444
+ package_id=package_id, item_id=item_id, title=anchor, body=body,
445
+ surface=_surface_for("rule"), tier=entry["tier"], domains=entry.get("domains", []),
446
+ kind="rule", members={"rule.md": body + "\n"}, primary_member="rule.md",
447
+ source_path="claude/CLAUDE.md",
448
+ anchor=anchor,
449
+ ), package_id, seen))
450
+
451
+ section_specs = (
452
+ ("guides", "guide", "claude/guides"),
453
+ ("hooks", "hook", "claude/hooks"),
454
+ ("agents", "agent", "claude/agents"),
455
+ )
456
+ for section, kind, source_dir in section_specs:
457
+ entries = manifest.get(section, {})
458
+ if not isinstance(entries, dict):
459
+ raise CatalogError(f"core manifest {section} must be an object")
460
+ for name, entry in entries.items():
461
+ if not isinstance(entry, dict) or not isinstance(entry.get("item_id"), str):
462
+ raise CatalogError(f"{section}/{name} lacks persistent item_id")
463
+ source = repo / source_dir / name
464
+ body = _read_utf8(source)
465
+ members = {f"{kind}s/{name}": body}
466
+ if kind == "guide":
467
+ try:
468
+ from assemble import guide_members
469
+ except ImportError:
470
+ from .assemble import guide_members
471
+ try:
472
+ members = {f"guides/{member}": _read_utf8(repo / source_dir / member)
473
+ for member in guide_members(repo / source_dir, name)}
474
+ except ValueError as exc:
475
+ raise CatalogError(f"invalid guide bundle {name}: {exc}") from exc
476
+ item = _manifest_item(
477
+ package_id=package_id, item_id=entry["item_id"], title=name, body=body,
478
+ surface=_surface_for(kind), tier=entry["tier"], domains=entry.get("domains", []),
479
+ kind=kind, members=members,
480
+ primary_member=f"{kind}s/{name}", source_path=f"{source_dir}/{name}",
481
+ )
482
+ if kind == "hook" and name in hook_bindings:
483
+ item["hook"] = hook_bindings[name]
484
+ items.append(_validate_item(item, package_id, seen))
485
+
486
+ skills = manifest.get("skills", {})
487
+ if not isinstance(skills, dict):
488
+ raise CatalogError("core manifest skills must be an object")
489
+ for name, entry in skills.items():
490
+ if not isinstance(entry, dict) or not isinstance(entry.get("item_id"), str):
491
+ raise CatalogError(f"skills/{name} lacks persistent item_id")
492
+ root = repo / "claude" / "skills" / name
493
+ if root.is_symlink() or not root.is_dir():
494
+ raise CatalogError(f"skill source is not a directory: {root}")
495
+ members: dict[str, str] = {}
496
+ for path in sorted(root.rglob("*")):
497
+ if path.is_symlink():
498
+ raise CatalogError(f"symlink input is not a corpus member: {path}")
499
+ if path.is_file():
500
+ relative = path.relative_to(root).as_posix()
501
+ members[f"skills/{name}/{relative}"] = _read_utf8(path)
502
+ if f"skills/{name}/SKILL.md" not in members:
503
+ raise CatalogError(f"skill source lacks SKILL.md: {root}")
504
+ items.append(_validate_item(_manifest_item(
505
+ package_id=package_id, item_id=entry["item_id"], title=name,
506
+ body=members[f"skills/{name}/SKILL.md"], surface=_surface_for("skill"),
507
+ tier=entry["tier"], domains=entry.get("domains", []), kind="skill",
508
+ members=members, primary_member=f"skills/{name}/SKILL.md", source_path=f"claude/skills/{name}",
509
+ ), package_id, seen))
510
+ if not items:
511
+ raise CatalogError("core catalog is empty")
512
+ return {
513
+ "schema_version": SCHEMA_VERSION,
514
+ "packages": [{"package_id": package_id, "domains": domains}],
515
+ "items": items,
516
+ }
517
+
518
+
519
+ def _external_catalog(package_root: pathlib.Path) -> dict[str, Any]:
520
+ manifest_path = package_root / "manifest.json"
521
+ manifest = json.loads(_read_utf8(manifest_path))
522
+ if manifest.get("schema_version", SCHEMA_VERSION) != SCHEMA_VERSION:
523
+ raise CatalogError("unsupported external package schema")
524
+ package_id = manifest.get("package_id")
525
+ if not valid_package_id(package_id):
526
+ raise CatalogError(f"invalid package_id: {package_id!r}")
527
+ domains = manifest.get("domains", {})
528
+ if not isinstance(domains, dict) or any(not isinstance(k, str) or not isinstance(v, str)
529
+ for k, v in domains.items()):
530
+ raise CatalogError("external package domains must map names to descriptions")
531
+ raw_items = manifest.get("items")
532
+ if raw_items is None:
533
+ item_path = package_root / "items.json"
534
+ raw_items = json.loads(_read_utf8(item_path)).get("items") if item_path.is_file() else None
535
+ if not isinstance(raw_items, list) or not raw_items:
536
+ raise CatalogError("external package must contain non-empty items")
537
+ seen: set[str] = set()
538
+ items = [_validate_item(item, package_id, seen) for item in raw_items]
539
+ return {"schema_version": SCHEMA_VERSION,
540
+ "packages": [{"package_id": package_id, "domains": domains}], "items": items}
541
+
542
+
543
+ def load_catalog(repo: pathlib.Path) -> dict[str, Any]:
544
+ """Load a core repository, or an external package root with ``manifest.json``.
545
+
546
+ An external manifest uses the normalized item shape returned here, keeping a
547
+ single validation authority for built-in, personal, and imported packages.
548
+ """
549
+ root = pathlib.Path(repo).resolve()
550
+ if root.is_symlink():
551
+ raise CatalogError(f"catalog root must not be a symlink: {root}")
552
+ if (root / "compose" / "domains.json").is_file():
553
+ return _core_catalog(root)
554
+ if (root / "manifest.json").is_file():
555
+ return _external_catalog(root)
556
+ raise CatalogError(f"no core catalog or package manifest at {root}")
557
+
558
+
559
+ def _destination_path(destination: pathlib.Path, relative: str) -> pathlib.Path:
560
+ relative = _relative_path(relative, "compiler")
561
+ path = destination / relative
562
+ # ``relative`` has already excluded traversal. resolve() after joining still
563
+ # detects a pre-existing symlink in a caller-provided destination.
564
+ try:
565
+ path.resolve().relative_to(destination.resolve())
566
+ except ValueError as exc:
567
+ raise CatalogError(f"compiler destination escapes root: {relative!r}") from exc
568
+ return path
569
+
570
+
571
+ def _write_private(path: pathlib.Path, content: str) -> None:
572
+ if path.is_symlink():
573
+ raise CatalogError(f"compiler refuses symlink output: {path}")
574
+ path.parent.mkdir(parents=True, exist_ok=True)
575
+ try:
576
+ with path.open('x', encoding='utf-8') as output:
577
+ output.write(content)
578
+ except FileExistsError as exc:
579
+ raise CatalogError(f"compiler output already exists or aliases another member: {path}") from exc
580
+
581
+
582
+ def _replace_private_owned(path: pathlib.Path, content: str) -> None:
583
+ """Atomically rewrite a member this compiler emitted earlier in this run."""
584
+ if path.is_symlink() or not path.is_file():
585
+ raise CatalogError(f"compiler cannot replace unowned native member: {path}")
586
+ descriptor, name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent)
587
+ temporary = pathlib.Path(name)
588
+ try:
589
+ with os.fdopen(descriptor, "w", encoding="utf-8") as output:
590
+ output.write(content)
591
+ output.flush()
592
+ os.fsync(output.fileno())
593
+ temporary.chmod(path.stat().st_mode & 0o777)
594
+ os.replace(temporary, path)
595
+ finally:
596
+ temporary.unlink(missing_ok=True)
597
+
598
+
599
+ def _resource_rewrites(items: list[dict[str, Any]], destination: pathlib.Path) -> dict[str, str]:
600
+ """Known canonical source paths -> the corresponding private copied member.
601
+
602
+ We only rewrite source links which name a catalog member. Broad replacement
603
+ of config-home variables would mutate unrelated prose and reintroduce a
604
+ global-home dependency.
605
+ """
606
+ rewrites: dict[str, str] = {}
607
+ for item in items:
608
+ source = item.get("origin", {}).get("source_path")
609
+ if not isinstance(source, str):
610
+ continue
611
+ members = item["members"]
612
+ if item["kind"] == "skill":
613
+ continue
614
+ primary = item.get("primary_member")
615
+ if primary not in members:
616
+ continue
617
+ emitted_root = destination / "items" / _safe_ref_path(item["ref"])
618
+ emitted = emitted_root / primary
619
+ rewrites[source] = str(emitted)
620
+ # Current corpus prose uses these two host config forms. The generated
621
+ # target is private and host-neutral; no generic variable substitution.
622
+ if source.startswith("claude/guides/"):
623
+ tail = source.removeprefix("claude/guides/")
624
+ rewrites[f"${{CLAUDE_CONFIG_DIR:-$HOME/.claude}}/guides/{tail}"] = str(emitted)
625
+ rewrites[f"${{CODEX_HOME:-$HOME/.codex}}/guides/{tail}"] = str(emitted)
626
+ primary_dir = pathlib.PurePosixPath(primary).parent
627
+ for member in members:
628
+ relative = pathlib.PurePosixPath(member)
629
+ if not relative.is_relative_to(primary_dir):
630
+ continue
631
+ suffix = relative.relative_to(primary_dir).as_posix()
632
+ member_source = pathlib.PurePosixPath(source).parent / suffix
633
+ member_target = str(emitted_root / member)
634
+ rewrites[member_source.as_posix()] = member_target
635
+ member_tail = member_source.as_posix().removeprefix("claude/guides/")
636
+ rewrites[f"${{CLAUDE_CONFIG_DIR:-$HOME/.claude}}/guides/{member_tail}"] = member_target
637
+ rewrites[f"${{CODEX_HOME:-$HOME/.codex}}/guides/{member_tail}"] = member_target
638
+ return rewrites
639
+
640
+
641
+ def _safe_ref_path(ref: str) -> str:
642
+ # package refs contain '@', '/', and ':'; preserve identity without allowing
643
+ # their separators to create destination hierarchy outside this item root.
644
+ return "item-" + re.sub(r"[^A-Za-z0-9._-]", "_", ref)
645
+
646
+
647
+ def _rewrite_resources(text: str, rewrites: dict[str, str]) -> str:
648
+ for source, replacement in sorted(rewrites.items(), key=lambda pair: len(pair[0]), reverse=True):
649
+ text = text.replace(source, replacement)
650
+ return text
651
+
652
+
653
+ def _rewrite_native_hook_guide(item: dict[str, Any], text: str, rewrites: dict[str, str]) -> str:
654
+ """Resolve the hook's own relative guide constant only in a native plugin."""
655
+ source = item.get("origin", {}).get("source_path")
656
+ if item.get("kind") != "hook" or not isinstance(source, str):
657
+ return text
658
+ if not source.startswith("claude/hooks/"):
659
+ return text
660
+ match = re.search(r'(?m)^GUIDE = "guides/([A-Za-z0-9][A-Za-z0-9._-]*\.md)"$', text)
661
+ if not match:
662
+ return text
663
+ target = rewrites.get(f"claude/guides/{match.group(1)}")
664
+ if target is None:
665
+ return text
666
+ return text[:match.start()] + f'GUIDE = {target!r}' + text[match.end():]
667
+
668
+
669
+ def _procedure_description(item: dict[str, Any]) -> str:
670
+ """A compact, authored-first summary for requested procedure access."""
671
+ explicit = item.get("description")
672
+ if isinstance(explicit, str) and explicit.strip():
673
+ return explicit.strip()
674
+ match = re.search(r"^description:\s*(.+)$", item["body"], flags=re.MULTILINE)
675
+ if match:
676
+ return match.group(1).strip().strip('"')
677
+ for line in item["body"].splitlines():
678
+ text = line.strip()
679
+ if text and text != "---" and not text.startswith("#"):
680
+ return text
681
+ return "Private procedure"
682
+
683
+
684
+ def _router_text(relevant: list[dict[str, Any]], destination: pathlib.Path) -> str:
685
+ lines = ["# Generated relevant-corpus router", "",
686
+ "Use a listed guide only when the task matches its domains.", ""]
687
+ for item in sorted(relevant, key=lambda x: x["ref"]):
688
+ primary = item["_emitted_members"][item["primary_member"]]
689
+ domains = ", ".join(item["domains"]) or "all selected environments"
690
+ front = re.match(r"\A---\r?\n(.*?)\r?\n---(?:\r?\n|\Z)", item["body"], flags=re.DOTALL)
691
+ description = re.search(r"^description:[ \t]*(.+)$", front.group(1), flags=re.MULTILINE) if front else None
692
+ condition = description.group(1).strip().strip('\"\'') if description else ""
693
+ detail = f"{condition} — {domains}" if condition else domains
694
+ lines.append(f"- [{item['title']}]({primary}) — {detail} ({item['ref']})")
695
+ return "\n".join(lines) + "\n"
696
+
697
+
698
+ def _plugin_namespace(ref: str) -> str:
699
+ """A compact namespace derived from the complete, package-qualified ref."""
700
+ return "agent-bios-" + hashlib.sha256(ref.encode("utf-8")).hexdigest()[:24]
701
+
702
+
703
+ def _native_hook_carrier(item: dict[str, Any]) -> tuple[str, dict[str, str]]:
704
+ source = item.get("origin", {}).get("source_path")
705
+ if item.get("kind") != "hook" or not isinstance(source, str):
706
+ raise CatalogError("event item has no installed hook carrier provenance")
707
+ matched = re.fullmatch(r"claude/hooks/([A-Za-z0-9][A-Za-z0-9._-]*\.py)", source)
708
+ if not matched:
709
+ raise CatalogError("event item origin is not an installed canonical hooks/*.py carrier")
710
+ member = f"hooks/{matched.group(1)}"
711
+ if member not in item["members"]:
712
+ raise CatalogError("event item no longer retains its installed hook entrypoint")
713
+ try:
714
+ ast.parse(item["members"][member], filename=member)
715
+ except SyntaxError as exc:
716
+ raise CatalogError(f"native hook entrypoint is not valid Python: {exc.msg}") from exc
717
+ binding = item.get("hook")
718
+ if not isinstance(binding, dict):
719
+ raise CatalogError("event item has no registered hook binding")
720
+ return member, binding
721
+
722
+
723
+ def _native_agent_carrier(item: dict[str, Any]) -> tuple[str, str]:
724
+ source = item.get("origin", {}).get("source_path")
725
+ if item.get("kind") != "agent" or not isinstance(source, str):
726
+ raise CatalogError("delegated item has no installed Claude agent carrier provenance")
727
+ matched = re.fullmatch(r"claude/agents/([A-Za-z0-9][A-Za-z0-9._-]*\.md)", source)
728
+ if not matched:
729
+ raise CatalogError("delegated item origin is not an installed Claude agents/*.md carrier")
730
+ filename = matched.group(1)
731
+ member = f"agents/{filename}"
732
+ if member not in item["members"]:
733
+ raise CatalogError("delegated item no longer retains its installed agent entrypoint")
734
+ # Claude advertises the frontmatter name, not the filename. Read only the
735
+ # bounded routing slug; leave all other native YAML interpretation to Claude.
736
+ header = re.match(r"\A---\r?\n(.*?)\r?\n---(?:\r?\n|$)", item["members"][member], re.S)
737
+ names = re.findall(r"(?m)^name:[ \t]*([^\r\n]*)", header.group(1)) if header else []
738
+ if len(names) != 1:
739
+ raise CatalogError("native agent needs one top-level frontmatter name")
740
+ name = names[0].strip()
741
+ if len(name) >= 2 and name[0] == name[-1] and name[0] in "\"'":
742
+ name = name[1:-1]
743
+ if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]*", name):
744
+ raise CatalogError("native agent name must be a plain or quoted routing slug")
745
+ return member, name
746
+
747
+
748
+ def _assert_native_member_safety(item: dict[str, Any], carrier: str, surface: str) -> None:
749
+ """A plugin root may not smuggle a second discovered capability into launch."""
750
+ agent_members = {member for member in item["members"] if re.fullmatch(r"agents/[^/]+\.md", member)}
751
+ if surface == "delegated" and agent_members != {carrier}:
752
+ raise CatalogError("delegated item must retain exactly its one agents/*.md carrier")
753
+ for member in item["members"]:
754
+ first = pathlib.PurePosixPath(member).parts[0]
755
+ if first in {"skills", "commands", ".claude-plugin", ".codex-plugin", ".mcp.json"}:
756
+ raise CatalogError(f"native plugin member would auto-discover {member!r}")
757
+ if member in {"hooks/hooks.json", "hooks.json", "settings.json", "config.toml"}:
758
+ raise CatalogError(f"native plugin member would override generated registration {member!r}")
759
+ if first == "agents" and member != carrier:
760
+ raise CatalogError(f"native plugin member adds an unselected agent carrier {member!r}")
761
+ # Hook code and ordinary reference/assets remain valid. The generated
762
+ # hooks.json calls only the proven carrier, so support files stay inert.
763
+
764
+
765
+ def _write_plugin_manifest(root: pathlib.Path, namespace: str, item: dict[str, Any]) -> str:
766
+ relative = pathlib.PurePosixPath(".claude-plugin") / "plugin.json"
767
+ _write_private(root / relative, json.dumps({
768
+ "name": namespace,
769
+ "version": "1.0.0",
770
+ "description": f"Private agent-bios corpus item {item['ref']}",
771
+ }, ensure_ascii=False, separators=(",", ":")) + "\n")
772
+ return relative.as_posix()
773
+
774
+
775
+ def _emit_native_item(
776
+ item: dict[str, Any], destination: pathlib.Path, namespace: str, base_instruction_text: str,
777
+ host: str, reference_root: pathlib.Path,
778
+ ) -> tuple[list[str], dict[str, Any] | None]:
779
+ root = destination / "items" / _safe_ref_path(item["ref"])
780
+ if item["surface"] == "event":
781
+ member, binding = _native_hook_carrier(item)
782
+ validate_hook_binding(binding, host)
783
+ _assert_native_member_safety(item, member, "event")
784
+ emitted = [_write_plugin_manifest(root, namespace, item)] if host == "claude" else []
785
+ reference = reference_root / "items" / _safe_ref_path(item["ref"]) / member
786
+ target = f'"${{CLAUDE_PLUGIN_ROOT}}/{member}"' if host == "claude" else shlex.quote(str(reference))
787
+ command = f'{shlex.quote(sys.executable)} {target}'
788
+ hooks = {"hooks": {binding["event"]: [{
789
+ "matcher": binding["matcher"],
790
+ "hooks": [{"type": "command", "command": command}],
791
+ }]}}
792
+ relative = pathlib.PurePosixPath("hooks") / "hooks.json"
793
+ _write_private(root / relative, json.dumps(hooks, ensure_ascii=False, separators=(",", ":")) + "\n")
794
+ emitted.append(relative.as_posix())
795
+ return emitted, {"ref": item["ref"], "plugin": namespace, "hook": binding,
796
+ "entrypoint": member, "hooks": hooks["hooks"]}
797
+ if item["surface"] == "delegated":
798
+ if host != "claude":
799
+ raise CatalogError("native delegated adapter is unsupported for codex: authored Claude agent frontmatter requires a Codex agent projection")
800
+ member, name = _native_agent_carrier(item)
801
+ _assert_native_member_safety(item, member, "delegated")
802
+ # Carrier validation happens before any plugin write. The child receives
803
+ # the ordinary compiled snapshot, while its qualified route is added to
804
+ # the parent instruction later so it cannot recurse into this body.
805
+ target = root / member
806
+ _replace_private_owned(target, _read_utf8(target) + "\n\n" + base_instruction_text)
807
+ emitted = [_write_plugin_manifest(root, namespace, item)]
808
+ return emitted, {"ref": item["ref"], "plugin": namespace, "agent_name": name,
809
+ "route": f"{namespace}:{name}"}
810
+ raise CatalogError(f"unsupported native surface {item['surface']!r}")
811
+
812
+
813
+ def compile_items(items: list[dict[str, Any]], destination: pathlib.Path, host: str,
814
+ native: bool = False, reference_root: pathlib.Path | None = None) -> dict[str, Any]:
815
+ """Emit selected items into one private destination.
816
+
817
+ ``host`` is currently a validation seam for adapters. Claude and Codex use
818
+ the same private file layout, while the caller injects ``instruction_text``
819
+ through the corresponding per-session adapter.
820
+ """
821
+ if host not in {"claude", "codex"}:
822
+ raise CatalogError(f"unsupported host: {host!r}")
823
+ if not isinstance(native, bool):
824
+ raise CatalogError("native must be boolean")
825
+ if not isinstance(items, list):
826
+ raise CatalogError("compiler requires an item list")
827
+ destination = pathlib.Path(destination)
828
+ # Emit bytes into staging while quoting paths for their final location. A
829
+ # string replacement after shell/Python quoting cannot relocate all paths.
830
+ reference_root = pathlib.Path(reference_root) if reference_root is not None else destination
831
+ if destination.exists() and destination.is_symlink():
832
+ raise CatalogError(f"compiler destination must not be a symlink: {destination}")
833
+ destination.mkdir(parents=True, exist_ok=True)
834
+ seen: set[str] = set()
835
+ normalized = [_validate_item(item, item.get("package_id", ""), seen) for item in items]
836
+ unresolved = [item["ref"] for item in normalized if item.get("content_conflict")]
837
+ if unresolved:
838
+ raise CatalogError(f"content_conflict: selected item needs reconciliation: {', '.join(unresolved)}")
839
+ rewrites = _resource_rewrites(normalized, reference_root)
840
+ files: list[str] = []
841
+ for item in normalized:
842
+ emitted: dict[str, str] = {}
843
+ root = destination / "items" / _safe_ref_path(item["ref"])
844
+ for member, content in item["members"].items():
845
+ relative = pathlib.PurePosixPath("items") / _safe_ref_path(item["ref"]) / member
846
+ target = _destination_path(destination, relative.as_posix())
847
+ rewritten = _rewrite_resources(content, rewrites)
848
+ if native and item["surface"] == "event":
849
+ rewritten = _rewrite_native_hook_guide(item, rewritten, rewrites)
850
+ _write_private(target, rewritten)
851
+ emitted[member] = str(reference_root / relative)
852
+ files.append(relative.as_posix())
853
+ item["_emitted_members"] = emitted
854
+
855
+ always = [item for item in normalized if item["surface"] == "always"]
856
+ relevant = [item for item in normalized if item["surface"] == "relevant"]
857
+ requested = [item for item in normalized if item["surface"] == "requested"]
858
+ router_path = None
859
+ if relevant:
860
+ router_relative = "router/relevant.md"
861
+ router_path = _destination_path(destination, router_relative)
862
+ _write_private(router_path, _router_text(relevant, destination))
863
+ files.append(router_relative)
864
+ instruction_parts = ["# Activated private corpus", ""]
865
+ if always:
866
+ instruction_parts.append("## Always in this environment")
867
+ instruction_parts.append("")
868
+ instruction_parts.extend(_rewrite_resources(item["body"], rewrites).rstrip("\n") for item in always)
869
+ instruction_parts.append("")
870
+ if router_path is not None:
871
+ instruction_parts.append(f"Relevant procedures: {reference_root / 'router/relevant.md'}")
872
+ if requested:
873
+ instruction_parts.append("Requested procedures:")
874
+ for item in requested:
875
+ primary = item["_emitted_members"][item["primary_member"]]
876
+ instruction_parts.append(
877
+ f"- {item['title']} — {_procedure_description(item)}: {primary} ({item['ref']})")
878
+ base_instruction_text = "\n".join(instruction_parts).rstrip() + "\n"
879
+ unavailable: list[dict[str, str]] = []
880
+ assets: dict[str, Any] = {}
881
+ plugin_names: dict[str, str] = {}
882
+ plugin_roots: list[str] = []
883
+ hook_config: dict[str, list[dict[str, Any]]] = {}
884
+ agent_routes: list[dict[str, str]] = []
885
+ for item in normalized:
886
+ if item["surface"] not in {"event", "delegated"}:
887
+ continue
888
+ if not native:
889
+ unavailable.append({"ref": item["ref"], "surface": item["surface"],
890
+ "reason": f"native {item['surface']} consumption is disabled; opt in with --corpus-native"})
891
+ continue
892
+ namespace = _plugin_namespace(item["ref"])
893
+ prior = plugin_names.get(namespace)
894
+ if prior is not None and prior != item["ref"]:
895
+ raise CatalogError(f"native namespace collision: {prior} and {item['ref']}")
896
+ plugin_names[namespace] = item["ref"]
897
+ try:
898
+ emitted, route = _emit_native_item(item, destination, namespace, base_instruction_text, host, reference_root)
899
+ except CatalogError as exc:
900
+ unavailable.append({"ref": item["ref"], "surface": item["surface"], "reason": str(exc)})
901
+ continue
902
+ root_relative = (pathlib.PurePosixPath("items") / _safe_ref_path(item["ref"])).as_posix()
903
+ if host == "claude":
904
+ plugin_roots.append(root_relative)
905
+ else:
906
+ for event, groups in route["hooks"].items():
907
+ hook_config.setdefault(event, []).extend(groups)
908
+ files.extend((pathlib.PurePosixPath(root_relative) / path).as_posix() for path in emitted)
909
+ if route and item["surface"] == "delegated":
910
+ agent_routes.append(route)
911
+ if native and host == "claude":
912
+ assets["claude_plugins"] = sorted(plugin_roots)
913
+ if native and host == "codex":
914
+ assets["codex_hooks"] = hook_config
915
+ instruction_text = base_instruction_text
916
+ if agent_routes:
917
+ instruction_text += "\nNative delegated agents:\n"
918
+ for route in sorted(agent_routes, key=lambda row: row["ref"]):
919
+ instruction_text += f"- {route['route']} ({route['ref']})\n"
920
+ instruction_relative = "launch-content/instructions.md"
921
+ _write_private(_destination_path(destination, instruction_relative), instruction_text)
922
+ files.append(instruction_relative)
923
+ for item in normalized:
924
+ item.pop("_emitted_members", None)
925
+ return {
926
+ "instruction_text": instruction_text,
927
+ "files": sorted(files),
928
+ "item_refs": [item["ref"] for item in normalized],
929
+ "unavailable": unavailable,
930
+ "assets": assets,
931
+ }