agent-bios 0.2.0 → 0.3.0

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.
@@ -167,6 +167,25 @@ if [ "${#extra_c[@]}" -gt 0 ]; then
167
167
  for kv in "${extra_c[@]}"; do args+=(-c "$kv"); done
168
168
  fi
169
169
 
170
+ # Dispatch audit: verifier diversity is only as real as the pinned backing
171
+ # model — an unpinned dispatch inherits config defaults and can silently
172
+ # collapse two "different" reviewers onto one backend. The audit line goes to
173
+ # the log file only; stdout/stderr stay reserved for the codex channels.
174
+ sandbox_label="$sandbox"
175
+ if [ "$bypass_sandbox" -eq 1 ]; then sandbox_label="bypass"; fi
176
+ cmodel=""
177
+ if [ "${#extra_c[@]}" -gt 0 ]; then
178
+ for kv in "${extra_c[@]}"; do
179
+ case "$kv" in model=*) cmodel="${kv#model=}" ;; esac
180
+ done
181
+ fi
182
+ dispatch_note="dispatch profile=$profile model=${model:-INHERITED_DEFAULT}${cmodel:+ c-model-override=$cmodel} effort=${effort:-config-default} sandbox=$sandbox_label"
183
+ if [ -z "$model" ] && [ -z "$cmodel" ]; then
184
+ echo "codex-run: WARNING: no --model pin; the backing model inherits the active config default" >&2
185
+ fi
186
+ mkdir -p "$real_home/log" 2>/dev/null || true
187
+ printf '%s %s\n' "$(date +%Y-%m-%dT%H:%M:%S%z)" "$dispatch_note" >> "$real_home/log/codex-run-dispatch.log" 2>/dev/null || true
188
+
170
189
  # stdin, stdout, and stderr already match this adapter's channel contract.
171
190
  set +e
172
191
  CODEX_HOME="$run_home" codex "${args[@]}"
@@ -126,6 +126,152 @@ migrate_state() {
126
126
  mv "$LEGACY_STATE_DIR" "$STATE_DIR" && info "migrated state $LEGACY_STATE_DIR -> $STATE_DIR"
127
127
  }
128
128
 
