agent-bios 0.9.3 → 0.9.5

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/claude/CLAUDE.md CHANGED
@@ -150,4 +150,4 @@
150
150
 
151
151
  ## Session Learning
152
152
 
153
- - `learn!` — session learning: when the user enters `learn!` in ANY session, capture durable lessons from THIS session for their own next session and the org. Read `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/guides/learning-flow.md` and run its flow (admission bar → type A–G → intended consumption layer → the user's explicit approval), submitting approved learnings ONLY through `scripts/collect-learning.py`, which owns the id/timestamp and writes both the prose and the JSON record. When a Session distill preset run (trigger `distill!`) is active, that mission owns capture — do not also run this; and never manufacture Type-G principles user-side (curator-only).
153
+ - `learn!` — session learning: when the user enters `learn!` in ANY session, capture durable lessons from THIS session for their own next session and the org. Read `${CLAUDE_CONFIG_DIR:-$HOME/.claude}/guides/learning-flow.md` and run its flow (admission bar → type A–G → intended consumption layer → the user's explicit approval), submitting approved learnings ONLY through `agent-bios learn`, which owns the id/timestamp and writes both the prose and the JSON record. When a Session distill preset run (trigger `distill!`) is active, that mission owns capture — do not also run this; and never manufacture Type-G principles user-side (curator-only).
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  name: workhorse
3
3
  description: WORKHORSE tier — bounded implementation, fixes, tests, and per-item judgment. Spawn decision-complete work with frozen scope/interfaces and a machine-checkable done-when; two or more independent items spawn in parallel.
4
- model: claude-sonnet-5
5
- effort: high
4
+ model: claude-opus-5
5
+ effort: medium
6
6
  ---
7
7
 
8
8
  Complete one bounded implementation, fix, or per-item judgment from a packet naming objective, frozen scope and inputs, allowed actions, output, done-when, and verification. Preserve out-of-scope behavior; batch independent reads; escalate missing decisions or authority instead of resolving them. Stage output for the main's acceptance — no external irreversible actions (push, install, credential, or remote mutation). Run the narrowest reliable changed-path check. Report: status, files_or_items_touched, evidence, verification or gap, risks_or_escalations.
@@ -162,7 +162,7 @@ Binding (2026-07-25):
162
162
  |---|---|---|
163
163
  | FRONTIER | Claude Fable 5 · GPT-5.6 Sol (read-only; max default, task-fit effort including Ultra) | bounded hardest decisions and verdicts |
164
164
  | HELM | Claude Opus 5 (xhigh) · GPT-5.6 Sol (xhigh main; main Ultra requires explicit selection; bounded FRONTIER Ultra allowed) | standing main; Codex defaults bypass, explicit sandbox narrows |
165
- | WORKHORSE | Claude Sonnet 5 · GPT-5.6 Terra (high) | implementation and per-item judgment |
165
+ | WORKHORSE | Claude Opus 5 (medium) · GPT-5.6 Terra (high) | implementation and per-item judgment |
166
166
  | SWEEP | Claude Haiku 4.5 · GPT-5.6 Luna (low) | clear repeatable scans and mechanical work |
167
167
  | VERIFIER-A | onto-mcp review | logic/authority kind; tier units per funnel |
168
168
  | VERIFIER-B | `ultracode-for-codex` CLI v0.4.5 (Codex-backed; callable from Claude or Codex) | code/execution kind |
@@ -187,7 +187,7 @@ Dispatch packets:
187
187
  | GPT-5.6 Terra WORKHORSE | outcome, frozen scope/inputs, authority, done-when, evidence/report, escalation; high |
188
188
  | GPT-5.6 Luna SWEEP | exact search space, rules, ambiguity behavior, stop, output; low; no architecture/debugging |
189
189
  | Claude Opus 5 HELM | xhigh for agentic work; high minimum for sensitive judgment; lower only when bounded/cost-led |
190
- | Claude Sonnet 5 WORKHORSE | exact scope, apply-to-all rules, tools, verification, report; high default |
190
+ | Claude Opus 5 WORKHORSE | exact scope, apply-to-all rules, tools, verification, report; high default |
191
191
  | Claude Haiku 4.5 SWEEP | closed, high-volume, latency-sensitive work with exact schema/examples |
192
192
 
193
193
  Use only task-relevant tools; parallelize independent calls. Worker report: `status`, `files_or_items_touched`, `evidence`, `verification`, `risks_or_escalations`. Official basis: OpenAI [model](https://developers.openai.com/api/docs/guides/latest-model?model=gpt-5.6), [migration](https://developers.openai.com/api/docs/guides/upgrading-to-gpt-5p6-sol), [prompting](https://developers.openai.com/api/docs/guides/prompt-guidance-gpt-5p6), [Codex models](https://learn.chatgpt.com/docs/models); Anthropic [subagents](https://code.claude.com/docs/en/sub-agents), [model effort](https://code.claude.com/docs/en/model-config).
@@ -9,7 +9,7 @@ use_when:
9
9
  core_rules:
10
10
  - three gates BEFORE asking the user — admission bar, type, consumption layer
11
11
  - the user approves every recorded learning; nothing is written without it
12
- - submit ONLY through scripts/collect-learning.py; never hand-write the record
12
+ - submit ONLY through `agent-bios learn`; never hand-write the record
13
13
  ---
14
14
 
15
15
  # Session learning flow (`learn!`)
@@ -83,7 +83,7 @@ match your host):
83
83
  echo '{"lesson":"…","domain":"builder-base","supporting_sessions":["<tool>:<session-short-id>"],
84
84
  "criteria":["recurrent_error"],
85
85
  "classification":{"type":"B","layer":"hook","meets_bar":true}}' \
86
- | python3 scripts/collect-learning.py --host <claude|codex>
86
+ | agent-bios learn --host <claude|codex>
87
87
 
88
88
  The script (capability boundary) owns `learning_id` / `created` / `schema_version`,
89
89
  validates against `config/learning.schema.json`, logs the JSON record, and writes
@@ -0,0 +1,60 @@
1
+ {
2
+ "hooks": {
3
+ "Stop": [
4
+ {
5
+ "matcher": ".*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "/opt/homebrew/bin/node /Users/kangmin/.claude/hooks/collect-session.js"
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "SessionEnd": [
15
+ {
16
+ "matcher": ".*",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "/opt/homebrew/bin/node /Users/kangmin/.claude/hooks/collect-session.js"
21
+ }
22
+ ]
23
+ }
24
+ ],
25
+ "PostToolUse": [
26
+ {
27
+ "matcher": ".*",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "/opt/homebrew/bin/node /Users/kangmin/.claude/hooks/collect-session.js"
32
+ }
33
+ ]
34
+ }
35
+ ],
36
+ "PostToolUseFailure": [
37
+ {
38
+ "matcher": ".*",
39
+ "hooks": [
40
+ {
41
+ "type": "command",
42
+ "command": "/opt/homebrew/bin/node /Users/kangmin/.claude/hooks/collect-session.js"
43
+ }
44
+ ]
45
+ }
46
+ ],
47
+ "PreToolUse": [
48
+ {
49
+ "matcher": "Bash",
50
+ "hooks": [
51
+ {
52
+ "type": "command",
53
+ "command": "python3 /Users/kangmin/.claude/hooks/tooling-gotchas-hook.py"
54
+ }
55
+ ]
56
+ }
57
+ ]
58
+ },
59
+ "remoteControlAtStartup": false
60
+ }
package/codex/AGENTS.md CHANGED
@@ -151,4 +151,4 @@
151
151
 
