agent-bios 0.9.7 → 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 +209 -25
- 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/compose/register-hooks.py +44 -0
- package/{scripts/install.sh → install.sh} +403 -94
- 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 -23
- 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,26 +117,129 @@ deploy_glob() {
|
|
|
105
117
|
done
|
|
106
118
|
}
|
|
107
119
|
|
|
108
|
-
#
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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"
|
|
129
|
+
}
|
|
115
130
|
|
|
116
|
-
|
|
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
|
+
}
|
|
175
|
+
|
|
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() {
|
|
117
211
|
local args=(--claude-dir "$CLAUDE_DIR" --codex-dir "$CODEX_DIR" --state-dir "$STATE_DIR") rc=0
|
|
118
|
-
[ "${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
|
|
119
219
|
[ "$DRY_RUN" = 1 ] && args+=(--dry-run)
|
|
120
|
-
|
|
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=$?
|
|
121
225
|
if [ "$rc" = 2 ]; then
|
|
122
|
-
log "
|
|
226
|
+
log "entry file needs user action (import line missing); central content will not load until it is added"
|
|
123
227
|
elif [ "$rc" != 0 ]; then
|
|
124
228
|
return 1
|
|
125
229
|
fi
|
|
126
230
|
if [ "$DRY_RUN" != 1 ]; then
|
|
127
|
-
|
|
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
|
|
128
243
|
fi
|
|
129
244
|
}
|
|
130
245
|
|
|
@@ -300,13 +415,25 @@ PY
|
|
|
300
415
|
|
|
301
416
|
# ---- optional dependencies -----------------------------------------------
|
|
302
417
|
# Capabilities are optional: a missing one only degrades the review routes that
|
|
303
|
-
# need it.
|
|
418
|
+
# need it. launch/agent-launch.toml is the single source for both the command
|
|
304
419
|
# that gates a route and the install line offered here.
|
|
305
420
|
capability_table() {
|
|
306
|
-
python3 - "$REPO/
|
|
421
|
+
python3 - "$REPO/launch/agent-launch.toml" <<'PY'
|
|
307
422
|
import sys, tomllib
|
|
308
|
-
|
|
309
|
-
|
|
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", ""))))
|
|
310
437
|
PY
|
|
311
438
|
}
|
|
312
439
|
|
|
@@ -330,6 +457,16 @@ handle_capabilities() {
|
|
|
330
457
|
[ -n "$name" ] || continue
|
|
331
458
|
if command -v "$command" >/dev/null 2>&1; then
|
|
332
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
|
|
333
470
|
continue
|
|
334
471
|
fi
|
|
335
472
|
if printf '%s\n' "${requested//,/ }" | tr ' ' '\n' | grep -qx "$name"; then
|
|
@@ -353,7 +490,7 @@ EOF
|
|
|
353
490
|
# Presets the launcher saved into the deployed profiles.toml (pre-split, or by hand)
|
|
354
491
|
# would be lost to the cp below; move them into the user-owned presets file first.
|
|
355
492
|
migrate_user_presets() {
|
|
356
|
-
local src="$REPO/
|
|
493
|
+
local src="$REPO/launch/agent-launch.toml"
|
|
357
494
|
local dst="$LAUNCH_DIR/profiles.toml"
|
|
358
495
|
local user="$LAUNCH_DIR/$USER_PRESETS_NAME"
|
|
359
496
|
[ -f "$dst" ] || return 0
|
|
@@ -420,10 +557,10 @@ remove_zsh_hook() {
|
|
|
420
557
|
# corpus AND are in this user's assembled bundle. Best-effort: a prune failure
|
|
421
558
|
# (or an absent manifest/script) never fails the install. Runs per host.
|
|
422
559
|
migrate_learnings() {
|
|
423
|
-
local script="$REPO/
|
|
424
|
-
{ [ -f "$script" ] && [ -f "$REPO/
|
|
560
|
+
local script="$REPO/learn/migrate-learnings.py"
|
|
561
|
+
{ [ -f "$script" ] && [ -f "$REPO/learn/promotions.json" ]; } || return 0
|
|
425
562
|
local -a sel dry
|
|
426
|
-
|
|
563
|
+
sel=(--selection-file "$STATE_DIR/selection.json")
|
|
427
564
|
[ "$DRY_RUN" = 1 ] && dry=(--dry-run) || dry=()
|
|
428
565
|
# ${dry[@]+...}: expanding an empty array as "${dry[@]}" is an unbound-variable
|
|
429
566
|
# error under `set -u` on bash 3.2 (macOS default) and would abort the install.
|
|
@@ -440,29 +577,31 @@ cmd_install() {
|
|
|
440
577
|
if [ "$DRY_RUN" != 1 ]; then
|
|
441
578
|
mkdir -p "$STATE_DIR"
|
|
442
579
|
BACKUP_DIR="$STATE_DIR/backups/$(date +%Y%m%d-%H%M%S)"
|
|
580
|
+
[ -f "$MANIFEST" ] && cp "$MANIFEST" "$PRIOR_MANIFEST"
|
|
443
581
|
: > "$MANIFEST"
|
|
444
582
|
fi
|
|
445
583
|
log "Deploying agent-bios from $REPO"
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
deploy_file "$REPO/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md"
|
|
455
|
-
deploy_glob "$REPO/codex/guides" "*.md" "$CODEX_DIR/guides"
|
|
456
|
-
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"
|
|
457
592
|
migrate_learnings # Phase 4: clear personal copies now absorbed by the corpus
|
|
458
593
|
deploy_glob "$REPO/codex/agents" "*.toml" "$CODEX_DIR/agents"
|
|
459
594
|
codex_config_additions merge || exit 1
|
|
460
|
-
deploy_file "$REPO/
|
|
461
|
-
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"
|
|
462
601
|
migrate_user_presets || exit 1 # must precede the deploy below, which overwrites profiles.toml
|
|
463
|
-
deploy_file "$REPO/
|
|
464
|
-
deploy_file "$REPO/
|
|
465
|
-
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"
|
|
466
605
|
log ""
|
|
467
606
|
log "Optional dependencies (missing ones only degrade the routes that need them)..."
|
|
468
607
|
handle_capabilities "$WITH" || exit 1
|
|
@@ -470,11 +609,11 @@ cmd_install() {
|
|
|
470
609
|
if [ "$DRY_RUN" = 1 ]; then
|
|
471
610
|
info "[dry-run] provision managed Textual venv"
|
|
472
611
|
else
|
|
473
|
-
bash "$REPO/
|
|
612
|
+
bash "$REPO/launch/provision-venv.sh" && info "managed venv OK" \
|
|
474
613
|
|| log "warning: venv provisioning failed (numbered-prompt fallback applies)"
|
|
475
614
|
fi
|
|
476
615
|
add_zsh_hook
|
|
477
|
-
if python3 "$REPO/
|
|
616
|
+
if python3 "$REPO/compose/corpus-state.py" project --repo "$REPO" >/dev/null 2>&1; then
|
|
478
617
|
info "corpus-status projected"
|
|
479
618
|
else
|
|
480
619
|
log "note: corpus-status projection unavailable (versions.json/ledger missing?)"
|
|
@@ -505,6 +644,7 @@ PY
|
|
|
505
644
|
# An untouched backup dir means nothing was replaced; that healthy state
|
|
506
645
|
# must not become a nonzero exit under set -e.
|
|
507
646
|
{ [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ] && log "Replaced files were backed up under $BACKUP_DIR"; } || true
|
|
647
|
+
prune_backups
|
|
508
648
|
else
|
|
509
649
|
log "VERIFY FAILED after install — see messages above"
|
|
510
650
|
exit 1
|
|
@@ -515,37 +655,60 @@ verify_match() { if cmp -s "$1" "$2"; then info "match $2"; else log "MISMA
|
|
|
515
655
|
verify_present() { if [ -f "$1" ]; then return 0; else log "missing $1"; return 1; fi; }
|
|
516
656
|
|
|
517
657
|
cmd_verify() {
|
|
518
|
-
local fail=0 gp
|
|
658
|
+
local fail=0 gp gb
|
|
519
659
|
if [ ! -d "$REPO/.git" ] && [ "$(drift_state)" = "drift" ]; then
|
|
520
660
|
log "deploy drift: deployed $(deployed_version), package $(source_version) — run: agent-bios install"
|
|
521
661
|
fail=1
|
|
522
662
|
fi
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
|
695
|
+
else
|
|
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"
|
|
538
706
|
else
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
for gp in "$REPO"/codex/guides/*.md; do verify_present "$CODEX_DIR/guides/$(basename "$gp")" || fail=1; done
|
|
545
|
-
fi
|
|
546
|
-
verify_match "$REPO/scripts/agent-launch.py" "$BIN_DIR/agent-launch" || fail=1
|
|
547
|
-
verify_match "$REPO/config/agent-launch.toml" "$LAUNCH_DIR/profiles.toml" || fail=1
|
|
548
|
-
verify_match "$REPO/shell/agent-launch.zsh" "$LAUNCH_DIR/shell.zsh" || fail=1
|
|
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
|
|
549
712
|
python3 - "$CODEX_DIR/agents" <<'PY' && info "agent TOMLs OK" || fail=1
|
|
550
713
|
import sys, pathlib, tomllib
|
|
551
714
|
root = pathlib.Path(sys.argv[1])
|
|
@@ -563,6 +726,24 @@ PY
|
|
|
563
726
|
log "codex-helm dry-run FAILED"; fail=1
|
|
564
727
|
fi
|
|
565
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
|
|
566
747
|
local vpy="${AGENT_LAUNCH_VENV:-$HOME/.local/share/agent-launch/venv}/bin/python"
|
|
567
748
|
if [ -x "$vpy" ] && "$vpy" -c 'import textual' 2>/dev/null; then
|
|
568
749
|
info "managed venv (textual) OK"
|
|
@@ -571,25 +752,25 @@ PY
|
|
|
571
752
|
fi
|
|
572
753
|
# A file this installer executes but never ships is invisible from a clone and
|
|
573
754
|
# fatal on npm, so the payload gate runs wherever it exists (maintainer-side).
|
|
574
|
-
if [ -x "$REPO/
|
|
575
|
-
if "$REPO/
|
|
755
|
+
if [ -x "$REPO/gates/check-package.sh" ]; then
|
|
756
|
+
if "$REPO/gates/check-package.sh" >/dev/null 2>&1; then
|
|
576
757
|
info "npm payload OK"
|
|
577
758
|
else
|
|
578
|
-
log "npm payload incomplete; run
|
|
759
|
+
log "npm payload incomplete; run gates/check-package.sh"
|
|
579
760
|
fail=1
|
|
580
761
|
fi
|
|
581
762
|
fi
|
|
582
763
|
# Repo-internal mirror parity is a maintainer gate; only meaningful from a clone.
|
|
583
|
-
if [ -d "$REPO/ko" ] && [ -x "$REPO/
|
|
584
|
-
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
|
|
585
766
|
fi
|
|
586
767
|
# Prompting guides name concrete models, so they go stale on a model change
|
|
587
768
|
# rather than degrading quietly; this checks them against the launch config.
|
|
588
|
-
if [ -x "$REPO/
|
|
589
|
-
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
|
|
590
771
|
info "prompting targets OK"
|
|
591
772
|
else
|
|
592
|
-
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"
|
|
593
774
|
fail=1
|
|
594
775
|
fi
|
|
595
776
|
fi
|
|
@@ -599,38 +780,149 @@ PY
|
|
|
599
780
|
cmd_uninstall() {
|
|
600
781
|
migrate_state
|
|
601
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
|
|
602
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)"; }
|
|
603
800
|
local f
|
|
604
801
|
while IFS= read -r f; do
|
|
605
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
|
|
606
806
|
[ -f "$f" ] && { run rm -f "$f"; [ "$DRY_RUN" = 1 ] || info "removed $f"; }
|
|
607
807
|
done < "$MANIFEST"
|
|
808
|
+
{ [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ] && info "staged for the archive: $BACKUP_DIR"; } || true
|
|
608
809
|
[ "$DRY_RUN" = 1 ] || rm -f "$MANIFEST"
|
|
609
810
|
else
|
|
610
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.
|
|
611
818
|
local p
|
|
612
|
-
for p in "$
|
|
613
|
-
"$
|
|
819
|
+
for p in "$CODEX_DIR/bin/codex-run" "$CODEX_DIR/bin/codex-helm" \
|
|
820
|
+
"$CLAUDE_DIR/bin/claude-run" \
|
|
614
821
|
"$LAUNCH_DIR/profiles.toml" "$LAUNCH_DIR/shell.zsh" "$BIN_DIR/agent-launch"; do
|
|
615
822
|
[ -f "$p" ] && { run rm -f "$p"; [ "$DRY_RUN" = 1 ] || info "removed $p"; }
|
|
616
823
|
done
|
|
617
824
|
fi
|
|
618
825
|
local d
|
|
619
|
-
|
|
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
|
|
620
835
|
[ -d "$d" ] && rmdir "$d" 2>/dev/null && info "removed empty $d" || true
|
|
621
836
|
done
|
|
622
837
|
remove_zsh_hook
|
|
838
|
+
archive_and_purge
|
|
623
839
|
log ""
|
|
624
|
-
log "Uninstalled deployed files and the
|
|
625
|
-
|
|
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
|
|
626
918
|
}
|
|
627
919
|
|
|
628
920
|
cmd_onboard() {
|
|
629
921
|
log "agent-bios onboarding — pick your domain packages (core + infra always install)"
|
|
630
922
|
local names=() line i=1 choice sel="" n picks
|
|
631
923
|
while IFS= read -r line; do names+=("$line"); done \
|
|
632
|
-
< <(python3 -c "import json;print('\n'.join(sorted(json.load(open('$REPO/
|
|
633
|
-
[ "${#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; }
|
|
634
926
|
if [ "$DOMAINS_SET" = 1 ]; then
|
|
635
927
|
# Non-interactive path, per this installer's input contract (stdin is
|
|
636
928
|
# detached at the top of the script): selection arrives as domain names.
|
|
@@ -662,10 +954,13 @@ cmd_onboard() {
|
|
|
662
954
|
log ""
|
|
663
955
|
log "Activation canary (proves the bundle loads in a live session)..."
|
|
664
956
|
if [ "$DRY_RUN" = 1 ]; then info "[dry-run] skip canary probe"; return; fi
|
|
665
|
-
bash "$REPO/
|
|
957
|
+
bash "$REPO/compose/canary.sh" || {
|
|
666
958
|
log "ONBOARDING INCOMPLETE: the bundle is installed but not loading — fix the cause above and re-run: agent-bios verify"
|
|
667
959
|
exit 1
|
|
668
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
|
|
669
964
|
}
|
|
670
965
|
|
|
671
966
|
# ---- deploy-chain drift ---------------------------------------------------
|
|
@@ -738,7 +1033,7 @@ usage() {
|
|
|
738
1033
|
agent-bios — deploy the Claude/Codex instruction SSOT into $HOME (by copy).
|
|
739
1034
|
|
|
740
1035
|
agent-bios install deploy into this environment (backs up + verifies)
|
|
741
|
-
agent-bios onboard interactive domain selection +
|
|
1036
|
+
agent-bios onboard interactive domain selection + install + activation canary
|
|
742
1037
|
agent-bios verify check the deployed state matches the source
|
|
743
1038
|
agent-bios learn submit a session learning (reads the JSON record on
|
|
744
1039
|
stdin; this is what the learn! flow calls, and it
|
|
@@ -749,20 +1044,34 @@ agent-bios — deploy the Claude/Codex instruction SSOT into $HOME (by copy).
|
|
|
749
1044
|
agent-bios help
|
|
750
1045
|
|
|
751
1046
|
Flags: --dry-run print actions without changing anything
|
|
752
|
-
--domains a,b
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
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.
|
|
759
1053
|
--with a,b also install the named optional dependencies (install only).
|
|
760
1054
|
Without it, install offers each missing one when the terminal
|
|
761
1055
|
is interactive, and otherwise just prints its install line.
|
|
762
|
-
|
|
763
|
-
|
|
1056
|
+
Missing ones are not fatal — they only degrade the review
|
|
1057
|
+
routes that need them.
|
|
764
1058
|
Env: CLAUDE_CONFIG_DIR, CODEX_HOME, AGENT_LAUNCH_VENV, ZDOTDIR
|
|
765
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
|
|
766
1075
|
}
|
|
767
1076
|
|
|
768
1077
|
# ---- dispatch ------------------------------------------------------------
|
|
@@ -772,10 +1081,10 @@ if [ $# -gt 0 ]; then shift; fi
|
|
|
772
1081
|
# `learn` forwards its arguments and stdin straight to the collector, so it must
|
|
773
1082
|
# bypass the flag parser below (which rejects anything it does not know). This
|
|
774
1083
|
# subcommand is the only PATH-reachable entry to capture: the corpus guide used
|
|
775
|
-
# to invoke
|
|
1084
|
+
# to invoke learn/collect-learning.py relative to the cwd, which works from a
|
|
776
1085
|
# clone and silently fails for every other install.
|
|
777
1086
|
if [ "$CMD" = "learn" ]; then
|
|
778
|
-
collector="$REPO/
|
|
1087
|
+
collector="$REPO/learn/collect-learning.py"
|
|
779
1088
|
[ -f "$collector" ] || { log "learn: collector missing at $collector"; exit 1; }
|
|
780
1089
|
exec python3 "$collector" "$@" <&3
|
|
781
1090
|
fi
|