129
+ # ---- codex live-config additions -----------------------------------------
130
+ # The live ~/.codex/config.toml is user/runtime-owned; agent-bios never
131
+ # deploys or overwrites it. codex/config-additions.toml declares the only
132
+ # content agent-bios manages there — one marked [agents.*] block plus a tagged
133
+ # features.multi_agent line — and this helper merges (install), checks
134
+ # (verify), or removes (uninstall) exactly that content, backed up and
135
+ # tomllib-validated before any write. Modes: merge | check | remove.
136
+ codex_config_additions() {
137
+ AB_MODE="$1" AB_CODEX_DIR="$CODEX_DIR" AB_FRAGMENT="$REPO/codex/config-additions.toml" \
138
+ AB_BACKUP="${BACKUP_DIR:-}" AB_DRY="$DRY_RUN" python3 - <<'PY'
139
+ import os, pathlib, sys, tomllib
140
+
141
+ mode = os.environ["AB_MODE"]
142
+ codex_dir = pathlib.Path(os.environ["AB_CODEX_DIR"])
143
+ fragment_path = pathlib.Path(os.environ["AB_FRAGMENT"])
144
+ backup_root = os.environ.get("AB_BACKUP", "")
145
+ dry = os.environ.get("AB_DRY") == "1"
146
+ target = codex_dir / "config.toml"
147
+ BEGIN = "# >>> agent-bios additions >>>"
148
+ END = "# <<< agent-bios additions <<<"
149
+ TAG = "# agent-bios"
150
+
151
+ def info(msg): print(f" {msg}")
152
+ def fail(msg): print(msg); sys.exit(1)
153
+
154
+ frag_text = fragment_path.read_text().replace("${CODEX_HOME}", str(codex_dir))
155
+ want_agents = tomllib.loads(frag_text)["agents"]
156
+ live_text = target.read_text() if target.is_file() else ""
157
+ try:
158
+ live = tomllib.loads(live_text) if live_text else {}
159
+ except Exception as exc:
160
+ fail(f"live codex config does not parse; not touching it: {target} ({exc})")
161
+
162
+ def state_ok():
163
+ if live.get("features", {}).get("multi_agent") is not True:
164
+ return False
165
+ return all(
166
+ live.get("agents", {}).get(name, {}).get(key) == spec[key]
167
+ for name, spec in want_agents.items()
168
+ for key in ("description", "config_file")
169
+ )
170
+
171
+ if mode == "check":
172
+ problems = []
173
+ if live.get("features", {}).get("multi_agent") is not True:
174
+ problems.append("features.multi_agent is not true")
175
+ for name, spec in want_agents.items():
176
+ if live.get("agents", {}).get(name, {}).get("config_file") != spec["config_file"]:
177
+ problems.append(f"agents.{name}.config_file drifted or missing")
178
+ elif not pathlib.Path(spec["config_file"]).is_file():
179
+ problems.append(f"agents.{name} template missing: {spec['config_file']}")
180
+ if problems:
181
+ fail(f"codex config additions: {'; '.join(problems)} ({target})")
182
+ info("codex config additions OK")
183
+ sys.exit(0)
184
+
185
+ def strip_managed(text):
186
+ out, skipping = [], False
187
+ for line in text.splitlines(keepends=True):
188
+ s = line.strip()
189
+ if s == BEGIN: skipping = True; continue
190
+ if s == END: skipping = False; continue
191
+ if skipping or s.endswith(TAG): continue
192
+ out.append(line)
193
+ return "".join(out)
194
+
195
+ if mode == "remove":
196
+ if not target.is_file():
197
+ sys.exit(0)
198
+ stripped = strip_managed(live_text)
199
+ if stripped == live_text:
200
+ info(f"no agent-bios additions in {target}")
201
+ sys.exit(0)
202
+ try:
203
+ tomllib.loads(stripped)
204
+ except Exception as exc:
205
+ fail(f"refusing removal; result would not parse: {exc}")
206
+ if dry:
207
+ info(f"[dry-run] remove agent-bios additions from {target}")
208
+ sys.exit(0)
209
+ backup = target.with_name(target.name + ".bak-agent-bios-uninstall")
210
+ backup.write_text(live_text)
211
+ target.write_text(stripped)
212
+ info(f"removed additions {target} (backup: {backup.name})")
213
+ sys.exit(0)
214
+
215
+ # mode == merge
216
+ if state_ok():
217
+ info(f"unchanged {target} (additions present)")
218
+ sys.exit(0)
219
+
220
+ # A drifted [agents.<tier>] outside our markers would become a duplicate
221
+ # table if we appended ours; that conflict needs the user, not a clobber.
222
+ base = strip_managed(live_text)
223
+ base_data = tomllib.loads(base) if base.strip() else {}
224
+ clash = [name for name in want_agents if name in base_data.get("agents", {})]
225
+ if clash:
226
+ fail(
227
+ f"unmanaged [agents.{'/'.join(clash)}] with drifted content in {target}; "
228
+ "align or remove them, then rerun install"
229
+ )
230
+
231
+ block_lines = [BEGIN]
232
+ if "features" not in base_data:
233
+ block_lines += ["[features]", f"multi_agent = true {TAG}"]
234
+ for name, spec in want_agents.items():
235
+ block_lines += [
236
+ f"[agents.{name}]",
237
+ f'description = "{spec["description"]}"',
238
+ f'config_file = "{spec["config_file"]}"',
239
+ ]
240
+ block_lines.append(END)
241
+ block = "\n".join(block_lines) + "\n"
242
+
243
+ new_text = base
244
+ if "features" in base_data:
245
+ if base_data["features"].get("multi_agent") is None:
246
+ lines = new_text.splitlines(keepends=True)
247
+ for i, line in enumerate(lines):
248
+ if line.strip() == "[features]":
249
+ lines.insert(i + 1, f"multi_agent = true {TAG}\n")
250
+ break
251
+ new_text = "".join(lines)
252
+ elif base_data["features"].get("multi_agent") is not True:
253
+ info(f"note: features.multi_agent explicitly set in {target}; leaving it")
254
+ if new_text and not new_text.endswith("\n"):
255
+ new_text += "\n"
256
+ new_text += ("\n" if new_text else "") + block
257
+
258
+ try:
259
+ tomllib.loads(new_text)
260
+ except Exception as exc:
261
+ fail(f"merge result would not parse; live config untouched ({exc})")
262
+ if dry:
263
+ info(f"[dry-run] merge agent-bios additions into {target}")
264
+ sys.exit(0)
265
+ if live_text and backup_root:
266
+ bpath = pathlib.Path(backup_root + str(target))
267
+ bpath.parent.mkdir(parents=True, exist_ok=True)
268
+ bpath.write_text(live_text)
269
+ target.parent.mkdir(parents=True, exist_ok=True)
270
+ target.write_text(new_text)
271
+ info(f"merged additions {target}")
272
+ PY
273
+ }
274
+
129
275
  # ---- optional dependencies -----------------------------------------------
130
276
  # Capabilities are optional: a missing one only degrades the review routes that
131
277
  # need it. config/agent-launch.toml is the single source for both the command
