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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bios",
3
- "version": "0.9.7",
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",
@@ -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
- """Path-marker merge: central-owned hook entries reference central/hooks/."""
157
- marker = "/central/hooks/"
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
- 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", []))]
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)
@@ -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
- deploy_glob "$REPO/claude/hooks" "*.py" "$CLAUDE_DIR/hooks" "+x"
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()