agent-bios 0.9.4 → 0.9.6
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 +1 -1
- package/claude/guides/learning-flow.md +2 -2
- package/claude/settings.json +60 -0
- package/codex/AGENTS.md +1 -1
- package/codex/guides/learning-flow.md +2 -2
- package/config/domains.json +1 -0
- package/config/promotions.json +3 -1
- package/package.json +9 -2
- package/scripts/assemble.py +322 -0
- package/scripts/build-promotions.py +210 -0
- package/scripts/canary.sh +28 -0
- package/scripts/check-domains.py +256 -0
- package/scripts/ingest-learnings-export.py +258 -0
- package/scripts/install.sh +28 -0
- package/scripts/migrate-learnings.py +129 -12
- package/scripts/pkgid.py +45 -0
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 `
|
|
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).
|
|
@@ -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
|
|
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
|
-
|
|
|
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 `
|
|
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).
|
|
@@ -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
|
|
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
|
-
|
|
|
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
|
package/config/domains.json
CHANGED
package/config/promotions.json
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bios",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"releaseDate": "2026-07-
|
|
3
|
+
"version": "0.9.6",
|
|
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,12 @@
|
|
|
21
22
|
"config/promotions.json",
|
|
22
23
|
"shell/agent-launch.zsh",
|
|
23
24
|
"scripts/agent-launch.py",
|
|
25
|
+
"scripts/pkgid.py",
|
|
26
|
+
"scripts/assemble.py",
|
|
27
|
+
"scripts/check-domains.py",
|
|
28
|
+
"scripts/canary.sh",
|
|
29
|
+
"scripts/build-promotions.py",
|
|
30
|
+
"scripts/ingest-learnings-export.py",
|
|
24
31
|
"scripts/check-parity.sh",
|
|
25
32
|
"scripts/check-prompting-targets.sh",
|
|
26
33
|
"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()
|