agent-bios 0.9.7 → 0.9.8
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/package.json +2 -1
- package/scripts/assemble.py +23 -7
- package/scripts/install.sh +14 -2
- package/scripts/register-hooks.py +44 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bios",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8",
|
|
4
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": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"shell/agent-launch.zsh",
|
|
24
24
|
"scripts/agent-launch.py",
|
|
25
25
|
"scripts/pkgid.py",
|
|
26
|
+
"scripts/register-hooks.py",
|
|
26
27
|
"scripts/assemble.py",
|
|
27
28
|
"scripts/check-domains.py",
|
|
28
29
|
"scripts/canary.sh",
|
package/scripts/assemble.py
CHANGED
|
@@ -152,16 +152,31 @@ def copy_filtered(src_dir, names, dest, rewrite=None, dry=False):
|
|
|
152
152
|
(dest / n).write_text(body, encoding="utf-8")
|
|
153
153
|
|
|
154
154
|
|
|
155
|
-
def merge_settings(claude_dir, hook_names, template_path, dry=False):
|
|
156
|
-
"""
|
|
157
|
-
|
|
155
|
+
def merge_settings(claude_dir, hook_names, template_path, dry=False, owned_names=None):
|
|
156
|
+
"""Merge our hook registrations into the user's settings, owning by NAME.
|
|
157
|
+
|
|
158
|
+
`hook_names` is what to register now (the selection); `owned_names` is every
|
|
159
|
+
hook the manifest declares, which is what we may remove. Ownership is the
|
|
160
|
+
manifest name rather than a path marker because the same hook has lived at
|
|
161
|
+
two paths — `<claude>/hooks/` on the old full-install layout and
|
|
162
|
+
`central/hooks/` now — and a path-marker drop would leave the old entry
|
|
163
|
+
behind, registering the hook twice after the move. Dropping every declared
|
|
164
|
+
name and re-adding only the selected ones also makes deselection work.
|
|
165
|
+
|
|
166
|
+
Entries we do not own are never touched: `<claude>/hooks/` is shared with
|
|
167
|
+
other tools' hooks and state.
|
|
168
|
+
"""
|
|
169
|
+
owned = set(owned_names if owned_names is not None else hook_names)
|
|
158
170
|
spath = claude_dir / "settings.json"
|
|
159
171
|
settings = json.loads(spath.read_text(encoding="utf-8")) if spath.exists() else {}
|
|
160
172
|
template = json.loads(template_path.read_text(encoding="utf-8")) if template_path.exists() else {}
|
|
161
173
|
hooks = settings.setdefault("hooks", {})
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
174
|
+
|
|
175
|
+
def ours(en):
|
|
176
|
+
return any(n in h.get("command", "") for h in en.get("hooks", []) for n in owned)
|
|
177
|
+
|
|
178
|
+
for event, entries in list(hooks.items()): # drop every entry we own
|
|
179
|
+
hooks[event] = [en for en in entries if not ours(en)]
|
|
165
180
|
for event, entries in template.get("hooks", {}).items(): # re-add per selection
|
|
166
181
|
for en in entries:
|
|
167
182
|
cmds = [h.get("command", "") for h in en.get("hooks", [])]
|
|
@@ -299,7 +314,8 @@ def main():
|
|
|
299
314
|
rewrite=(f"{CLAUDE_VAR}/guides/", f"{CLAUDE_VAR}/central/guides/"), dry=dry)
|
|
300
315
|
copy_filtered(REPO / "claude" / "hooks", hooks, central / "hooks", dry=dry)
|
|
301
316
|
copy_filtered(REPO / "claude" / "agents", agents, central / "agents", dry=dry)
|
|
302
|
-
merge_settings(claude_dir, hooks, REPO / "claude" / "settings.template.json", dry=dry
|
|
317
|
+
merge_settings(claude_dir, hooks, REPO / "claude" / "settings.template.json", dry=dry,
|
|
318
|
+
owned_names=manifest.get("hooks", {})) # deselected hooks must drop too
|
|
303
319
|
entry_state = seed_entry(claude_dir, monolith, dry=dry)
|
|
304
320
|
|
|
305
321
|
merge_codex(codex_dir, codex_bundle, dry=dry)
|
package/scripts/install.sh
CHANGED
|
@@ -111,6 +111,15 @@ deploy_glob() {
|
|
|
111
111
|
# marker region, settings merge); the entry CLAUDE.md and AGENTS.md are NOT
|
|
112
112
|
# manifested — the entry is user-owned after seeding, AGENTS.md holds a
|
|
113
113
|
# personal region — so uninstall never deletes them.
|
|
114
|
+
# Full mode never runs the assembler, so nothing registered the hooks it
|
|
115
|
+
# deployed — the files landed and never fired. Reuse the assembler's own merge
|
|
116
|
+
# so both install paths register identically, under one ownership rule.
|
|
117
|
+
register_hooks_full() {
|
|
118
|
+
[ "$DRY_RUN" = 1 ] && { info "[dry-run] register central hooks"; return 0; }
|
|
119
|
+
python3 "$REPO/scripts/register-hooks.py" "$REPO" "$CLAUDE_DIR" \
|
|
120
|
+
|| log "note: hook registration failed; the deployed hooks will not fire"
|
|
121
|
+
}
|
|
122
|
+
|
|
114
123
|
packaged_mode() { [ "${DOMAINS_SET:-0}" = 1 ] || [ -f "$STATE_DIR/selection.json" ]; }
|
|
115
124
|
|
|
116
125
|
assemble_packaged() {
|
|
@@ -450,7 +459,10 @@ cmd_install() {
|
|
|
450
459
|
deploy_file "$REPO/claude/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
|
|
451
460
|
deploy_glob "$REPO/claude/guides" "*.md" "$CLAUDE_DIR/guides"
|
|
452
461
|
deploy_glob "$REPO/claude/agents" "*.md" "$CLAUDE_DIR/agents"
|
|
453
|
-
|
|
462
|
+
# Our hooks go under central/: <claude>/hooks is shared with other tools
|
|
463
|
+
# files, caches, and state, and we must not own a directory we share.
|
|
464
|
+
deploy_glob "$REPO/claude/hooks" "*.py" "$CLAUDE_DIR/central/hooks" "+x"
|
|
465
|
+
register_hooks_full
|
|
454
466
|
deploy_file "$REPO/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md"
|
|
455
467
|
deploy_glob "$REPO/codex/guides" "*.md" "$CODEX_DIR/guides"
|
|
456
468
|
fi
|
|
@@ -540,7 +552,7 @@ cmd_verify() {
|
|
|
540
552
|
verify_match "$REPO/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md" || fail=1
|
|
541
553
|
for gp in "$REPO"/claude/guides/*.md; do verify_present "$CLAUDE_DIR/guides/$(basename "$gp")" || fail=1; done
|
|
542
554
|
for gp in "$REPO"/claude/agents/*.md; do verify_present "$CLAUDE_DIR/agents/$(basename "$gp")" || fail=1; done
|
|
543
|
-
for gp in "$REPO"/claude/hooks/*.py; do verify_present "$CLAUDE_DIR/hooks/$(basename "$gp")" || fail=1; done
|
|
555
|
+
for gp in "$REPO"/claude/hooks/*.py; do verify_present "$CLAUDE_DIR/central/hooks/$(basename "$gp")" || fail=1; done
|
|
544
556
|
for gp in "$REPO"/codex/guides/*.md; do verify_present "$CODEX_DIR/guides/$(basename "$gp")" || fail=1; done
|
|
545
557
|
fi
|
|
546
558
|
verify_match "$REPO/scripts/agent-launch.py" "$BIN_DIR/agent-launch" || fail=1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Register the manifest's hooks in a deployed settings.json (full-install path).
|
|
3
|
+
|
|
4
|
+
The packaged path gets this for free: assemble.py composes the corpus and calls
|
|
5
|
+
merge_settings on the way out. A full install never runs the assembler, so the
|
|
6
|
+
hook files were deployed and nothing ever registered them — they sat on disk and
|
|
7
|
+
never fired. This runs the assembler's own merge so both paths register
|
|
8
|
+
identically, under the same name-based ownership rule.
|
|
9
|
+
|
|
10
|
+
Usage: register-hooks.py <repo> <claude-dir>
|
|
11
|
+
Exit 0 on success; non-zero (with a message) if the merge could not run, which
|
|
12
|
+
the installer reports as a note rather than failing the whole install.
|
|
13
|
+
"""
|
|
14
|
+
import importlib.util
|
|
15
|
+
import json
|
|
16
|
+
import pathlib
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_assemble(repo):
|
|
21
|
+
"""Import assemble.py by path — scripts/ is a flat toolbox, not a package."""
|
|
22
|
+
spec = importlib.util.spec_from_file_location("assemble", repo / "scripts" / "assemble.py")
|
|
23
|
+
mod = importlib.util.module_from_spec(spec)
|
|
24
|
+
spec.loader.exec_module(mod)
|
|
25
|
+
return mod
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main():
|
|
29
|
+
if len(sys.argv) != 3:
|
|
30
|
+
sys.exit("usage: register-hooks.py <repo> <claude-dir>")
|
|
31
|
+
repo, claude_dir = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
|
|
32
|
+
manifest = json.loads((repo / "config" / "domains.json").read_text(encoding="utf-8"))
|
|
33
|
+
names = sorted(manifest.get("hooks", {}))
|
|
34
|
+
if not names:
|
|
35
|
+
sys.exit("register-hooks: manifest declares no hooks — refusing to rewrite settings")
|
|
36
|
+
mod = load_assemble(repo)
|
|
37
|
+
mod.merge_settings(claude_dir, names,
|
|
38
|
+
repo / "claude" / "settings.template.json",
|
|
39
|
+
owned_names=names)
|
|
40
|
+
print(f" hooks registered ({len(names)})")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|