@@ -255,9 +401,12 @@ cmd_install() {
255
401
  log "Deploying agent-bios from $REPO"
256
402
  deploy_file "$REPO/claude/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
257
403
  deploy_glob "$REPO/claude/guides" "*.md" "$CLAUDE_DIR/guides"
404
+ deploy_glob "$REPO/claude/agents" "*.md" "$CLAUDE_DIR/agents"
405
+ deploy_glob "$REPO/claude/hooks" "*.py" "$CLAUDE_DIR/hooks" "+x"
258
406
  deploy_file "$REPO/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md"
259
407
  deploy_glob "$REPO/codex/guides" "*.md" "$CODEX_DIR/guides"
260
408
  deploy_glob "$REPO/codex/agents" "*.toml" "$CODEX_DIR/agents"
409
+ codex_config_additions merge || exit 1
261
410
  deploy_file "$REPO/scripts/codex-run.sh" "$CODEX_DIR/bin/codex-run" "+x"
262
411
  deploy_file "$REPO/scripts/codex-helm.sh" "$CODEX_DIR/bin/codex-helm" "+x"
263
412
  migrate_user_presets || exit 1 # must precede the deploy below, which overwrites profiles.toml
@@ -275,12 +424,19 @@ cmd_install() {
275
424
  || log "warning: venv provisioning failed (numbered-prompt fallback applies)"
276
425
  fi
277
426
  add_zsh_hook
427
+ if python3 "$REPO/scripts/session-learning/learning-state.py" project --repo "$REPO" >/dev/null 2>&1; then
428
+ info "learning-status projected"
429
+ else
430
+ log "note: learning-status projection unavailable (versions.json/ledger missing?)"
431
+ fi
278
432
  log ""
279
433
  log "Verifying deployment..."
280
434
  if cmd_verify; then
281
435
  log ""
282
436
  log "Done. Open a new shell (or: source \"$ZSHRC\") to activate the zero-arg launcher."
283
- [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ] && log "Replaced files were backed up under $BACKUP_DIR"
437
+ # An untouched backup dir means nothing was replaced; that healthy state
438
+ # must not become a nonzero exit under set -e.
439
+ { [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ] && log "Replaced files were backed up under $BACKUP_DIR"; } || true
284
440
  else
285
441
  log "VERIFY FAILED after install — see messages above"
286
442
  exit 1
@@ -298,6 +454,8 @@ cmd_verify() {
298
454
  verify_match "$REPO/config/agent-launch.toml" "$LAUNCH_DIR/profiles.toml" || fail=1
299
455
  verify_match "$REPO/shell/agent-launch.zsh" "$LAUNCH_DIR/shell.zsh" || fail=1
300
456
  for gp in "$REPO"/claude/guides/*.md; do verify_present "$CLAUDE_DIR/guides/$(basename "$gp")" || fail=1; done
457
+ for gp in "$REPO"/claude/agents/*.md; do verify_present "$CLAUDE_DIR/agents/$(basename "$gp")" || fail=1; done
458
+ for gp in "$REPO"/claude/hooks/*.py; do verify_present "$CLAUDE_DIR/hooks/$(basename "$gp")" || fail=1; done
301
459
  for gp in "$REPO"/codex/guides/*.md; do verify_present "$CODEX_DIR/guides/$(basename "$gp")" || fail=1; done
302
460
  python3 - "$CODEX_DIR/agents" <<'PY' && info "agent TOMLs OK" || fail=1
303
461
  import sys, pathlib, tomllib
@@ -308,6 +466,7 @@ assert not missing, f"missing agent TOMLs in {root}: {sorted(missing)}"
308
466
  for p in sorted(root.glob("*.toml")):
309
467
  tomllib.loads(p.read_text())
310
468
  PY
469
+ codex_config_additions check || fail=1
311
470
  if command -v codex >/dev/null 2>&1 && [ -x "$CODEX_DIR/bin/codex-helm" ]; then
312
471
  if "$CODEX_DIR/bin/codex-helm" --dry-run --mode review "probe" >/dev/null 2>&1; then
313
472
  info "codex-helm dry-run OK"
@@ -340,6 +499,7 @@ PY
340
499
 
341
500
  cmd_uninstall() {
342
501
  migrate_state
502
+ codex_config_additions remove || log "warning: could not remove codex config additions"
343
503
  if [ -f "$MANIFEST" ]; then
344
504
  local f
345
505
  while IFS= read -r f; do
@@ -357,7 +517,7 @@ cmd_uninstall() {
357
517
  done
358
518
  fi
359
519
  local d
360
- for d in "$CLAUDE_DIR/guides" "$CODEX_DIR/guides" "$CODEX_DIR/agents" "$CODEX_DIR/bin" "$LAUNCH_DIR"; do
520
+ for d in "$CLAUDE_DIR/guides" "$CLAUDE_DIR/agents" "$CODEX_DIR/guides" "$CODEX_DIR/agents" "$CODEX_DIR/bin" "$LAUNCH_DIR"; do
361
521
  [ -d "$d" ] && rmdir "$d" 2>/dev/null && info "removed empty $d" || true
362
522
  done
363
523
  remove_zsh_hook