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 +1 -1
- package/claude/agents/workhorse.md +2 -2
- package/claude/guides/cli-multi-model-workflow.md +2 -2
- package/claude/guides/learning-flow.md +2 -2
- package/claude/settings.json +60 -0
- package/codex/AGENTS.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +2 -2
- package/codex/guides/learning-flow.md +2 -2
- package/config/agent-launch.toml +2 -2
- package/package.json +8 -2
- package/scripts/assemble.py +322 -0
- package/scripts/build-promotions.py +183 -0
- package/scripts/canary.sh +28 -0
- package/scripts/check-domains.py +242 -0
- package/scripts/check-parity.sh +2 -2
- package/scripts/ingest-learnings-export.py +258 -0
- package/scripts/install.sh +28 -0
- package/scripts/session-cost.py +38 -15
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).
|
|
@@ -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-
|
|
5
|
-
effort:
|
|
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
|
|
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
|
|
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
|
|
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).
|
|
@@ -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
|
|
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
|
|
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
|
|
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/agent-launch.toml
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.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()
|