152
152
  ## Session Learning
153
153
 
154
- - `learn!` — session learning: when the user enters `learn!` in ANY session, capture durable lessons from THIS session for their own next session and the org. Read `${CODEX_HOME:-$HOME/.codex}/guides/learning-flow.md` and run its flow (admission bar → type A–G → intended consumption layer → the user's explicit approval), submitting approved learnings ONLY through `scripts/collect-learning.py`, which owns the id/timestamp and writes both the prose and the JSON record. When a Session distill preset run (trigger `distill!`) is active, that mission owns capture — do not also run this; and never manufacture Type-G principles user-side (curator-only).
154
+ - `learn!` — session learning: when the user enters `learn!` in ANY session, capture durable lessons from THIS session for their own next session and the org. Read `${CODEX_HOME:-$HOME/.codex}/guides/learning-flow.md` and run its flow (admission bar → type A–G → intended consumption layer → the user's explicit approval), submitting approved learnings ONLY through `agent-bios learn`, which owns the id/timestamp and writes both the prose and the JSON record. When a Session distill preset run (trigger `distill!`) is active, that mission owns capture — do not also run this; and never manufacture Type-G principles user-side (curator-only).
@@ -162,7 +162,7 @@ Binding (2026-07-25):
162
162
  |---|---|---|
163
163
  | FRONTIER | Claude Fable 5 · GPT-5.6 Sol (read-only; max default, task-fit effort including Ultra) | bounded hardest decisions and verdicts |
164
164
  | HELM | Claude Opus 5 (xhigh) · GPT-5.6 Sol (xhigh main; main Ultra requires explicit selection; bounded FRONTIER Ultra allowed) | standing main; Codex defaults bypass, explicit sandbox narrows |
165
- | WORKHORSE | Claude Sonnet 5 · GPT-5.6 Terra (high) | implementation and per-item judgment |
165
+ | WORKHORSE | Claude Opus 5 (medium) · GPT-5.6 Terra (high) | implementation and per-item judgment |
166
166
  | SWEEP | Claude Haiku 4.5 · GPT-5.6 Luna (low) | clear repeatable scans and mechanical work |
167
167
  | VERIFIER-A | onto-mcp review | logic/authority kind; tier units per funnel |
168
168
  | VERIFIER-B | `ultracode-for-codex` CLI v0.4.5 (Codex-backed; callable from Claude or Codex) | code/execution kind |
@@ -187,7 +187,7 @@ Dispatch packets:
187
187
  | GPT-5.6 Terra WORKHORSE | outcome, frozen scope/inputs, authority, done-when, evidence/report, escalation; high |
188
188
  | GPT-5.6 Luna SWEEP | exact search space, rules, ambiguity behavior, stop, output; low; no architecture/debugging |
189
189
  | Claude Opus 5 HELM | xhigh for agentic work; high minimum for sensitive judgment; lower only when bounded/cost-led |
190
- | Claude Sonnet 5 WORKHORSE | exact scope, apply-to-all rules, tools, verification, report; high default |
190
+ | Claude Opus 5 WORKHORSE | exact scope, apply-to-all rules, tools, verification, report; high default |
191
191
  | Claude Haiku 4.5 SWEEP | closed, high-volume, latency-sensitive work with exact schema/examples |
192
192
 
193
193
  Use only task-relevant tools; parallelize independent calls. Worker report: `status`, `files_or_items_touched`, `evidence`, `verification`, `risks_or_escalations`. Official basis: OpenAI [model](https://developers.openai.com/api/docs/guides/latest-model?model=gpt-5.6), [migration](https://developers.openai.com/api/docs/guides/upgrading-to-gpt-5p6-sol), [prompting](https://developers.openai.com/api/docs/guides/prompt-guidance-gpt-5p6), [Codex models](https://learn.chatgpt.com/docs/models); Anthropic [subagents](https://code.claude.com/docs/en/sub-agents), [model effort](https://code.claude.com/docs/en/model-config).
@@ -9,7 +9,7 @@ use_when:
9
9
  core_rules:
10
10
  - three gates BEFORE asking the user — admission bar, type, consumption layer
11
11
  - the user approves every recorded learning; nothing is written without it
12
- - submit ONLY through scripts/collect-learning.py; never hand-write the record
12
+ - submit ONLY through `agent-bios learn`; never hand-write the record
13
13
  ---
14
14
 
15
15
  # Session learning flow (`learn!`)
@@ -83,7 +83,7 @@ match your host):
83
83
  echo '{"lesson":"…","domain":"builder-base","supporting_sessions":["<tool>:<session-short-id>"],
84
84
  "criteria":["recurrent_error"],
85
85
  "classification":{"type":"B","layer":"hook","meets_bar":true}}' \
86
- | python3 scripts/collect-learning.py --host <claude|codex>
86
+ | agent-bios learn --host <claude|codex>
87
87
 
88
88
  The script (capability boundary) owns `learning_id` / `created` / `schema_version`,
89
89
  validates against `config/learning.schema.json`, logs the JSON record, and writes
@@ -62,8 +62,8 @@ model = "claude-opus-5"
62
62
  effort = "xhigh"
63
63
 
64
64
  [hosts.claude.tiers.workhorse]
65
- model = "claude-sonnet-5"
66
- effort = "high"
65
+ model = "claude-opus-5"
66
+ effort = "medium"
67
67
 
68
68
  [hosts.claude.tiers.sweep]
69
69
  model = "claude-haiku-4-5"
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "agent-bios",
3
- "version": "0.9.3",
4
- "releaseDate": "2026-07-25",
3
+ "version": "0.9.5",
4
+ "releaseDate": "2026-07-26",
5
5
  "description": "A thin, low-level instruction layer for LLM CLI agents: one set of principles and behavior whichever model you run. Deploys into $HOME by copy via an explicit `agent-bios install`.",
6
6
  "bin": {
7
7
  "agent-bios": "scripts/install.sh"
8
8
  },
9
9
  "files": [
10
10
  "claude/CLAUDE.md",
11
+ "claude/settings.json",
11
12
  "claude/guides/",
12
13
  "claude/hooks/",
13
14
  "claude/agents/",
@@ -21,6 +22,11 @@
21
22
  "config/promotions.json",
22
23
  "shell/agent-launch.zsh",
23
24
  "scripts/agent-launch.py",
25
+ "scripts/assemble.py",
26
+ "scripts/check-domains.py",
27
+ "scripts/canary.sh",
28
+ "scripts/build-promotions.py",
29
+ "scripts/ingest-learnings-export.py",
24
30
  "scripts/check-parity.sh",
25
31
  "scripts/check-prompting-targets.sh",
26
32
  "scripts/check-learning.py",
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env python3
2
+ """Per-selection assembler: canonical corpus + domains.json -> deployed trees.
3
+
4
+ Claude side (central-managed tree, overwritten on every run):
5
+ <claude-dir>/central/bundle.md core+infra+selected-domain bullets, the
6
+ monolith's section structure preserved,
7
+ guide references rewritten to central/guides/
8
+ <claude-dir>/central/guides|hooks|agents/ audience-filtered copies
9
+ <claude-dir>/settings.json merge, not overwrite: entries whose hook
10
+ command lives under central/hooks/ are
11
+ central-owned (path IS the ownership
12
+ marker); user-owned entries untouched
13
+ Entry file (<claude-dir>/CLAUDE.md) is personal-owned: seeded once when absent
14
+ (or when it byte-matches the legacy deployed monolith), read-checked otherwise
15
+ — never rewritten. Exit 2 = entry file needs user action.
16
+
17
+ Codex side: AGENTS.md central region between markers is replaced; text outside
18
+ the markers is preserved (weaker-guarantee realization; AGENTS.md has no
19
+ imports). Codex guides deploy filtered to <codex-dir>/guides as today.
20
+
21
+ env-personal is never assembled. The domains gate must be green first.
22
+ """
23
+ import argparse
24
+ import json
25
+ import pathlib
26
+ import re
27
+ import shutil
28
+ import subprocess
29
+ import sys
30
+ import time
31
+
32
+ REPO = pathlib.Path(__file__).resolve().parent.parent
33
+ MARK_START = "<!-- agent-bios:central:start -->"
34
+ MARK_END = "<!-- agent-bios:central:end -->"
35
+ IMPORT_LINE = "@central/bundle.md"
36
+ PERSONAL_IMPORT_LINE = "@personal/learnings.md"
37
+ CLAUDE_VAR = "${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
38
+ CODEX_VAR = "${CODEX_HOME:-$HOME/.codex}"
39
+ CODEX_ONLY_PREFIX = "- Codex-only standing authorization:"
40
+
41
+ ENTRY_SEED = f"""# CLAUDE.md
42
+
43
+ {IMPORT_LINE}
44
+ {PERSONAL_IMPORT_LINE}
45
+
46
+ ## Personal
47
+ <!-- Yours. The installer never rewrites this file after seeding. The first
48
+ import pulls in the centrally managed bundle; the second pulls in your
49
+ session learning file ({CLAUDE_VAR}/personal/learnings.md — automation-owned,
50
+ written by `learn!`). Add your own personal rules below. -->
51
+ """
52
+
53
+ # Automation-owned personal learnings file, pulled in by PERSONAL_IMPORT_LINE.
54
+ # Kept in sync with scripts/collect-learning.py (the light-flow submit tool),
55
+ # which appends learnings here; seeding it keeps the import from dangling.
56
+ PERSONAL_LEARNINGS_HEADER = """# Personal learnings
57
+
58
+ <!-- Automation-owned: written by the session learning flow (`learn!`,
59
+ scripts/collect-learning.py). Do NOT hand-edit — promote→migrate clears
60
+ applied items by learning_id when the org redistributes them. Your own
61
+ personal rules belong in the entry CLAUDE.md '## Personal' section, never
62
+ here. This file is pulled into context by the entry file's
63
+ `@personal/learnings.md` import. -->
64
+ """
65
+
66
+
67
+ def die(msg, code=1):
68
+ print(f"assemble: {msg}", file=sys.stderr)
69
+ sys.exit(code)
70
+
71
+
72
+ def audience(entry):
73
+ tier = entry.get("tier")
74
+ if tier in ("core", "infra"):
75
+ return "UNIVERSAL"
76
+ if tier == "domain":
77
+ return frozenset(entry.get("domains", []))
78
+ return "NEVER" # env-personal
79
+
80
+
81
+ def kept(entry, selection):
82
+ aud = audience(entry)
83
+ return aud == "UNIVERSAL" or (isinstance(aud, frozenset) and aud & selection)
84
+
85
+
86
+ def parse_monolith(text):
87
+ """-> (title, [(header, [bullets])])"""
88
+ title, sections, cur = None, [], None
89
+ for ln in text.splitlines():
90
+ if ln.startswith("# ") and title is None:
91
+ title = ln
92
+ elif ln.startswith("## "):
93
+ cur = (ln, [])
94
+ sections.append(cur)
95
+ elif ln.startswith("- "):
96
+ if cur is None:
97
+ die("bullet before first section header")
98
+ cur[1].append(ln)
99
+ return title, sections
100
+
101
+
102
+ def build_bundle(monolith_text, manifest, selection, tool):
103
+ entry_of = {}
104
+ for e in manifest["bullets"]:
105
+ entry_of[e["anchor"]] = e
106
+ title, sections = parse_monolith(monolith_text)
107
+ out, kept_anchors = [], set()
108
+ out.append(title if tool == "claude" else "# AGENTS.md")
109
+ for header, bullets in sections:
110
+ keep = []
111
+ for b in bullets:
112
+ matches = [a for a in entry_of if a in b]
113
+ if len(matches) != 1:
114
+ die(f"bullet has {len(matches)} manifest anchors (gate should have failed): {b[:60]!r}")
115
+ if kept(entry_of[matches[0]], selection):
116
+ keep.append(b)
117
+ kept_anchors.add(matches[0])
118
+ if keep:
119
+ out.extend(["", header, ""])
120
+ out.extend(keep)
121
+ text = "\n".join(out) + "\n"
122
+ if tool == "claude":
123
+ text = text.replace(f"{CLAUDE_VAR}/guides/", f"{CLAUDE_VAR}/central/guides/")
124
+ # Canary marker: a plain, verbatim-quotable line the activation canary
125
+ # asks a live session to echo back — proves the bundle entered context.
126
+ import hashlib
127
+ rev = hashlib.sha256(text.encode("utf-8")).hexdigest()[:8]
128
+ text += f"\nagent-bios-bundle-rev: {rev}\n"
129
+ else:
130
+ text = text.replace(CLAUDE_VAR, CODEX_VAR)
131
+ expected = {e["anchor"] for e in manifest["bullets"] if kept(e, selection)}
132
+ if kept_anchors != expected:
133
+ die(f"extracted set != manifest expectation ({len(kept_anchors)} vs {len(expected)}): "
134
+ f"missing={sorted(expected - kept_anchors)[:3]} extra={sorted(kept_anchors - expected)[:3]}")
135
+ return text, len(kept_anchors)
136
+
137
+
138
+ def filtered_files(manifest, key, selection):
139
+ return sorted(n for n, e in manifest.get(key, {}).items() if kept(e, selection))
140
+
141
+
142
+ def copy_filtered(src_dir, names, dest, rewrite=None, dry=False):
143
+ if not dry:
144
+ dest.mkdir(parents=True, exist_ok=True)
145
+ for n in names:
146
+ if dry:
147
+ print(f" [dry] copy {n} -> {dest}")
148
+ continue
149
+ body = (src_dir / n).read_text(encoding="utf-8")
150
+ if rewrite:
151
+ body = body.replace(*rewrite)
152
+ (dest / n).write_text(body, encoding="utf-8")
153
+
154
+
155
+ def merge_settings(claude_dir, hook_names, template_path, dry=False):
156
+ """Path-marker merge: central-owned hook entries reference central/hooks/."""
157
+ marker = "/central/hooks/"
158
+ spath = claude_dir / "settings.json"
159
+ settings = json.loads(spath.read_text(encoding="utf-8")) if spath.exists() else {}
160
+ template = json.loads(template_path.read_text(encoding="utf-8")) if template_path.exists() else {}
161
+ hooks = settings.setdefault("hooks", {})
162
+ for event, entries in list(hooks.items()): # drop stale central-owned
163
+ hooks[event] = [en for en in entries
164
+ if not any(marker in h.get("command", "") for h in en.get("hooks", []))]
165
+ for event, entries in template.get("hooks", {}).items(): # re-add per selection
166
+ for en in entries:
167
+ cmds = [h.get("command", "") for h in en.get("hooks", [])]
168
+ owner = next((n for n in hook_names if any(c.endswith("/hooks/" + n) or f"/hooks/{n} " in c + " " for c in cmds)), None)
169
+ if owner is None:
170
+ continue
171
+ clone = json.loads(json.dumps(en))
172
+ for h in clone.get("hooks", []):
173
+ h["command"] = re.sub(r"\S*/hooks/" + re.escape(owner),
174
+ str(claude_dir / "central" / "hooks" / owner),
175
+ h["command"])
176
+ hooks.setdefault(event, []).append(clone)
177
+ if dry:
178
+ print(f" [dry] merge settings.json ({len(hook_names)} central hooks)")
179
+ return
180
+ if spath.exists():
181
+ shutil.copy2(spath, spath.with_suffix(f".json.bak-{time.strftime('%Y%m%d-%H%M%S')}"))
182
+ spath.write_text(json.dumps(settings, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
183
+
184
+
185
+ def seed_personal_learnings(claude_dir, dry=False):
186
+ """Create the automation-owned personal learnings file when seeding the entry,
187
+ so the entry's @personal/learnings.md import always resolves before the first
188
+ learn!. scripts/collect-learning.py appends to it thereafter."""
189
+ md = claude_dir / "personal" / "learnings.md"
190
+ if md.exists():
191
+ return
192
+ if dry:
193
+ print(" [dry] seed personal/learnings.md")
194
+ return
195
+ md.parent.mkdir(parents=True, exist_ok=True)
196
+ md.write_text(PERSONAL_LEARNINGS_HEADER, encoding="utf-8")
197
+
198
+
199
+ def seed_entry(claude_dir, legacy_monolith, dry=False):
200
+ entry = claude_dir / "CLAUDE.md"
201
+ if not entry.exists():
202
+ if dry:
203
+ print(" [dry] seed entry CLAUDE.md")
204
+ seed_personal_learnings(claude_dir, dry)
205
+ return "seeded"
206
+ claude_dir.mkdir(parents=True, exist_ok=True)
207
+ entry.write_text(ENTRY_SEED, encoding="utf-8")
208
+ seed_personal_learnings(claude_dir, dry)
209
+ return "seeded"
210
+ body = entry.read_text(encoding="utf-8")
211
+ if IMPORT_LINE in body:
212
+ return "ok"
213
+ if body == legacy_monolith: # untouched legacy deploy squatting on the path
214
+ if dry:
215
+ print(" [dry] replace legacy deployed CLAUDE.md with seed (backup)")
216
+ seed_personal_learnings(claude_dir, dry)
217
+ return "seeded"
218
+ shutil.copy2(entry, entry.with_suffix(f".md.bak-legacy-{time.strftime('%Y%m%d-%H%M%S')}"))
219
+ entry.write_text(ENTRY_SEED, encoding="utf-8")
220
+ seed_personal_learnings(claude_dir, dry)
221
+ return "seeded"
222
+ return "needs-action" # user content without the import line: report, never rewrite
223
+
224
+
225
+ def merge_codex(codex_dir, central_text, dry=False):
226
+ agents = codex_dir / "AGENTS.md"
227
+ region = f"{MARK_START}\n{central_text}{MARK_END}\n"
228
+ if agents.exists():
229
+ body = agents.read_text(encoding="utf-8")
230
+ if MARK_START in body and MARK_END in body:
231
+ pre, rest = body.split(MARK_START, 1)
232
+ _, post = rest.split(MARK_END, 1)
233
+ new = pre + region + post
234
+ else: # legacy whole-file deploy: back up, adopt markers, keep nothing implicit
235
+ if not dry:
236
+ shutil.copy2(agents, agents.with_suffix(f".md.bak-legacy-{time.strftime('%Y%m%d-%H%M%S')}"))
237
+ new = region + "\n## Personal\n"
238
+ else:
239
+ new = region + "\n## Personal\n"
240
+ if dry:
241
+ print(f" [dry] write AGENTS.md central region ({len(central_text)} bytes)")
242
+ return
243
+ codex_dir.mkdir(parents=True, exist_ok=True)
244
+ agents.write_text(new, encoding="utf-8")
245
+
246
+
247
+ def main():
248
+ ap = argparse.ArgumentParser()
249
+ ap.add_argument("--domains", help="comma-separated selection; overrides selection.json")
250
+ ap.add_argument("--claude-dir", default=None)
251
+ ap.add_argument("--codex-dir", default=None)
252
+ ap.add_argument("--state-dir", default=None)
253
+ ap.add_argument("--dry-run", action="store_true")
254
+ args = ap.parse_args()
255
+
256
+ import os
257
+ claude_dir = pathlib.Path(args.claude_dir or os.environ.get("CLAUDE_CONFIG_DIR") or pathlib.Path.home() / ".claude")
258
+ codex_dir = pathlib.Path(args.codex_dir or os.environ.get("CODEX_HOME") or pathlib.Path.home() / ".codex")
259
+ state_dir = pathlib.Path(args.state_dir or pathlib.Path.home() / ".local/share/agent-bios")
260
+
261
+ gate = subprocess.run([sys.executable, str(REPO / "scripts" / "check-domains.py")],
262
+ capture_output=True, text=True)
263
+ if gate.returncode != 0:
264
+ die("domains gate FAILED — fix manifest/corpus first:\n" + gate.stdout + gate.stderr)
265
+
266
+ manifest = json.loads((REPO / "config" / "domains.json").read_text(encoding="utf-8"))
267
+ if args.domains is not None:
268
+ selection = frozenset(d for d in args.domains.split(",") if d)
269
+ else:
270
+ sel_file = state_dir / "selection.json"
271
+ if not sel_file.exists():
272
+ die(f"no --domains and no {sel_file}; run onboarding or pass --domains")
273
+ selection = frozenset(json.loads(sel_file.read_text(encoding="utf-8"))["domains"])
274
+ unknown = selection - set(manifest["domains"])
275
+ if unknown:
276
+ die(f"unknown domains: {sorted(unknown)} (known: {sorted(manifest['domains'])})")
277
+
278
+ monolith = (REPO / "claude" / "CLAUDE.md").read_text(encoding="utf-8")
279
+ bundle, n_bullets = build_bundle(monolith, manifest, selection, "claude")
280
+ codex_src = (REPO / "codex" / "AGENTS.md").read_text(encoding="utf-8")
281
+ codex_bundle, _ = build_bundle(monolith, manifest, selection, "codex")
282
+ if "multi-agent-orchestration" in selection:
283
+ codex_only = next((ln for ln in codex_src.splitlines() if ln.startswith(CODEX_ONLY_PREFIX)), None)
284
+ if codex_only:
285
+ codex_bundle += codex_only + "\n"
286
+
287
+ guides = filtered_files(manifest, "guides", selection)
288
+ hooks = filtered_files(manifest, "hooks", selection)
289
+ agents = filtered_files(manifest, "agents", selection)
290
+ dry = args.dry_run
291
+
292
+ central = claude_dir / "central"
293
+ if dry:
294
+ print(f"[dry] bundle.md: {n_bullets} bullets; guides={guides} hooks={hooks} agents={agents}")
295
+ else:
296
+ central.mkdir(parents=True, exist_ok=True)
297
+ (central / "bundle.md").write_text(bundle, encoding="utf-8")
298
+ copy_filtered(REPO / "claude" / "guides", guides, central / "guides",
299
+ rewrite=(f"{CLAUDE_VAR}/guides/", f"{CLAUDE_VAR}/central/guides/"), dry=dry)
300
+ copy_filtered(REPO / "claude" / "hooks", hooks, central / "hooks", dry=dry)
301
+ copy_filtered(REPO / "claude" / "agents", agents, central / "agents", dry=dry)
302
+ merge_settings(claude_dir, hooks, REPO / "claude" / "settings.json", dry=dry)
303
+ entry_state = seed_entry(claude_dir, monolith, dry=dry)
304
+
305
+ merge_codex(codex_dir, codex_bundle, dry=dry)
306
+ copy_filtered(REPO / "codex" / "guides", guides, codex_dir / "guides", dry=dry)
307
+
308
+ if not dry:
309
+ state_dir.mkdir(parents=True, exist_ok=True)
310
+ (state_dir / "selection.json").write_text(
311
+ json.dumps({"version": 1, "domains": sorted(selection)}, indent=2) + "\n", encoding="utf-8")
312
+
313
+ print(f"ASSEMBLED: {n_bullets} bullets, {len(guides)} guides, {len(hooks)} hooks, "
314
+ f"{len(agents)} agents for selection {sorted(selection)}; entry={entry_state}")
315
+ if entry_state == "needs-action":
316
+ print(f"ACTION NEEDED: {claude_dir / 'CLAUDE.md'} is user-owned and lacks '{IMPORT_LINE}' — "
317
+ "add the import line manually; the installer will not rewrite your file.")
318
+ sys.exit(2)
319
+
320
+
321
+ if __name__ == "__main__":
322
+ main()