agent-bios 0.9.8 → 0.9.9
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/DEPENDENCIES.md +19 -19
- package/README.md +34 -11
- package/claude/CLAUDE.md +2 -1
- package/claude/guides/claude-prompting.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +19 -1
- package/claude/guides/coding-staged-workflow.md +32 -0
- package/claude/guides/gpt-prompting.md +1 -1
- package/claude/guides/learning-flow.md +5 -5
- package/claude/guides/llm-capability-boundary.md +6 -1
- package/claude/guides/session-distill-workflow.md +19 -9
- package/claude/guides/tooling-gotchas.md +16 -0
- package/claude/hooks/__pycache__/tooling-gotchas-hook.cpython-314.pyc +0 -0
- package/claude/hooks/tooling-gotchas-hook.py +7 -0
- package/codex/AGENTS.md +2 -1
- package/codex/guides/claude-prompting.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +19 -1
- package/codex/guides/coding-staged-workflow.md +32 -0
- package/codex/guides/gpt-prompting.md +1 -1
- package/codex/guides/learning-flow.md +5 -5
- package/codex/guides/llm-capability-boundary.md +6 -1
- package/codex/guides/session-distill-workflow.md +19 -9
- package/codex/guides/tooling-gotchas.md +16 -0
- package/{scripts → compose}/assemble.py +184 -16
- package/{scripts → compose}/canary.sh +14 -5
- package/{scripts → compose}/check-domains.py +9 -3
- package/{config → compose}/domains.json +1 -0
- package/{scripts → compose}/pkgid.py +8 -1
- package/compose/prune-backups.py +204 -0
- package/{scripts → compose}/register-hooks.py +3 -3
- package/{scripts/install.sh → install.sh} +401 -104
- package/launch/agent-launch.py +5294 -0
- package/launch/agent-launch.toml +376 -0
- package/{scripts → launch}/check-prompting-targets.sh +1 -1
- package/{scripts → launch}/provision-venv.sh +1 -1
- package/{scripts → learn}/check-learning.py +7 -7
- package/{scripts → learn}/collect-learning.py +10 -10
- package/{config → learn}/learning.schema.json +3 -3
- package/{scripts → learn}/migrate-learnings.py +95 -54
- package/{scripts → learn}/redact.py +4 -4
- package/package.json +25 -24
- package/wrappers/claude-run.sh +162 -0
- package/{scripts → wrappers}/codex-run.sh +62 -6
- package/config/agent-launch.toml +0 -143
- package/scripts/agent-launch.py +0 -2350
- package/scripts/check-parity.sh +0 -2003
- /package/{shell → launch}/agent-launch.zsh +0 -0
- /package/{config → learn}/promotions.json +0 -0
- /package/{scripts/session-cost.py → session-cost.py} +0 -0
- /package/{scripts → wrappers}/codex-helm.sh +0 -0
|
@@ -25,7 +25,14 @@ set -euo pipefail
|
|
|
25
25
|
# CI, pipes, and background runs, where stdin stays open but never delivers.
|
|
26
26
|
# `learn` is the one subcommand whose payload IS stdin, so keep the caller's on
|
|
27
27
|
# fd 3 first and hand it back only there; every other path still sees /dev/null.
|
|
28
|
-
|
|
28
|
+
# The braces matter. `exec` with redirections and no command applies them to the
|
|
29
|
+
# SHELL, permanently — so the bare `exec 3<&0 2>/dev/null` this used to be sent
|
|
30
|
+
# every later error message on this script's stderr to /dev/null: bash's own
|
|
31
|
+
# set -e diagnostics, python tracebacks from deployed steps, and any `>&2` an
|
|
32
|
+
# author writes. Only stdout survived, which is why a failing install could stop
|
|
33
|
+
# with no reason on screen. The group scopes the suppression to the one command
|
|
34
|
+
# whose error is expected when a caller closed fd 0.
|
|
35
|
+
{ exec 3<&0; } 2>/dev/null || exec 3</dev/null # tolerate a caller that closed fd 0
|
|
29
36
|
exec </dev/null
|
|
30
37
|
|
|
31
38
|
# Resolve this script through symlinks before locating the package: npm links the
|
|
@@ -42,7 +49,7 @@ while [ -L "$SOURCE" ]; do
|
|
|
42
49
|
esac
|
|
43
50
|
done
|
|
44
51
|
SELF="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
45
|
-
REPO="$
|
|
52
|
+
REPO="$SELF"
|
|
46
53
|
|
|
47
54
|
CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
48
55
|
CODEX_DIR="${CODEX_HOME:-$HOME/.codex}"
|
|
@@ -52,6 +59,11 @@ BIN_DIR="$HOME/.local/bin"
|
|
|
52
59
|
STATE_DIR="$HOME/.local/share/agent-bios"
|
|
53
60
|
LEGACY_STATE_DIR="$HOME/.local/share/agent-dotfiles" # pre-rename state; migrated on first run
|
|
54
61
|
MANIFEST="$STATE_DIR/manifest.txt"
|
|
62
|
+
# The manifest is truncated at the start of every install, so what the PREVIOUS one deployed
|
|
63
|
+
# must be preserved before that happens. It is the only record that a file now on disk was
|
|
64
|
+
# written by us rather than authored by the user, and seed_entry needs exactly that to tell a
|
|
65
|
+
# previous release's deployed entry from someone's own CLAUDE.md.
|
|
66
|
+
PRIOR_MANIFEST="$STATE_DIR/manifest.prev.txt"
|
|
55
67
|
ZSHRC="${ZDOTDIR:-$HOME}/.zshrc"
|
|
56
68
|
ZSH_HOOK='[ -r "$HOME/.config/agent-launch/shell.zsh" ] && source "$HOME/.config/agent-launch/shell.zsh"'
|
|
57
69
|
HOOK_MARK='agent-launch/shell.zsh'
|
|
@@ -105,35 +117,129 @@ deploy_glob() {
|
|
|
105
117
|
done
|
|
106
118
|
}
|
|
107
119
|
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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"
|
|
120
|
+
# Backups are made on every install and every uninstall, and nothing used to remove them — 23
|
|
121
|
+
# directories in two weeks on the first machine measured, plus loose `.bak-*` files sitting in
|
|
122
|
+
# the user's own config dirs. Pruning runs AFTER the new copy exists, never before, so a failed
|
|
123
|
+
# run cannot leave the user with neither the change nor its backup.
|
|
124
|
+
prune_backups() {
|
|
125
|
+
[ -f "$REPO/compose/prune-backups.py" ] || return 0
|
|
126
|
+
python3 "$REPO/compose/prune-backups.py" --state-dir "$STATE_DIR" \
|
|
127
|
+
--claude-dir "$CLAUDE_DIR" --codex-dir "$CODEX_DIR" \
|
|
128
|
+
$([ "$DRY_RUN" = 1 ] && echo --dry-run) || log "note: backup pruning skipped"
|
|
121
129
|
}
|
|
122
130
|
|
|
123
|
-
|
|
131
|
+
# Guides whose frontmatter declares `audience: author`. They document steps only
|
|
132
|
+
# the corpus author can perform and name paths that exist in a checkout and
|
|
133
|
+
# nowhere else, so they are never installed. compose/assemble.py owns the
|
|
134
|
+
# declaration and withholds them from the destinations it writes; this asks it
|
|
135
|
+
# rather than keeping a second parser. Both callers below need that same answer
|
|
136
|
+
# for ground the assembler never writes — the sweep of the pre-unification
|
|
137
|
+
# $CLAUDE_DIR/guides, and verify's check that the absence holds in all three
|
|
138
|
+
# directories. Absence of the assembler degrades to withholding nothing.
|
|
139
|
+
# Fails LOUD, never open. An empty answer here means "withhold nothing", so
|
|
140
|
+
# swallowing an unreadable declaration would deploy exactly the guides this rule
|
|
141
|
+
# exists to hold back — the fail-open shape of every other degrade-on-absence
|
|
142
|
+
# fallback in this file, but inverted. compose/assemble.py ships, so its absence
|
|
143
|
+
# is a broken payload rather than a configuration a user might have.
|
|
144
|
+
author_only_guides() {
|
|
145
|
+
local out
|
|
146
|
+
if ! out=$(python3 - "$REPO" <<'PY'
|
|
147
|
+
import pathlib, sys
|
|
148
|
+
root = pathlib.Path(sys.argv[1])
|
|
149
|
+
sys.path.insert(0, str(root / "compose"))
|
|
150
|
+
try:
|
|
151
|
+
from assemble import author_only
|
|
152
|
+
except ImportError as exc:
|
|
153
|
+
raise SystemExit(f"compose/assemble.py owns the audience declaration and "
|
|
154
|
+
f"could not be imported ({exc})")
|
|
155
|
+
guides = root / "claude" / "guides"
|
|
156
|
+
if not guides.is_dir():
|
|
157
|
+
raise SystemExit(f"{guides} is missing; the payload is incomplete")
|
|
158
|
+
for p in sorted(guides.glob("*.md")):
|
|
159
|
+
if author_only(p):
|
|
160
|
+
print(p.name)
|
|
161
|
+
PY
|
|
162
|
+
); then
|
|
163
|
+
# stderr, not stdout: every caller reads this function through a command
|
|
164
|
+
# substitution, so anything printed normally would be captured INTO the
|
|
165
|
+
# variable instead of reaching the operator — and `exit` here would leave
|
|
166
|
+
# only the subshell, letting the caller proceed with the error text as its
|
|
167
|
+
# list of withheld guides. The caller checks the status and exits itself.
|
|
168
|
+
log "cannot read which guides are author-only — refusing to deploy rather than" >&2
|
|
169
|
+
log " shipping them by default. compose/assemble.py owns that declaration and" >&2
|
|
170
|
+
log " must be present; reinstall the package." >&2
|
|
171
|
+
return 1
|
|
172
|
+
fi
|
|
173
|
+
printf '%s\n' "$out"
|
|
174
|
+
}
|
|
124
175
|
|
|
125
|
-
|
|
176
|
+
# Remove a withheld guide from a directory this installer deploys into, backing it
|
|
177
|
+
# up first exactly as deploy_file backs up a file it is about to overwrite. An
|
|
178
|
+
# unconditional rm would delete a same-named file this installer never wrote, and
|
|
179
|
+
# a shared or symlinked guides directory makes that somebody's own file.
|
|
180
|
+
prune_withheld() {
|
|
181
|
+
local dstdir="$1" base
|
|
182
|
+
printf '%s\n' "$WITHHELD_GUIDES" | while IFS= read -r base; do
|
|
183
|
+
[ -n "$base" ] || continue
|
|
184
|
+
[ -f "$dstdir/$base" ] || continue
|
|
185
|
+
if [ -n "$BACKUP_DIR" ]; then
|
|
186
|
+
run mkdir -p "$(dirname "$BACKUP_DIR$dstdir/$base")"
|
|
187
|
+
run cp "$dstdir/$base" "$BACKUP_DIR$dstdir/$base"
|
|
188
|
+
fi
|
|
189
|
+
run rm -f "$dstdir/$base"
|
|
190
|
+
[ "$DRY_RUN" = 1 ] || info "withheld (audience: author), removed stale copy: $dstdir/$base"
|
|
191
|
+
done
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# ---- corpus deploy -------------------------------------------------------
|
|
195
|
+
# assemble.py owns the corpus surfaces (central tree, entry seeding, codex marker region,
|
|
196
|
+
# settings merge). The entry CLAUDE.md and AGENTS.md are NOT manifested — the entry is
|
|
197
|
+
# user-owned after seeding and AGENTS.md holds a personal region — so uninstall removes our
|
|
198
|
+
# central tree and marked regions and leaves the user's file itself alone.
|
|
199
|
+
# There is ONE install shape. There used to be two: a "full" deploy that wrote the corpus into
|
|
200
|
+
# the entry file, and a packaged one that assembled selected domains under `central/` and left the
|
|
201
|
+
# entry file to the user. They differed in the thing that matters most — who owns the entry file —
|
|
202
|
+
# so the same path was ours in one mode and theirs in the other, and no rule about user-owned
|
|
203
|
+
# content could be true of both. Full mode is now "every domain selected": the same assembly, the
|
|
204
|
+
# same ownership, one set of answers.
|
|
205
|
+
#
|
|
206
|
+
# What this buys, in the order the questions were asked: the user's own additions live in a file
|
|
207
|
+
# we never rewrite, so they are separately versioned by construction; a package can be added or
|
|
208
|
+
# dropped later by re-assembling, with no need to know which bytes came from where; and uninstall
|
|
209
|
+
# can take everything of ours because nothing of theirs is mixed into it.
|
|
210
|
+
assemble_corpus() {
|
|
126
211
|
local args=(--claude-dir "$CLAUDE_DIR" --codex-dir "$CODEX_DIR" --state-dir "$STATE_DIR") rc=0
|
|
127
|
-
[ "${DOMAINS_SET:-0}" = 1 ]
|
|
212
|
+
if [ "${DOMAINS_SET:-0}" = 1 ]; then
|
|
213
|
+
args+=(--domains "$DOMAINS_ARG")
|
|
214
|
+
elif [ ! -f "$STATE_DIR/selection.json" ]; then
|
|
215
|
+
# No flag and no saved selection: install everything. This is what "full" meant, expressed as
|
|
216
|
+
# a selection so it goes down the same path as every other one.
|
|
217
|
+
args+=(--domains "$(python3 -c "import json,sys;print(','.join(sorted(json.load(open(sys.argv[1]))['domains'])))" "$REPO/compose/domains.json")")
|
|
218
|
+
fi
|
|
128
219
|
[ "$DRY_RUN" = 1 ] && args+=(--dry-run)
|
|
129
|
-
|
|
220
|
+
# A dry run never truncated the manifest, so the live one still describes the last install.
|
|
221
|
+
local prior="$PRIOR_MANIFEST"
|
|
222
|
+
[ "$DRY_RUN" = 1 ] && prior="$MANIFEST"
|
|
223
|
+
[ -f "$prior" ] && args+=(--prior-manifest "$prior")
|
|
224
|
+
python3 "$REPO/compose/assemble.py" "${args[@]}" || rc=$?
|
|
130
225
|
if [ "$rc" = 2 ]; then
|
|
131
|
-
log "
|
|
226
|
+
log "entry file needs user action (import line missing); central content will not load until it is added"
|
|
132
227
|
elif [ "$rc" != 0 ]; then
|
|
133
228
|
return 1
|
|
134
229
|
fi
|
|
135
230
|
if [ "$DRY_RUN" != 1 ]; then
|
|
136
|
-
|
|
231
|
+
# `central/` is ours whole, so scanning it is right. `$CODEX_DIR/guides` is SHARED — the
|
|
232
|
+
# user and other tools keep files there, and the assembler deliberately leaves any name it
|
|
233
|
+
# did not deploy alone. Scanning that directory claimed those files anyway, and uninstall
|
|
234
|
+
# removes whatever the manifest names, so the protection was undone one step later. The
|
|
235
|
+
# ownership rule is the assembler's: the name exists in our source tree.
|
|
236
|
+
find "$CLAUDE_DIR/central" -type f 2>/dev/null >> "$MANIFEST"
|
|
237
|
+
local g dst
|
|
238
|
+
for g in "$REPO"/codex/guides/*.md; do
|
|
239
|
+
[ -f "$g" ] || continue
|
|
240
|
+
dst="$CODEX_DIR/guides/$(basename "$g")"
|
|
241
|
+
[ -f "$dst" ] && printf '%s\n' "$dst" >> "$MANIFEST"
|
|
242
|
+
done
|
|
137
243
|
fi
|
|
138
244
|
}
|
|
139
245
|
|
|
@@ -309,13 +415,25 @@ PY
|
|
|
309
415
|
|
|
310
416
|
# ---- optional dependencies -----------------------------------------------
|
|
311
417
|
# Capabilities are optional: a missing one only degrades the review routes that
|
|
312
|
-
# need it.
|
|
418
|
+
# need it. launch/agent-launch.toml is the single source for both the command
|
|
313
419
|
# that gates a route and the install line offered here.
|
|
314
420
|
capability_table() {
|
|
315
|
-
python3 - "$REPO/
|
|
421
|
+
python3 - "$REPO/launch/agent-launch.toml" <<'PY'
|
|
316
422
|
import sys, tomllib
|
|
317
|
-
|
|
318
|
-
|
|
423
|
+
# `${backend}` means "the CLI of the host this capability offers on". Resolved HERE because
|
|
424
|
+
# the probe below runs `command -v` on whatever this prints, and the literal sentinel is not
|
|
425
|
+
# a command: the built-in workflow capability reported as unavailable while the launcher ran
|
|
426
|
+
# it perfectly well.
|
|
427
|
+
data = tomllib.load(open(sys.argv[1], "rb"))
|
|
428
|
+
backends = data.get("backends", {})
|
|
429
|
+
for name, cap in data.get("capabilities", {}).items():
|
|
430
|
+
command = cap.get("command", "")
|
|
431
|
+
if command == "${backend}":
|
|
432
|
+
hosts = sorted({h for offer in cap.get("offers", []) for h in offer.get("hosts", [])})
|
|
433
|
+
command = next(
|
|
434
|
+
(backends[h]["command"] for h in hosts if backends.get(h, {}).get("command")), ""
|
|
435
|
+
)
|
|
436
|
+
print("\t".join((name, command, cap.get("install", ""))))
|
|
319
437
|
PY
|
|
320
438
|
}
|
|
321
439
|
|
|
@@ -339,6 +457,16 @@ handle_capabilities() {
|
|
|
339
457
|
[ -n "$name" ] || continue
|
|
340
458
|
if command -v "$command" >/dev/null 2>&1; then
|
|
341
459
|
info "capability present $name ($command)"
|
|
460
|
+
# Explicitly requested, already there, and nothing to install: say so, because after
|
|
461
|
+
# the rename `--with ultracode` names the claude-backed reviewer — which needs no
|
|
462
|
+
# install — while the tool the user probably meant stays missing and is only hinted
|
|
463
|
+
# at further down. The request silently no-opped and install still exited 0.
|
|
464
|
+
if [ -z "$line" ] && printf '%s\n' "${requested//,/ }" | tr ' ' '\n' | grep -qx "$name"; then
|
|
465
|
+
log "note: $name needs no install (it is the host CLI); nothing was installed for it"
|
|
466
|
+
local installable
|
|
467
|
+
installable=$(capability_table | awk -F'\t' -v me="$name" '$1 != me && $3 != "" { print $1 }' | tr '\n' ' ')
|
|
468
|
+
[ -n "$installable" ] && log " capabilities that DO install: ${installable% }"
|
|
469
|
+
fi
|
|
342
470
|
continue
|
|
343
471
|
fi
|
|
344
472
|
if printf '%s\n' "${requested//,/ }" | tr ' ' '\n' | grep -qx "$name"; then
|
|
@@ -362,7 +490,7 @@ EOF
|
|
|
362
490
|
# Presets the launcher saved into the deployed profiles.toml (pre-split, or by hand)
|
|
363
491
|
# would be lost to the cp below; move them into the user-owned presets file first.
|
|
364
492
|
migrate_user_presets() {
|
|
365
|
-
local src="$REPO/
|
|
493
|
+
local src="$REPO/launch/agent-launch.toml"
|
|
366
494
|
local dst="$LAUNCH_DIR/profiles.toml"
|
|
367
495
|
local user="$LAUNCH_DIR/$USER_PRESETS_NAME"
|
|
368
496
|
[ -f "$dst" ] || return 0
|
|
@@ -429,10 +557,10 @@ remove_zsh_hook() {
|
|
|
429
557
|
# corpus AND are in this user's assembled bundle. Best-effort: a prune failure
|
|
430
558
|
# (or an absent manifest/script) never fails the install. Runs per host.
|
|
431
559
|
migrate_learnings() {
|
|
432
|
-
local script="$REPO/
|
|
433
|
-
{ [ -f "$script" ] && [ -f "$REPO/
|
|
560
|
+
local script="$REPO/learn/migrate-learnings.py"
|
|
561
|
+
{ [ -f "$script" ] && [ -f "$REPO/learn/promotions.json" ]; } || return 0
|
|
434
562
|
local -a sel dry
|
|
435
|
-
|
|
563
|
+
sel=(--selection-file "$STATE_DIR/selection.json")
|
|
436
564
|
[ "$DRY_RUN" = 1 ] && dry=(--dry-run) || dry=()
|
|
437
565
|
# ${dry[@]+...}: expanding an empty array as "${dry[@]}" is an unbound-variable
|
|
438
566
|
# error under `set -u` on bash 3.2 (macOS default) and would abort the install.
|
|
@@ -449,32 +577,31 @@ cmd_install() {
|
|
|
449
577
|
if [ "$DRY_RUN" != 1 ]; then
|
|
450
578
|
mkdir -p "$STATE_DIR"
|
|
451
579
|
BACKUP_DIR="$STATE_DIR/backups/$(date +%Y%m%d-%H%M%S)"
|
|
580
|
+
[ -f "$MANIFEST" ] && cp "$MANIFEST" "$PRIOR_MANIFEST"
|
|
452
581
|
: > "$MANIFEST"
|
|
453
582
|
fi
|
|
454
583
|
log "Deploying agent-bios from $REPO"
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
466
|
-
deploy_file "$REPO/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md"
|
|
467
|
-
deploy_glob "$REPO/codex/guides" "*.md" "$CODEX_DIR/guides"
|
|
468
|
-
fi
|
|
584
|
+
# Refuse loudly before doing any work: an unreadable declaration must not degrade
|
|
585
|
+
# into withholding nothing, which is the fail-open shape this rule exists to avoid.
|
|
586
|
+
WITHHELD_GUIDES="$(author_only_guides)" || exit 1
|
|
587
|
+
assemble_corpus || exit 1
|
|
588
|
+
# assemble.py withholds author-only guides and prunes the destinations it writes.
|
|
589
|
+
# $CLAUDE_DIR/guides is not one of them — it is where the pre-unification full
|
|
590
|
+
# install put guides, so a machine that installed then would keep its copy for good.
|
|
591
|
+
prune_withheld "$CLAUDE_DIR/guides"
|
|
469
592
|
migrate_learnings # Phase 4: clear personal copies now absorbed by the corpus
|
|
470
593
|
deploy_glob "$REPO/codex/agents" "*.toml" "$CODEX_DIR/agents"
|
|
471
594
|
codex_config_additions merge || exit 1
|
|
472
|
-
deploy_file "$REPO/
|
|
473
|
-
deploy_file "$REPO/
|
|
595
|
+
deploy_file "$REPO/wrappers/codex-run.sh" "$CODEX_DIR/bin/codex-run" "+x"
|
|
596
|
+
deploy_file "$REPO/wrappers/codex-helm.sh" "$CODEX_DIR/bin/codex-helm" "+x"
|
|
597
|
+
# The claude-side adapter lands under CLAUDE_DIR for the same reason its codex twin
|
|
598
|
+
# lands under CODEX_DIR: each is that host's, and a single shared bin would make the
|
|
599
|
+
# two families' review routes indistinguishable from their install paths.
|
|
600
|
+
deploy_file "$REPO/wrappers/claude-run.sh" "$CLAUDE_DIR/bin/claude-run" "+x"
|
|
474
601
|
migrate_user_presets || exit 1 # must precede the deploy below, which overwrites profiles.toml
|
|
475
|
-
deploy_file "$REPO/
|
|
476
|
-
deploy_file "$REPO/
|
|
477
|
-
deploy_file "$REPO/
|
|
602
|
+
deploy_file "$REPO/launch/agent-launch.toml" "$LAUNCH_DIR/profiles.toml"
|
|
603
|
+
deploy_file "$REPO/launch/agent-launch.zsh" "$LAUNCH_DIR/shell.zsh"
|
|
604
|
+
deploy_file "$REPO/launch/agent-launch.py" "$BIN_DIR/agent-launch" "+x"
|
|
478
605
|
log ""
|
|
479
606
|
log "Optional dependencies (missing ones only degrade the routes that need them)..."
|
|
480
607
|
handle_capabilities "$WITH" || exit 1
|
|
@@ -482,11 +609,11 @@ cmd_install() {
|
|
|
482
609
|
if [ "$DRY_RUN" = 1 ]; then
|
|
483
610
|
info "[dry-run] provision managed Textual venv"
|
|
484
611
|
else
|
|
485
|
-
bash "$REPO/
|
|
612
|
+
bash "$REPO/launch/provision-venv.sh" && info "managed venv OK" \
|
|
486
613
|
|| log "warning: venv provisioning failed (numbered-prompt fallback applies)"
|
|
487
614
|
fi
|
|
488
615
|
add_zsh_hook
|
|
489
|
-
if python3 "$REPO/
|
|
616
|
+
if python3 "$REPO/compose/corpus-state.py" project --repo "$REPO" >/dev/null 2>&1; then
|
|
490
617
|
info "corpus-status projected"
|
|
491
618
|
else
|
|
492
619
|
log "note: corpus-status projection unavailable (versions.json/ledger missing?)"
|
|
@@ -517,6 +644,7 @@ PY
|
|
|
517
644
|
# An untouched backup dir means nothing was replaced; that healthy state
|
|
518
645
|
# must not become a nonzero exit under set -e.
|
|
519
646
|
{ [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ] && log "Replaced files were backed up under $BACKUP_DIR"; } || true
|
|
647
|
+
prune_backups
|
|
520
648
|
else
|
|
521
649
|
log "VERIFY FAILED after install — see messages above"
|
|
522
650
|
exit 1
|
|
@@ -527,37 +655,60 @@ verify_match() { if cmp -s "$1" "$2"; then info "match $2"; else log "MISMA
|
|
|
527
655
|
verify_present() { if [ -f "$1" ]; then return 0; else log "missing $1"; return 1; fi; }
|
|
528
656
|
|
|
529
657
|
cmd_verify() {
|
|
530
|
-
local fail=0 gp
|
|
658
|
+
local fail=0 gp gb
|
|
531
659
|
if [ ! -d "$REPO/.git" ] && [ "$(drift_state)" = "drift" ]; then
|
|
532
660
|
log "deploy drift: deployed $(deployed_version), package $(source_version) — run: agent-bios install"
|
|
533
661
|
fail=1
|
|
534
662
|
fi
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
663
|
+
# Not mode-specific: an author-only guide must be absent from EVERY directory any
|
|
664
|
+
# install writes, including the one the pre-unification full install owned.
|
|
665
|
+
local verify_withheld
|
|
666
|
+
verify_withheld="$(author_only_guides)" || return 1
|
|
667
|
+
for gb in $verify_withheld; do
|
|
668
|
+
for gp in "$CLAUDE_DIR/guides/$gb" "$CLAUDE_DIR/central/guides/$gb" "$CODEX_DIR/guides/$gb"; do
|
|
669
|
+
if [ -f "$gp" ]; then
|
|
670
|
+
log "author-only guide is still installed: $gp"; fail=1
|
|
671
|
+
fi
|
|
672
|
+
done
|
|
673
|
+
done
|
|
674
|
+
[ -n "$verify_withheld" ] && info "author-only guides withheld: $(printf '%s' "$verify_withheld" | tr '\n' ' ')"
|
|
675
|
+
# Corpus surfaces are selection-derived, not repo-identical, so verify reads the assembled
|
|
676
|
+
# shape rather than byte-comparing against the repo. The entry file is user-owned — READ-check
|
|
677
|
+
# the import line, never rewrite it.
|
|
678
|
+
python3 "$REPO/compose/check-domains.py" >/dev/null 2>&1 && info "domains gate OK" || { log "domains gate FAILED"; fail=1; }
|
|
679
|
+
verify_present "$CLAUDE_DIR/central/bundle.md" || fail=1
|
|
680
|
+
if grep -qF '@central/bundle.md' "$CLAUDE_DIR/CLAUDE.md" 2>/dev/null; then
|
|
681
|
+
info "entry import line present"
|
|
682
|
+
else
|
|
683
|
+
log "entry $CLAUDE_DIR/CLAUDE.md lacks '@central/bundle.md' — central corpus is NOT loading"; fail=1
|
|
684
|
+
fi
|
|
685
|
+
if grep -qF 'agent-bios:central:start' "$CODEX_DIR/AGENTS.md" 2>/dev/null; then
|
|
686
|
+
info "codex central region present"
|
|
687
|
+
else
|
|
688
|
+
log "codex AGENTS.md central region missing"; fail=1
|
|
689
|
+
fi
|
|
690
|
+
# Every capability must probe as a real command. `${backend}` reaching this table means
|
|
691
|
+
# the sentinel was not resolved, and the probe below would then report a capability the
|
|
692
|
+
# launcher can run perfectly well as unavailable.
|
|
693
|
+
if capability_table | awk -F'\t' '$2 == "" || $2 == "${backend}" { print; found=1 } END { exit !found }' >/dev/null 2>&1; then
|
|
694
|
+
log "capability table has an unresolved or empty command — the installer would misreport it"; fail=1
|
|
550
695
|
else
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
696
|
+
info "capability commands resolve"
|
|
697
|
+
fi
|
|
698
|
+
# The help's --with list must BE the capability table, not a copy of it: the copy went
|
|
699
|
+
# stale on the first rename and advertised a name that selects a different capability
|
|
700
|
+
# than the one carrying the install line.
|
|
701
|
+
local advertised table
|
|
702
|
+
advertised=$(usage 2>/dev/null | sed -n 's/^ *--with names: //p')
|
|
703
|
+
table=$(capability_table 2>/dev/null | cut -f1 | tr '\n' ' ')
|
|
704
|
+
if [ "$advertised" = "${table% }" ]; then
|
|
705
|
+
info "--with help matches the capability table"
|
|
706
|
+
else
|
|
707
|
+
log "--with help advertises '$advertised' but the capabilities are '${table% }'"; fail=1
|
|
708
|
+
fi
|
|
709
|
+
verify_match "$REPO/launch/agent-launch.py" "$BIN_DIR/agent-launch" || fail=1
|
|
710
|
+
verify_match "$REPO/launch/agent-launch.toml" "$LAUNCH_DIR/profiles.toml" || fail=1
|
|
711
|
+
verify_match "$REPO/launch/agent-launch.zsh" "$LAUNCH_DIR/shell.zsh" || fail=1
|
|
561
712
|
python3 - "$CODEX_DIR/agents" <<'PY' && info "agent TOMLs OK" || fail=1
|
|
562
713
|
import sys, pathlib, tomllib
|
|
563
714
|
root = pathlib.Path(sys.argv[1])
|
|
@@ -575,6 +726,24 @@ PY
|
|
|
575
726
|
log "codex-helm dry-run FAILED"; fail=1
|
|
576
727
|
fi
|
|
577
728
|
fi
|
|
729
|
+
# Existence first, because the guard below is written as `if executable` and a MISSING
|
|
730
|
+
# adapter would satisfy it by never running — a deploy target whose only check skips
|
|
731
|
+
# itself when the deploy failed is not checked at all.
|
|
732
|
+
verify_present "$CLAUDE_DIR/bin/claude-run" || fail=1
|
|
733
|
+
# Then WHICH version landed, because this file is on the panel's dispatch path now and
|
|
734
|
+
# an older copy would dispatch reviews that quietly emit nothing. The earlier check
|
|
735
|
+
# here asserted that an unpinned dispatch is refused; that guard was deliberately
|
|
736
|
+
# removed when the adapter went live — refusing turned "the review ran unpinned" into
|
|
737
|
+
# "the review did not run" — so asserting it would now fail against correct behaviour.
|
|
738
|
+
# `--help` is the only probe that reaches no network: an unpinned run warns and then
|
|
739
|
+
# dispatches for real.
|
|
740
|
+
if [ -x "$CLAUDE_DIR/bin/claude-run" ]; then
|
|
741
|
+
if "$CLAUDE_DIR/bin/claude-run" --help 2>/dev/null | grep -q 'REVIEW_RECEIPT_DIR'; then
|
|
742
|
+
info "claude-run is receipt-aware"
|
|
743
|
+
else
|
|
744
|
+
log "claude-run predates the receipt contract — reviews through it emit nothing"; fail=1
|
|
745
|
+
fi
|
|
746
|
+
fi
|
|
578
747
|
local vpy="${AGENT_LAUNCH_VENV:-$HOME/.local/share/agent-launch/venv}/bin/python"
|
|
579
748
|
if [ -x "$vpy" ] && "$vpy" -c 'import textual' 2>/dev/null; then
|
|
580
749
|
info "managed venv (textual) OK"
|
|
@@ -583,25 +752,25 @@ PY
|
|
|
583
752
|
fi
|
|
584
753
|
# A file this installer executes but never ships is invisible from a clone and
|
|
585
754
|
# fatal on npm, so the payload gate runs wherever it exists (maintainer-side).
|
|
586
|
-
if [ -x "$REPO/
|
|
587
|
-
if "$REPO/
|
|
755
|
+
if [ -x "$REPO/gates/check-package.sh" ]; then
|
|
756
|
+
if "$REPO/gates/check-package.sh" >/dev/null 2>&1; then
|
|
588
757
|
info "npm payload OK"
|
|
589
758
|
else
|
|
590
|
-
log "npm payload incomplete; run
|
|
759
|
+
log "npm payload incomplete; run gates/check-package.sh"
|
|
591
760
|
fail=1
|
|
592
761
|
fi
|
|
593
762
|
fi
|
|
594
763
|
# Repo-internal mirror parity is a maintainer gate; only meaningful from a clone.
|
|
595
|
-
if [ -d "$REPO/ko" ] && [ -x "$REPO/
|
|
596
|
-
if "$REPO/
|
|
764
|
+
if [ -d "$REPO/ko" ] && [ -x "$REPO/gates/check-parity.sh" ]; then
|
|
765
|
+
if "$REPO/gates/check-parity.sh" >/dev/null 2>&1; then info "repo mirror parity OK"; else log "repo mirror parity FAILED"; fail=1; fi
|
|
597
766
|
fi
|
|
598
767
|
# Prompting guides name concrete models, so they go stale on a model change
|
|
599
768
|
# rather than degrading quietly; this checks them against the launch config.
|
|
600
|
-
if [ -x "$REPO/
|
|
601
|
-
if "$REPO/
|
|
769
|
+
if [ -x "$REPO/launch/check-prompting-targets.sh" ]; then
|
|
770
|
+
if "$REPO/launch/check-prompting-targets.sh" >/dev/null 2>&1; then
|
|
602
771
|
info "prompting targets OK"
|
|
603
772
|
else
|
|
604
|
-
log "prompting guides do not cover a configured model; run
|
|
773
|
+
log "prompting guides do not cover a configured model; run launch/check-prompting-targets.sh"
|
|
605
774
|
fail=1
|
|
606
775
|
fi
|
|
607
776
|
fi
|
|
@@ -611,38 +780,149 @@ PY
|
|
|
611
780
|
cmd_uninstall() {
|
|
612
781
|
migrate_state
|
|
613
782
|
codex_config_additions remove || log "warning: could not remove codex config additions"
|
|
783
|
+
# The assembler's two spans in files we do not own — settings registrations and the AGENTS.md
|
|
784
|
+
# central region — are merged in and were never removed here, so uninstall used to leave hooks
|
|
785
|
+
# invoking deleted files and instructions pointing at deleted guides. It reads ownership the
|
|
786
|
+
# same way the merge wrote it, so it cannot reach past what we put there.
|
|
787
|
+
if [ -f "$REPO/compose/assemble.py" ]; then
|
|
788
|
+
python3 "$REPO/compose/assemble.py" --remove-owned --claude-dir "$CLAUDE_DIR" \
|
|
789
|
+
--codex-dir "$CODEX_DIR" --state-dir "$STATE_DIR" ${DRY_RUN:+} \
|
|
790
|
+
$([ "$DRY_RUN" = 1 ] && echo --dry-run) \
|
|
791
|
+
|| log "warning: could not remove assembler-owned regions"
|
|
792
|
+
fi
|
|
614
793
|
if [ -f "$MANIFEST" ]; then
|
|
794
|
+
# Back up before deleting, the way install backs up before overwriting. Full mode deploys the
|
|
795
|
+
# entry CLAUDE.md/AGENTS.md as ordinary targets, so they are manifested and removed here —
|
|
796
|
+
# correct, since in that mode the entry file IS the corpus. What was wrong is that anything a
|
|
797
|
+
# user added to it disappeared with no copy, while the same file overwritten during install
|
|
798
|
+
# would have been backed up. Removal is symmetric with deployment; recoverability now is too.
|
|
799
|
+
[ "$DRY_RUN" = 1 ] || { mkdir -p "$STATE_DIR"; BACKUP_DIR="$STATE_DIR/backups/uninstall-$(date +%Y%m%d-%H%M%S)"; }
|
|
615
800
|
local f
|
|
616
801
|
while IFS= read -r f; do
|
|
617
802
|
[ -n "$f" ] || continue
|
|
803
|
+
if [ -f "$f" ] && [ -n "$BACKUP_DIR" ] && [ "$DRY_RUN" != 1 ]; then
|
|
804
|
+
mkdir -p "$(dirname "$BACKUP_DIR$f")" && cp "$f" "$BACKUP_DIR$f"
|
|
805
|
+
fi
|
|
618
806
|
[ -f "$f" ] && { run rm -f "$f"; [ "$DRY_RUN" = 1 ] || info "removed $f"; }
|
|
619
807
|
done < "$MANIFEST"
|
|
808
|
+
{ [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ] && info "staged for the archive: $BACKUP_DIR"; } || true
|
|
620
809
|
[ "$DRY_RUN" = 1 ] || rm -f "$MANIFEST"
|
|
621
810
|
else
|
|
622
811
|
log "no manifest at $MANIFEST; removing known deploy targets"
|
|
812
|
+
# The two entry files are deliberately NOT in this list. `$CLAUDE_DIR/CLAUDE.md` is seeded
|
|
813
|
+
# once and the user's thereafter, and `$CODEX_DIR/AGENTS.md` is ours only between the central
|
|
814
|
+
# markers, which --remove-owned above already took. This branch runs precisely when state is
|
|
815
|
+
# missing or agent-bios was never installed here, so nothing says we wrote either file —
|
|
816
|
+
# and deleting them whole destroyed personal instructions with no copy at all, since
|
|
817
|
+
# BACKUP_DIR is set only on the manifest path above.
|
|
623
818
|
local p
|
|
624
|
-
for p in "$
|
|
625
|
-
"$
|
|
819
|
+
for p in "$CODEX_DIR/bin/codex-run" "$CODEX_DIR/bin/codex-helm" \
|
|
820
|
+
"$CLAUDE_DIR/bin/claude-run" \
|
|
626
821
|
"$LAUNCH_DIR/profiles.toml" "$LAUNCH_DIR/shell.zsh" "$BIN_DIR/agent-launch"; do
|
|
627
822
|
[ -f "$p" ] && { run rm -f "$p"; [ "$DRY_RUN" = 1 ] || info "removed $p"; }
|
|
628
823
|
done
|
|
629
824
|
fi
|
|
630
825
|
local d
|
|
631
|
-
|
|
826
|
+
# `central/` and its subdirectories are entirely ours — the assembler creates them and the
|
|
827
|
+
# manifest covers every file inside — so they belong in this sweep. They were missing from it,
|
|
828
|
+
# which left three empty directories in the user's config dir after a "leave no trace" removal.
|
|
829
|
+
# rmdir, never rm -rf: a directory that is not empty is one we did not fully account for, and
|
|
830
|
+
# the right answer then is to leave it and be visibly incomplete.
|
|
831
|
+
for d in "$CLAUDE_DIR/central/guides" "$CLAUDE_DIR/central/hooks" "$CLAUDE_DIR/central/agents" \
|
|
832
|
+
"$CLAUDE_DIR/central" \
|
|
833
|
+
"$CLAUDE_DIR/guides" "$CLAUDE_DIR/agents" "$CLAUDE_DIR/bin" \
|
|
834
|
+
"$CODEX_DIR/guides" "$CODEX_DIR/agents" "$CODEX_DIR/bin" "$LAUNCH_DIR"; do
|
|
632
835
|
[ -d "$d" ] && rmdir "$d" 2>/dev/null && info "removed empty $d" || true
|
|
633
836
|
done
|
|
634
837
|
remove_zsh_hook
|
|
838
|
+
archive_and_purge
|
|
635
839
|
log ""
|
|
636
|
-
log "Uninstalled deployed files and the
|
|
637
|
-
|
|
840
|
+
log "Uninstalled: deployed files, the zsh hook, state, backups, cache, and the managed venv."
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
# Uninstall is a SECURITY operation — nothing of ours may survive it on the machine. That
|
|
844
|
+
# conflicts with never destroying what a user added, because full mode writes the corpus into an
|
|
845
|
+
# entry file they then edit, so removal takes their work with it. One artifact settles both:
|
|
846
|
+
# everything removed leaves as a single archive that can be handed off or deleted in one act,
|
|
847
|
+
# and every managed location is then purged. The archive lands in $HOME, outside every path we
|
|
848
|
+
# manage, because a copy inside a directory we are about to delete is not a copy.
|
|
849
|
+
#
|
|
850
|
+
# Order is the safety property: archive first, verify the archive exists, purge only then. A
|
|
851
|
+
# failed archive leaves the machine untouched rather than clean and empty-handed.
|
|
852
|
+
archive_and_purge() {
|
|
853
|
+
local ts stage rel out list staged skipped
|
|
854
|
+
ts="$(date +%Y%m%d-%H%M%S)"
|
|
855
|
+
out="${AGENT_BIOS_UNINSTALL_ARCHIVE:-$HOME}/agent-bios-uninstall-$ts.tar.gz"
|
|
856
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
857
|
+
info "[dry-run] archive removed content to $out, then purge $STATE_DIR, the cache and the venv"
|
|
858
|
+
return 0
|
|
859
|
+
fi
|
|
860
|
+
stage="$(mktemp -d)" || { log "warning: no temp dir; leaving state in place"; return 0; }
|
|
861
|
+
list="$(mktemp)" || { rm -rf "$stage"; log "warning: no temp file; leaving state in place"; return 0; }
|
|
862
|
+
[ -d "$STATE_DIR" ] && cp -R "$STATE_DIR" "$stage/state" 2>/dev/null
|
|
863
|
+
# WHICH loose copies are ours is compose/prune-backups.py's question — it deletes them too, on
|
|
864
|
+
# the retention path, and a rule written in both places drifts on one side. `*.bak-*` was the
|
|
865
|
+
# match here, which also claims a `notes.bak-old` the user saved by hand; these sit in
|
|
866
|
+
# directories we share with them and with other tools, so the name must carry the ownership.
|
|
867
|
+
if [ -f "$REPO/compose/prune-backups.py" ]; then
|
|
868
|
+
python3 "$REPO/compose/prune-backups.py" --list-owned \
|
|
869
|
+
--claude-dir "$CLAUDE_DIR" --codex-dir "$CODEX_DIR" >"$list" 2>/dev/null || : >"$list"
|
|
870
|
+
else
|
|
871
|
+
log "note: compose/prune-backups.py is absent, so loose backup copies are left where they are"
|
|
872
|
+
fi
|
|
873
|
+
# Staged UNDER their absolute path, the way deploy_file backs up to "$BACKUP_DIR$dst".
|
|
874
|
+
# Flattening them into one directory let two same-named copies from the two host trees
|
|
875
|
+
# collide, and the loser was then deleted with nothing in the archive to restore it from.
|
|
876
|
+
# Only what verifiably reached the stage may be deleted later. Every failure mode here is
|
|
877
|
+
# silent — a full temp filesystem, an unreadable source, a directory that cannot be created —
|
|
878
|
+
# and purging from the ORIGINAL list removed files the archive does not contain. Size equality
|
|
879
|
+
# rather than cp's exit status alone, because the question is whether the archive can restore
|
|
880
|
+
# it, not whether the copy was attempted.
|
|
881
|
+
staged="$(mktemp)" || { rm -f "$list"; rm -rf "$stage"
|
|
882
|
+
log "warning: no temp file; leaving state in place"; return 0; }
|
|
883
|
+
skipped=0
|
|
884
|
+
while IFS= read -r -d '' rel; do
|
|
885
|
+
if mkdir -p "$(dirname "$stage/loose$rel")" 2>/dev/null \
|
|
886
|
+
&& cp "$rel" "$stage/loose$rel" 2>/dev/null \
|
|
887
|
+
&& [ "$(wc -c <"$rel" 2>/dev/null)" = "$(wc -c <"$stage/loose$rel" 2>/dev/null)" ]; then
|
|
888
|
+
printf '%s\0' "$rel" >> "$staged"
|
|
889
|
+
else
|
|
890
|
+
skipped=$((skipped + 1))
|
|
891
|
+
rm -f "$stage/loose$rel" 2>/dev/null # a partial copy must not look archived
|
|
892
|
+
log "warning: could not stage $rel — leaving it in place"
|
|
893
|
+
fi
|
|
894
|
+
done <"$list"
|
|
895
|
+
if tar czf "$out" -C "$stage" . 2>/dev/null && [ -s "$out" ]; then
|
|
896
|
+
# Only here, with the archive written and non-empty, is removing the originals recoverable.
|
|
897
|
+
# Deleting them before the tar meant a failed archive took the backups with it — while the
|
|
898
|
+
# warning below told the operator they had been left in place.
|
|
899
|
+
while IFS= read -r -d '' rel; do rm -f "$rel" 2>/dev/null; done <"$staged"
|
|
900
|
+
rm -f "$list" "$staged"
|
|
901
|
+
rm -rf "$stage"
|
|
902
|
+
rm -rf "$STATE_DIR" "$HOME/.cache/agent-launch" \
|
|
903
|
+
"${AGENT_LAUNCH_VENV:-$HOME/.local/share/agent-launch}"
|
|
904
|
+
log ""
|
|
905
|
+
log "Everything removed is in ONE archive: $out"
|
|
906
|
+
if [ "$skipped" -gt 0 ]; then
|
|
907
|
+
log "Move it somewhere central or delete it. $skipped backup copy(ies) could not be staged,"
|
|
908
|
+
log "so they were LEFT ON DISK rather than deleted with nothing to restore them from."
|
|
909
|
+
else
|
|
910
|
+
log "Move it somewhere central or delete it — nothing of ours is left on this machine."
|
|
911
|
+
fi
|
|
912
|
+
else
|
|
913
|
+
rm -f "$list" "$staged"
|
|
914
|
+
rm -rf "$stage"
|
|
915
|
+
log "warning: could not write $out — state, backups and the venv were left in place rather"
|
|
916
|
+
log " than deleted with no copy. Re-run once the archive path is writable."
|
|
917
|
+
fi
|
|
638
918
|
}
|
|
639
919
|
|
|
640
920
|
cmd_onboard() {
|
|
641
921
|
log "agent-bios onboarding — pick your domain packages (core + infra always install)"
|
|
642
922
|
local names=() line i=1 choice sel="" n picks
|
|
643
923
|
while IFS= read -r line; do names+=("$line"); done \
|
|
644
|
-
< <(python3 -c "import json;print('\n'.join(sorted(json.load(open('$REPO/
|
|
645
|
-
[ "${#names[@]}" -ge 1 ] || { log "no domains found in
|
|
924
|
+
< <(python3 -c "import json;print('\n'.join(sorted(json.load(open('$REPO/compose/domains.json'))['domains'])))")
|
|
925
|
+
[ "${#names[@]}" -ge 1 ] || { log "no domains found in compose/domains.json"; exit 1; }
|
|
646
926
|
if [ "$DOMAINS_SET" = 1 ]; then
|
|
647
927
|
# Non-interactive path, per this installer's input contract (stdin is
|
|
648
928
|
# detached at the top of the script): selection arrives as domain names.
|
|
@@ -674,10 +954,13 @@ cmd_onboard() {
|
|
|
674
954
|
log ""
|
|
675
955
|
log "Activation canary (proves the bundle loads in a live session)..."
|
|
676
956
|
if [ "$DRY_RUN" = 1 ]; then info "[dry-run] skip canary probe"; return; fi
|
|
677
|
-
bash "$REPO/
|
|
957
|
+
bash "$REPO/compose/canary.sh" || {
|
|
678
958
|
log "ONBOARDING INCOMPLETE: the bundle is installed but not loading — fix the cause above and re-run: agent-bios verify"
|
|
679
959
|
exit 1
|
|
680
960
|
}
|
|
961
|
+
# The prune is authorized by the canary's proof, and cmd_install ran before the canary existed
|
|
962
|
+
# for this bundle rev — so it kept everything. Now that loading is proven, run it for real.
|
|
963
|
+
migrate_learnings
|
|
681
964
|
}
|
|
682
965
|
|
|
683
966
|
# ---- deploy-chain drift ---------------------------------------------------
|
|
@@ -750,7 +1033,7 @@ usage() {
|
|
|
750
1033
|
agent-bios — deploy the Claude/Codex instruction SSOT into $HOME (by copy).
|
|
751
1034
|
|
|
752
1035
|
agent-bios install deploy into this environment (backs up + verifies)
|
|
753
|
-
agent-bios onboard interactive domain selection +
|
|
1036
|
+
agent-bios onboard interactive domain selection + install + activation canary
|
|
754
1037
|
agent-bios verify check the deployed state matches the source
|
|
755
1038
|
agent-bios learn submit a session learning (reads the JSON record on
|
|
756
1039
|
stdin; this is what the learn! flow calls, and it
|
|
@@ -761,20 +1044,34 @@ agent-bios — deploy the Claude/Codex instruction SSOT into $HOME (by copy).
|
|
|
761
1044
|
agent-bios help
|
|
762
1045
|
|
|
763
1046
|
Flags: --dry-run print actions without changing anything
|
|
764
|
-
--domains a,b
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
today's full-corpus deploy.
|
|
1047
|
+
--domains a,b assemble ONLY the named domain packages (plus core+infra);
|
|
1048
|
+
with onboard, 'none' means core+infra only. The selection
|
|
1049
|
+
persists in the state dir and later installs/updates reuse
|
|
1050
|
+
it. Default (no flag, no saved selection) selects every
|
|
1051
|
+
domain — one install shape, "full" is just everything
|
|
1052
|
+
selected.
|
|
771
1053
|
--with a,b also install the named optional dependencies (install only).
|
|
772
1054
|
Without it, install offers each missing one when the terminal
|
|
773
1055
|
is interactive, and otherwise just prints its install line.
|
|
774
|
-
|
|
775
|
-
|
|
1056
|
+
Missing ones are not fatal — they only degrade the review
|
|
1057
|
+
routes that need them.
|
|
776
1058
|
Env: CLAUDE_CONFIG_DIR, CODEX_HOME, AGENT_LAUNCH_VENV, ZDOTDIR
|
|
777
1059
|
EOF
|
|
1060
|
+
# DERIVED, not typed: the hardcoded pair went stale the moment a capability was renamed,
|
|
1061
|
+
# and the name it still advertised selected a different capability than the one that
|
|
1062
|
+
# carries the install line.
|
|
1063
|
+
local known; known=$(capability_table 2>/dev/null | cut -f1 | tr '\n' ' ')
|
|
1064
|
+
[ -n "$known" ] && printf ' --with names: %s\n' "${known% }"
|
|
1065
|
+
|
|
1066
|
+
# Recovery exists in both modes at different granularity, and naming the wrong one is worse
|
|
1067
|
+
# than naming none: a clone can roll the corpus back to a registered mining window, while an
|
|
1068
|
+
# npm install has no git history to read and rolls the whole package back by version instead.
|
|
1069
|
+
# Derived from which install this is, for the same reason --with names is.
|
|
1070
|
+
if [ -d "$REPO/.git" ]; then
|
|
1071
|
+
printf '\nRecover: python3 %s/compose/corpus-state.py list, then rollback --version V\n' "$REPO"
|
|
1072
|
+
else
|
|
1073
|
+
printf '\nRecover: npm install -g agent-bios@<older-version> && agent-bios install\n'
|
|
1074
|
+
fi
|
|
778
1075
|
}
|
|
779
1076
|
|
|
780
1077
|
# ---- dispatch ------------------------------------------------------------
|
|
@@ -784,10 +1081,10 @@ if [ $# -gt 0 ]; then shift; fi
|
|
|
784
1081
|
# `learn` forwards its arguments and stdin straight to the collector, so it must
|
|
785
1082
|
# bypass the flag parser below (which rejects anything it does not know). This
|
|
786
1083
|
# subcommand is the only PATH-reachable entry to capture: the corpus guide used
|
|
787
|
-
# to invoke
|
|
1084
|
+
# to invoke learn/collect-learning.py relative to the cwd, which works from a
|
|
788
1085
|
# clone and silently fails for every other install.
|
|
789
1086
|
if [ "$CMD" = "learn" ]; then
|
|
790
|
-
collector="$REPO/
|
|
1087
|
+
collector="$REPO/learn/collect-learning.py"
|
|
791
1088
|
[ -f "$collector" ] || { log "learn: collector missing at $collector"; exit 1; }
|
|
792
1089
|
exec python3 "$collector" "$@" <&3
|
|
793
1090
|
fi
|