agent-bios 0.10.0 → 0.12.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.
- package/DEPENDENCIES.md +3 -3
- package/README.md +4 -4
- package/claude/guides/cli-multi-model-workflow.md +50 -4
- package/claude/guides/coding-staged-workflow.md +1 -1
- package/claude/guides/session-distill-workflow.md +3 -3
- package/claude/hooks/tooling-gotchas-hook.py +37 -2
- package/claude/skills/repo-charter/SKILL.md +149 -0
- package/codex/guides/cli-multi-model-workflow.md +50 -4
- package/codex/guides/coding-staged-workflow.md +1 -1
- package/codex/guides/session-distill-workflow.md +3 -3
- package/compose/assemble.py +184 -21
- package/compose/canary.sh +13 -0
- package/compose/check-domains.py +144 -20
- package/compose/domains.json +3 -0
- package/compose/prune-backups.py +57 -7
- package/install.sh +442 -30
- package/launch/agent-launch.py +4567 -659
- package/launch/agent-launch.toml +65 -110
- package/launch/agent-launch.zsh +7 -2
- package/launch/i18n/en.toml +186 -0
- package/launch/i18n/ja.toml +182 -0
- package/launch/i18n/ko.toml +182 -0
- package/learn/check-learning.py +19 -1
- package/learn/collect-learning.py +57 -2
- package/learn/migrate-learnings.py +140 -16
- package/learn/redact.py +14 -5
- package/package.json +4 -2
- package/provenance.json +1 -1
- package/session-cost.py +402 -33
- package/wrappers/claude-run.sh +49 -4
- package/wrappers/codex-run.sh +1 -1
package/install.sh
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
# agent-bios install deploy into this environment (backs up + verifies)
|
|
12
12
|
# agent-bios verify check the deployed state matches the source
|
|
13
13
|
# agent-bios status show what is installed and where
|
|
14
|
+
# agent-bios cost the session cost / context meter, from any directory
|
|
14
15
|
# agent-bios update git pull + reinstall (clone), or print the npm update line
|
|
15
16
|
# agent-bios uninstall remove deployed files and the zsh hook
|
|
16
17
|
# agent-bios help
|
|
@@ -70,6 +71,18 @@ HOOK_MARK='agent-launch/shell.zsh'
|
|
|
70
71
|
|
|
71
72
|
DRY_RUN=0
|
|
72
73
|
BACKUP_DIR=""
|
|
74
|
+
# Set when uninstall could not undo the spans the assembler merged into files we do not own.
|
|
75
|
+
# Global for the same reason UNBACKED is: the closing summary must not claim a clean removal
|
|
76
|
+
# that did not happen.
|
|
77
|
+
CLEANUP_FAILED=0
|
|
78
|
+
# Set when the assembler reported the user-owned entry file needs a line added by hand. That
|
|
79
|
+
# is an outstanding ACTION, not a failed deployment: everything else really did land, and
|
|
80
|
+
# treating it as a failure made cmd_install exit 1 and the EXIT trap restore the previous
|
|
81
|
+
# manifest — leaving the new corpus on disk with the record saying the old one was deployed.
|
|
82
|
+
ENTRY_NEEDS_ACTION=0
|
|
83
|
+
# Deployed files uninstall could not back up, so did not delete. Global rather than local
|
|
84
|
+
# because the closing summary must not claim a clean removal that did not happen.
|
|
85
|
+
UNBACKED=0
|
|
73
86
|
|
|
74
87
|
log() { printf '%s\n' "$*"; }
|
|
75
88
|
info() { printf ' %s\n' "$*"; }
|
|
@@ -117,6 +130,115 @@ deploy_glob() {
|
|
|
117
130
|
done
|
|
118
131
|
}
|
|
119
132
|
|
|
133
|
+
# A subtree, file by file, so every file lands in the manifest by its own name — the
|
|
134
|
+
# ownership rule the hooks fix settled on. Skills need this: a skill is a directory
|
|
135
|
+
# (SKILL.md plus free subdirectories) that lands in a directory the host scans and other
|
|
136
|
+
# tools populate, so nothing may claim it by path prefix, and uninstall removes exactly
|
|
137
|
+
# the names we wrote. `find -type f`, not a glob: subdirectories are the format.
|
|
138
|
+
deploy_tree() {
|
|
139
|
+
local srcdir="$1" dstdir="$2" f rel
|
|
140
|
+
[ -d "$srcdir" ] || { log "source tree missing: $srcdir"; return 1; }
|
|
141
|
+
while IFS= read -r f; do
|
|
142
|
+
rel="${f#"$srcdir"/}"
|
|
143
|
+
deploy_file "$f" "$dstdir/$rel"
|
|
144
|
+
done < <(find "$srcdir" -type f | LC_ALL=C sort)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# The skills we ship: every subdirectory of the repo's claude/skills/. One tree serves both
|
|
148
|
+
# hosts — the format is identical (SKILL.md + name/description frontmatter + free
|
|
149
|
+
# subdirectories) and the body names no host path — so the same files land under each
|
|
150
|
+
# host's skills directory. Iterated here rather than listed, so a second skill ships by
|
|
151
|
+
# being added to the tree. This is the SHIPPED set — what the tree carries; what a given
|
|
152
|
+
# machine RECEIVES is the selected set below.
|
|
153
|
+
shipped_skills() {
|
|
154
|
+
local d
|
|
155
|
+
for d in "$REPO/claude/skills"/*/; do
|
|
156
|
+
[ -f "$d/SKILL.md" ] || continue
|
|
157
|
+
basename "$d"
|
|
158
|
+
done
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# The selection, as the assembler receives it: an explicit --domains, else the saved
|
|
162
|
+
# selection.json, else every domain (what "full" means). One function so the corpus and
|
|
163
|
+
# the skills are always answered from the same selection — a skill decided from a
|
|
164
|
+
# different reading than the bundle would ship a package the bundle disagrees with.
|
|
165
|
+
selection_args() { # -> SEL_ARGS
|
|
166
|
+
SEL_ARGS=()
|
|
167
|
+
if [ "${DOMAINS_SET:-0}" = 1 ]; then
|
|
168
|
+
SEL_ARGS=(--domains "$DOMAINS_ARG")
|
|
169
|
+
elif [ ! -f "$STATE_DIR/selection.json" ]; then
|
|
170
|
+
SEL_ARGS=(--domains "$(all_domains_csv)")
|
|
171
|
+
fi
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# The skills this selection delivers, by name. Skills are classified in
|
|
175
|
+
# compose/domains.json like guides, hooks and agents, and the assembler owns the rule
|
|
176
|
+
# that turns a classification and a selection into a delivered set; this asks it rather
|
|
177
|
+
# than reading domains.json here, so there is one owner of "who gets what". Deploy,
|
|
178
|
+
# prune and verify all iterate THIS list; a skill the tree ships but the selection does
|
|
179
|
+
# not cover is treated exactly like a file a later release withdrew.
|
|
180
|
+
selected_skills() {
|
|
181
|
+
selection_args
|
|
182
|
+
python3 "$REPO/compose/assemble.py" --selected-skills --state-dir "$STATE_DIR" \
|
|
183
|
+
${SEL_ARGS[@]+"${SEL_ARGS[@]}"}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
# Membership in the selected set, for the loops that iterate the shipped one. Refuses to
|
|
187
|
+
# answer before the set is resolved: an unset variable read as "nothing selected" would
|
|
188
|
+
# make prune remove every skill and verify demand every absence — the fail-open shape.
|
|
189
|
+
skill_selected() { # $1: skill name; reads SELECTED_SKILLS
|
|
190
|
+
[ -n "${SELECTED_SKILLS+x}" ] || { log "BUG: skill selection read before it was resolved"; exit 1; }
|
|
191
|
+
case " $(printf '%s' "$SELECTED_SKILLS" | tr '\n' ' ') " in *" $1 "*) return 0 ;; esac
|
|
192
|
+
return 1
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Our skill directories, and only ours: `$CLAUDE_DIR/skills` and `$CODEX_DIR/skills` are
|
|
196
|
+
# the hosts' shared scan directories, holding other tools' skills, and are never touched.
|
|
197
|
+
# Deepest first, so a skill's subdirectories go before the skill itself; rmdir throughout,
|
|
198
|
+
# so a directory holding anything we did not write is left, visibly.
|
|
199
|
+
rmdir_skill_dirs() {
|
|
200
|
+
local skill="$1" sd d
|
|
201
|
+
for sd in "$CLAUDE_DIR/skills/$skill" "$CODEX_DIR/skills/$skill"; do
|
|
202
|
+
[ -d "$sd" ] || continue
|
|
203
|
+
while IFS= read -r d; do
|
|
204
|
+
rmdir "$d" 2>/dev/null && info "removed empty $d" || true
|
|
205
|
+
done < <(find "$sd" -depth -type d)
|
|
206
|
+
done
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# A skill file the PREVIOUS install wrote and this release no longer ships is ours by the
|
|
210
|
+
# prior manifest and would otherwise stay on disk unowned: the rebuilt manifest stops
|
|
211
|
+
# naming it, verify enumerates only current files, and the host keeps loading it. So the
|
|
212
|
+
# prior manifest is read for paths under either host's skills root that no shipped file
|
|
213
|
+
# maps to, and those are removed — backed up like any overwrite — before the deploy.
|
|
214
|
+
# Only skill paths: every other deploy target is a fixed name a later release overwrites
|
|
215
|
+
# in place. Only names the manifest proves we wrote.
|
|
216
|
+
prune_stale_skills() {
|
|
217
|
+
local prior="$PRIOR_MANIFEST" f skill rel touched=""
|
|
218
|
+
[ "$DRY_RUN" = 1 ] && prior="$MANIFEST"
|
|
219
|
+
[ -f "$prior" ] || return 0
|
|
220
|
+
while IFS= read -r f; do
|
|
221
|
+
[ -n "$f" ] || continue
|
|
222
|
+
case "$f" in
|
|
223
|
+
"$CLAUDE_DIR/skills/"*) rel="${f#"$CLAUDE_DIR/skills/"}" ;;
|
|
224
|
+
"$CODEX_DIR/skills/"*) rel="${f#"$CODEX_DIR/skills/"}" ;;
|
|
225
|
+
*) continue ;;
|
|
226
|
+
esac
|
|
227
|
+
skill="${rel%%/*}"; rel="${rel#*/}"
|
|
228
|
+
# Still shipped AND still selected: only that combination keeps a prior file. A
|
|
229
|
+
# deselected skill's files are stale exactly like a withdrawn release's — the host
|
|
230
|
+
# would go on loading them for a package the user no longer takes.
|
|
231
|
+
[ -f "$REPO/claude/skills/$skill/$rel" ] && skill_selected "$skill" && continue
|
|
232
|
+
[ -f "$f" ] || continue
|
|
233
|
+
if [ -n "$BACKUP_DIR" ] && [ "$DRY_RUN" != 1 ]; then
|
|
234
|
+
mkdir -p "$(dirname "$BACKUP_DIR$f")" && cp "$f" "$BACKUP_DIR$f"
|
|
235
|
+
fi
|
|
236
|
+
run rm -f "$f"; [ "$DRY_RUN" = 1 ] || info "removed stale skill file $f"
|
|
237
|
+
case " $touched " in *" $skill "*) ;; *) touched="$touched $skill" ;; esac
|
|
238
|
+
done < "$prior"
|
|
239
|
+
for skill in $touched; do rmdir_skill_dirs "$skill"; done
|
|
240
|
+
}
|
|
241
|
+
|
|
120
242
|
# Backups are made on every install and every uninstall, and nothing used to remove them — 23
|
|
121
243
|
# directories in two weeks on the first machine measured, plus loose `.bak-*` files sitting in
|
|
122
244
|
# the user's own config dirs. Pruning runs AFTER the new copy exists, never before, so a failed
|
|
@@ -207,15 +329,17 @@ prune_withheld() {
|
|
|
207
329
|
# we never rewrite, so they are separately versioned by construction; a package can be added or
|
|
208
330
|
# dropped later by re-assembling, with no need to know which bytes came from where; and uninstall
|
|
209
331
|
# can take everything of ours because nothing of theirs is mixed into it.
|
|
332
|
+
all_domains_csv() {
|
|
333
|
+
python3 -c "import json,sys;print(','.join(sorted(json.load(open(sys.argv[1]))['domains'])))" \
|
|
334
|
+
"$REPO/compose/domains.json"
|
|
335
|
+
}
|
|
336
|
+
|
|
210
337
|
assemble_corpus() {
|
|
211
338
|
local args=(--claude-dir "$CLAUDE_DIR" --codex-dir "$CODEX_DIR" --state-dir "$STATE_DIR") rc=0
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
339
|
+
# No flag and no saved selection: install everything. This is what "full" meant, expressed as
|
|
340
|
+
# a selection so it goes down the same path as every other one (selection_args).
|
|
341
|
+
selection_args
|
|
342
|
+
args+=(${SEL_ARGS[@]+"${SEL_ARGS[@]}"})
|
|
219
343
|
[ "$DRY_RUN" = 1 ] && args+=(--dry-run)
|
|
220
344
|
# A dry run never truncated the manifest, so the live one still describes the last install.
|
|
221
345
|
local prior="$PRIOR_MANIFEST"
|
|
@@ -223,24 +347,46 @@ assemble_corpus() {
|
|
|
223
347
|
[ -f "$prior" ] && args+=(--prior-manifest "$prior")
|
|
224
348
|
python3 "$REPO/compose/assemble.py" "${args[@]}" || rc=$?
|
|
225
349
|
if [ "$rc" = 2 ]; then
|
|
350
|
+
ENTRY_NEEDS_ACTION=1
|
|
226
351
|
log "entry file needs user action (import line missing); central content will not load until it is added"
|
|
227
352
|
elif [ "$rc" != 0 ]; then
|
|
228
353
|
return 1
|
|
229
354
|
fi
|
|
230
355
|
if [ "$DRY_RUN" != 1 ]; then
|
|
231
|
-
#
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
#
|
|
236
|
-
|
|
237
|
-
|
|
356
|
+
# The ownership rule is the assembler's, and it is one rule for every destination: the
|
|
357
|
+
# name exists in our source tree (compose/assemble.py copy_filtered — "it is not in
|
|
358
|
+
# src_dir, so it is never a candidate"). `central/` was exempted as "ours whole" and
|
|
359
|
+
# scanned, which claimed whatever a user had put there — and uninstall removes whatever
|
|
360
|
+
# the manifest names, so the assembler's care was undone one step later. That is the
|
|
361
|
+
# same defect this comment records having fixed for the SHARED $CODEX_DIR/guides; the
|
|
362
|
+
# directory being ours by convention did not make the scan a different mistake.
|
|
363
|
+
# bundle.md is generated rather than copied, so it has no source name and is ours
|
|
364
|
+
# unconditionally.
|
|
365
|
+
local g dst sub s
|
|
366
|
+
if [ -f "$CLAUDE_DIR/central/bundle.md" ]; then
|
|
367
|
+
printf '%s\n' "$CLAUDE_DIR/central/bundle.md" >> "$MANIFEST"
|
|
368
|
+
fi
|
|
369
|
+
for sub in guides hooks agents; do
|
|
370
|
+
for s in "$REPO"/claude/"$sub"/*; do
|
|
371
|
+
[ -f "$s" ] || continue
|
|
372
|
+
dst="$CLAUDE_DIR/central/$sub/$(basename "$s")"
|
|
373
|
+
# if-form for the reason spelled out below: a trailing false status becomes the
|
|
374
|
+
# loop's, then the function's, and the install dies silently after ASSEMBLED.
|
|
375
|
+
if [ -f "$dst" ]; then printf '%s\n' "$dst" >> "$MANIFEST"; fi
|
|
376
|
+
done
|
|
377
|
+
done
|
|
238
378
|
for g in "$REPO"/codex/guides/*.md; do
|
|
239
379
|
[ -f "$g" ] || continue
|
|
240
380
|
dst="$CODEX_DIR/guides/$(basename "$g")"
|
|
241
|
-
[ -f
|
|
381
|
+
# if-form, not `[ -f ] && printf`: when the alphabetically LAST guide is
|
|
382
|
+
# not deployed (a partial domain selection on a fresh machine), the
|
|
383
|
+
# compound's false status became the loop's, the loop's became the
|
|
384
|
+
# function's, and the install died silently right after ASSEMBLED —
|
|
385
|
+
# a latent first-user bug the I9 scenario exposed.
|
|
386
|
+
if [ -f "$dst" ]; then printf '%s\n' "$dst" >> "$MANIFEST"; fi
|
|
242
387
|
done
|
|
243
388
|
fi
|
|
389
|
+
return 0
|
|
244
390
|
}
|
|
245
391
|
|
|
246
392
|
add_zsh_hook() {
|
|
@@ -560,7 +706,18 @@ migrate_learnings() {
|
|
|
560
706
|
local script="$REPO/learn/migrate-learnings.py"
|
|
561
707
|
{ [ -f "$script" ] && [ -f "$REPO/learn/promotions.json" ]; } || return 0
|
|
562
708
|
local -a sel dry
|
|
563
|
-
|
|
709
|
+
# The SAME three-way assemble_corpus resolves, because these two must be answering one
|
|
710
|
+
# question. Naming only the selection file made a fresh `--dry-run` preview migration as
|
|
711
|
+
# "skipped" — a dry run deliberately does not write selection.json, while the real run
|
|
712
|
+
# writes it moments earlier and then migrates for real. A preview that reports the
|
|
713
|
+
# opposite of what the run does is worse than no preview.
|
|
714
|
+
if [ "${DOMAINS_SET:-0}" = 1 ]; then
|
|
715
|
+
sel=(--domains "$DOMAINS_ARG")
|
|
716
|
+
elif [ -f "$STATE_DIR/selection.json" ]; then
|
|
717
|
+
sel=(--selection-file "$STATE_DIR/selection.json")
|
|
718
|
+
else
|
|
719
|
+
sel=(--domains "$(all_domains_csv)")
|
|
720
|
+
fi
|
|
564
721
|
[ "$DRY_RUN" = 1 ] && dry=(--dry-run) || dry=()
|
|
565
722
|
# ${dry[@]+...}: expanding an empty array as "${dry[@]}" is an unbound-variable
|
|
566
723
|
# error under `set -u` on bash 3.2 (macOS default) and would abort the install.
|
|
@@ -579,6 +736,18 @@ cmd_install() {
|
|
|
579
736
|
BACKUP_DIR="$STATE_DIR/backups/$(date +%Y%m%d-%H%M%S)"
|
|
580
737
|
[ -f "$MANIFEST" ] && cp "$MANIFEST" "$PRIOR_MANIFEST"
|
|
581
738
|
: > "$MANIFEST"
|
|
739
|
+
# The manifest is emptied here and refilled as files are deployed, so between this
|
|
740
|
+
# line and a completed install it does not describe what is on disk. An install that
|
|
741
|
+
# fails in between — `--domains no-such-domain` is enough — left it EMPTY, and
|
|
742
|
+
# `uninstall` consumes it: it removed nothing, exited 0, and told the user the
|
|
743
|
+
# uninstall had succeeded while all fifty deployed files were still there. A
|
|
744
|
+
# destructive command reporting success for doing nothing is the worst reading in
|
|
745
|
+
# this file, so the prior manifest goes back unless the install reaches the end.
|
|
746
|
+
INSTALL_COMPLETED=0
|
|
747
|
+
trap 'if [ "${INSTALL_COMPLETED:-0}" != 1 ] && [ -f "$PRIOR_MANIFEST" ]; then
|
|
748
|
+
cp "$PRIOR_MANIFEST" "$MANIFEST"
|
|
749
|
+
log "install did not complete; restored the previous manifest so uninstall still knows what was deployed"
|
|
750
|
+
fi' EXIT
|
|
582
751
|
fi
|
|
583
752
|
log "Deploying agent-bios from $REPO"
|
|
584
753
|
# Refuse loudly before doing any work: an unreadable declaration must not degrade
|
|
@@ -591,6 +760,16 @@ cmd_install() {
|
|
|
591
760
|
prune_withheld "$CLAUDE_DIR/guides"
|
|
592
761
|
migrate_learnings # Phase 4: clear personal copies now absorbed by the corpus
|
|
593
762
|
deploy_glob "$REPO/codex/agents" "*.toml" "$CODEX_DIR/agents"
|
|
763
|
+
# Resolved once, before the prune that reads it: an assembler that cannot answer must
|
|
764
|
+
# stop the install here, not let `for skill in $(...)` iterate an empty answer and
|
|
765
|
+
# deploy nothing while reporting success.
|
|
766
|
+
SELECTED_SKILLS="$(selected_skills)" || { log "could not resolve the selected skills"; exit 1; }
|
|
767
|
+
prune_stale_skills
|
|
768
|
+
local skill
|
|
769
|
+
for skill in $SELECTED_SKILLS; do
|
|
770
|
+
deploy_tree "$REPO/claude/skills/$skill" "$CLAUDE_DIR/skills/$skill" || exit 1
|
|
771
|
+
deploy_tree "$REPO/claude/skills/$skill" "$CODEX_DIR/skills/$skill" || exit 1
|
|
772
|
+
done
|
|
594
773
|
codex_config_additions merge || exit 1
|
|
595
774
|
deploy_file "$REPO/wrappers/codex-run.sh" "$CODEX_DIR/bin/codex-run" "+x"
|
|
596
775
|
deploy_file "$REPO/wrappers/codex-helm.sh" "$CODEX_DIR/bin/codex-helm" "+x"
|
|
@@ -601,7 +780,45 @@ cmd_install() {
|
|
|
601
780
|
migrate_user_presets || exit 1 # must precede the deploy below, which overwrites profiles.toml
|
|
602
781
|
deploy_file "$REPO/launch/agent-launch.toml" "$LAUNCH_DIR/profiles.toml"
|
|
603
782
|
deploy_file "$REPO/launch/agent-launch.zsh" "$LAUNCH_DIR/shell.zsh"
|
|
783
|
+
# UI text catalogs: deploy-managed siblings of the config, one per language.
|
|
784
|
+
# The launcher resolves them from the config path, so the deployed home is
|
|
785
|
+
# $LAUNCH_DIR/i18n exactly as the checkout's is launch/i18n.
|
|
786
|
+
#
|
|
787
|
+
# The launcher goes FIRST, and the order is chosen for how an interrupted
|
|
788
|
+
# install FAILS rather than for how much it breaks. The deployed launcher keeps
|
|
789
|
+
# no catalog beside itself -- it lives in $BIN_DIR while the catalogs live under
|
|
790
|
+
# $LAUNCH_DIR -- so it renders whatever is deployed, and the two orders leave
|
|
791
|
+
# opposite states:
|
|
792
|
+
#
|
|
793
|
+
# launcher first -> new launcher, old catalogs: keys it added render as their
|
|
794
|
+
# own names AND t() prints "the deployed catalogs are older
|
|
795
|
+
# than this launcher; run: agent-bios install". Loud, and
|
|
796
|
+
# the message is the fix.
|
|
797
|
+
# catalogs first -> old launcher, new catalogs: a value whose SHAPE changed
|
|
798
|
+
# renders its template. Measured on the 1ba2d82 launcher
|
|
799
|
+
# against these catalogs, the checklist drew
|
|
800
|
+
# "선택 적용{pending}" with no diagnostic at all.
|
|
801
|
+
#
|
|
802
|
+
# The second is less broken and entirely silent, which is the worse trade: a
|
|
803
|
+
# user cannot act on what does not announce itself. This ordering was briefly
|
|
804
|
+
# the other way round on the claim that spare keys are harmless; they are, but
|
|
805
|
+
# changed values are not, and four keys in this change altered shape.
|
|
806
|
+
#
|
|
807
|
+
# The loudness is contingent, not structural, and narrower than an earlier version
|
|
808
|
+
# of this comment claimed: the notice fires only for a key the FIRST SCREEN itself
|
|
809
|
+
# requests and the old catalogs lack. Adding keys anywhere is not enough. This
|
|
810
|
+
# release adds four and the root screen requests none of them — they are reached
|
|
811
|
+
# from the registration wizard and the corpus screens — so for this release the
|
|
812
|
+
# ordering buys no diagnostic at all, only the smaller blast radius of a launcher
|
|
813
|
+
# that is newer than its catalogs rather than older. The shape-changed keys are
|
|
814
|
+
# silent in that direction too: an old value with no slot formats to itself and the
|
|
815
|
+
# argument simply vanishes. Keep the order for the blast radius; do not rely on the
|
|
816
|
+
# diagnostic unless a release actually adds a root-screen key.
|
|
604
817
|
deploy_file "$REPO/launch/agent-launch.py" "$BIN_DIR/agent-launch" "+x"
|
|
818
|
+
local lang
|
|
819
|
+
for lang in en ko ja; do
|
|
820
|
+
deploy_file "$REPO/launch/i18n/$lang.toml" "$LAUNCH_DIR/i18n/$lang.toml"
|
|
821
|
+
done
|
|
605
822
|
log ""
|
|
606
823
|
log "Optional dependencies (missing ones only degrade the routes that need them)..."
|
|
607
824
|
handle_capabilities "$WITH" || exit 1
|
|
@@ -613,7 +830,15 @@ cmd_install() {
|
|
|
613
830
|
|| log "warning: venv provisioning failed (numbered-prompt fallback applies)"
|
|
614
831
|
fi
|
|
615
832
|
add_zsh_hook
|
|
616
|
-
|
|
833
|
+
# Guarded, because this WRITES. The projection takes a lock and rewrites
|
|
834
|
+
# corpus-status.json, and it sat outside the dry-run branch above — so
|
|
835
|
+
# `install --dry-run` changed the file whose whole purpose is to describe what is
|
|
836
|
+
# deployed, against this script's own `--dry-run print actions without changing
|
|
837
|
+
# anything` and README's identical sentence. A dry run that edits state is worse than
|
|
838
|
+
# no dry run: it is consulted precisely when the user is unwilling to touch anything.
|
|
839
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
840
|
+
info "[dry-run] project corpus-status"
|
|
841
|
+
elif python3 "$REPO/compose/corpus-state.py" project --repo "$REPO" >/dev/null 2>&1; then
|
|
617
842
|
info "corpus-status projected"
|
|
618
843
|
else
|
|
619
844
|
log "note: corpus-status projection unavailable (versions.json/ledger missing?)"
|
|
@@ -653,10 +878,29 @@ PY
|
|
|
653
878
|
fi
|
|
654
879
|
fi
|
|
655
880
|
log ""
|
|
881
|
+
if [ "$DRY_RUN" = 1 ]; then
|
|
882
|
+
# A dry run wrote nothing, so there is nothing of THIS plan to verify. Running the live
|
|
883
|
+
# verifier anyway made a valid preview exit 1 on any machine where the planned files are
|
|
884
|
+
# not already present — which is every fresh one, the case a preview is most for. On an
|
|
885
|
+
# installed machine it is no better: it would be reporting on the previous install while
|
|
886
|
+
# standing where a verdict on the plan just printed belongs.
|
|
887
|
+
INSTALL_COMPLETED=1
|
|
888
|
+
log "[dry-run] nothing was written, so nothing is verified — re-run without --dry-run to install."
|
|
889
|
+
return 0
|
|
890
|
+
fi
|
|
656
891
|
log "Verifying deployment..."
|
|
657
892
|
if cmd_verify; then
|
|
893
|
+
# Reached the end: the manifest now describes what is actually deployed, so the
|
|
894
|
+
# restore armed above must not fire.
|
|
895
|
+
INSTALL_COMPLETED=1
|
|
658
896
|
log ""
|
|
659
897
|
log "Done. Open a new shell (or: source \"$ZSHRC\") to activate the zero-arg launcher."
|
|
898
|
+
if [ "${ENTRY_NEEDS_ACTION:-0}" = 1 ]; then
|
|
899
|
+
log ""
|
|
900
|
+
log "ONE STEP LEFT: add this line to $CLAUDE_DIR/CLAUDE.md (yours; we never rewrite it):"
|
|
901
|
+
log " @central/bundle.md"
|
|
902
|
+
log "Until then the deployed corpus will not load."
|
|
903
|
+
fi
|
|
660
904
|
# An untouched backup dir means nothing was replaced; that healthy state
|
|
661
905
|
# must not become a nonzero exit under set -e.
|
|
662
906
|
{ [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ] && log "Replaced files were backed up under $BACKUP_DIR"; } || true
|
|
@@ -695,6 +939,12 @@ cmd_verify() {
|
|
|
695
939
|
verify_present "$CLAUDE_DIR/central/bundle.md" || fail=1
|
|
696
940
|
if grep -qF '@central/bundle.md' "$CLAUDE_DIR/CLAUDE.md" 2>/dev/null; then
|
|
697
941
|
info "entry import line present"
|
|
942
|
+
elif [ "${ENTRY_NEEDS_ACTION:-0}" = 1 ]; then
|
|
943
|
+
# The install that just ran said this, and said it because the file is the user's and is
|
|
944
|
+
# never rewritten. Failing on it a second time turned a deployment that fully succeeded
|
|
945
|
+
# into one whose record was rolled back. Standalone `agent-bios verify` has this unset,
|
|
946
|
+
# so it still reports a corpus that is not loading as the failure it is.
|
|
947
|
+
log "ACTION NEEDED: add '@central/bundle.md' to $CLAUDE_DIR/CLAUDE.md — everything else deployed"
|
|
698
948
|
else
|
|
699
949
|
log "entry $CLAUDE_DIR/CLAUDE.md lacks '@central/bundle.md' — central corpus is NOT loading"; fail=1
|
|
700
950
|
fi
|
|
@@ -725,6 +975,14 @@ cmd_verify() {
|
|
|
725
975
|
verify_match "$REPO/launch/agent-launch.py" "$BIN_DIR/agent-launch" || fail=1
|
|
726
976
|
verify_match "$REPO/launch/agent-launch.toml" "$LAUNCH_DIR/profiles.toml" || fail=1
|
|
727
977
|
verify_match "$REPO/launch/agent-launch.zsh" "$LAUNCH_DIR/shell.zsh" || fail=1
|
|
978
|
+
# The catalogs were the one deploy-managed artifact nothing verified, which is
|
|
979
|
+
# the state that renders key names on screen: the deployed launcher keeps no
|
|
980
|
+
# catalog beside itself, so whatever is here IS the UI text. Byte-identity, the
|
|
981
|
+
# same bar as its siblings above.
|
|
982
|
+
local lang
|
|
983
|
+
for lang in en ko ja; do
|
|
984
|
+
verify_match "$REPO/launch/i18n/$lang.toml" "$LAUNCH_DIR/i18n/$lang.toml" || fail=1
|
|
985
|
+
done
|
|
728
986
|
python3 - "$CODEX_DIR/agents" <<'PY' && info "agent TOMLs OK" || fail=1
|
|
729
987
|
import sys, pathlib, tomllib
|
|
730
988
|
root = pathlib.Path(sys.argv[1])
|
|
@@ -735,6 +993,26 @@ for p in sorted(root.glob("*.toml")):
|
|
|
735
993
|
tomllib.loads(p.read_text())
|
|
736
994
|
PY
|
|
737
995
|
codex_config_additions check || fail=1
|
|
996
|
+
# Every file of every SELECTED skill, byte-identical on both hosts — the same bar as the
|
|
997
|
+
# launcher and its catalogs, because a skill's SKILL.md IS what the host reads. A skill
|
|
998
|
+
# the selection does not deliver must be absent on both hosts, or the host is loading a
|
|
999
|
+
# package the user did not take.
|
|
1000
|
+
local skill sf rel
|
|
1001
|
+
SELECTED_SKILLS="$(selected_skills)" || { log "could not resolve the selected skills"; fail=1; }
|
|
1002
|
+
for skill in $(shipped_skills); do
|
|
1003
|
+
if skill_selected "$skill"; then
|
|
1004
|
+
while IFS= read -r sf; do
|
|
1005
|
+
rel="${sf#"$REPO/claude/skills/$skill"/}"
|
|
1006
|
+
verify_match "$sf" "$CLAUDE_DIR/skills/$skill/$rel" || fail=1
|
|
1007
|
+
verify_match "$sf" "$CODEX_DIR/skills/$skill/$rel" || fail=1
|
|
1008
|
+
done < <(find "$REPO/claude/skills/$skill" -type f | LC_ALL=C sort)
|
|
1009
|
+
else
|
|
1010
|
+
for sf in "$CLAUDE_DIR/skills/$skill/SKILL.md" "$CODEX_DIR/skills/$skill/SKILL.md"; do
|
|
1011
|
+
if [ -f "$sf" ]; then log "deselected skill still deployed $sf"; fail=1
|
|
1012
|
+
else info "absent (deselected) $sf"; fi
|
|
1013
|
+
done
|
|
1014
|
+
fi
|
|
1015
|
+
done
|
|
738
1016
|
if command -v codex >/dev/null 2>&1 && [ -x "$CODEX_DIR/bin/codex-helm" ]; then
|
|
739
1017
|
if "$CODEX_DIR/bin/codex-helm" --dry-run --mode review "probe" >/dev/null 2>&1; then
|
|
740
1018
|
info "codex-helm dry-run OK"
|
|
@@ -778,7 +1056,20 @@ PY
|
|
|
778
1056
|
fi
|
|
779
1057
|
# Repo-internal mirror parity is a maintainer gate; only meaningful from a clone.
|
|
780
1058
|
if [ -d "$REPO/ko" ] && [ -x "$REPO/gates/check-parity.sh" ]; then
|
|
781
|
-
|
|
1059
|
+
# Output kept, not discarded — the third place in this repo where a gate's own
|
|
1060
|
+
# explanation went to /dev/null and left "it failed" as the entire report. The umbrella
|
|
1061
|
+
# and the install-scenario harness each learned this after a failure cost an eleven-
|
|
1062
|
+
# minute re-run that came back green; verify is where a USER meets it, with no clone to
|
|
1063
|
+
# re-run from.
|
|
1064
|
+
parity_log="$(mktemp -t verify-parity)"
|
|
1065
|
+
if "$REPO/gates/check-parity.sh" >"$parity_log" 2>&1; then
|
|
1066
|
+
info "repo mirror parity OK"; rm -f "$parity_log"
|
|
1067
|
+
else
|
|
1068
|
+
log "repo mirror parity FAILED"
|
|
1069
|
+
grep -a 'FAIL' "$parity_log" | head -10 | sed 's/^/ /'
|
|
1070
|
+
log " full output: $parity_log"
|
|
1071
|
+
fail=1
|
|
1072
|
+
fi
|
|
782
1073
|
fi
|
|
783
1074
|
# Prompting guides name concrete models, so they go stale on a model change
|
|
784
1075
|
# rather than degrading quietly; this checks them against the launch config.
|
|
@@ -801,10 +1092,14 @@ cmd_uninstall() {
|
|
|
801
1092
|
# invoking deleted files and instructions pointing at deleted guides. It reads ownership the
|
|
802
1093
|
# same way the merge wrote it, so it cannot reach past what we put there.
|
|
803
1094
|
if [ -f "$REPO/compose/assemble.py" ]; then
|
|
1095
|
+
# The failure is recorded, not only printed. A warning scrolls past and the summary at
|
|
1096
|
+
# the end went on saying "nothing of ours is left on this machine" — while the spans this
|
|
1097
|
+
# step exists to remove were still in the user's settings.json, now pointing at hook files
|
|
1098
|
+
# the next step deletes. A warning nobody reads is how the two came to disagree.
|
|
804
1099
|
python3 "$REPO/compose/assemble.py" --remove-owned --claude-dir "$CLAUDE_DIR" \
|
|
805
1100
|
--codex-dir "$CODEX_DIR" --state-dir "$STATE_DIR" ${DRY_RUN:+} \
|
|
806
1101
|
$([ "$DRY_RUN" = 1 ] && echo --dry-run) \
|
|
807
|
-
|| log "warning: could not remove assembler-owned regions"
|
|
1102
|
+
|| { CLEANUP_FAILED=1; log "warning: could not remove assembler-owned regions"; }
|
|
808
1103
|
fi
|
|
809
1104
|
if [ -f "$MANIFEST" ]; then
|
|
810
1105
|
# Back up before deleting, the way install backs up before overwriting. Full mode deploys the
|
|
@@ -814,15 +1109,37 @@ cmd_uninstall() {
|
|
|
814
1109
|
# would have been backed up. Removal is symmetric with deployment; recoverability now is too.
|
|
815
1110
|
[ "$DRY_RUN" = 1 ] || { mkdir -p "$STATE_DIR"; BACKUP_DIR="$STATE_DIR/backups/uninstall-$(date +%Y%m%d-%H%M%S)"; }
|
|
816
1111
|
local f
|
|
1112
|
+
UNBACKED=0
|
|
817
1113
|
while IFS= read -r f; do
|
|
818
1114
|
[ -n "$f" ] || continue
|
|
819
|
-
|
|
820
|
-
|
|
1115
|
+
[ -f "$f" ] || continue
|
|
1116
|
+
if [ -n "$BACKUP_DIR" ] && [ "$DRY_RUN" != 1 ]; then
|
|
1117
|
+
# The rule archive_and_purge applies below, applied here too: only what verifiably
|
|
1118
|
+
# reached a copy may be deleted. The mkdir and the cp both fail silently — a full
|
|
1119
|
+
# disk, a name already taken by a file — and the `&&` chain's result was never read,
|
|
1120
|
+
# so the deployed file was removed anyway and reached neither the backup nor the
|
|
1121
|
+
# archive built from it. Size equality, not cp's status, because the question is
|
|
1122
|
+
# whether it can be restored.
|
|
1123
|
+
if ! { mkdir -p "$(dirname "$BACKUP_DIR$f")" 2>/dev/null \
|
|
1124
|
+
&& cp "$f" "$BACKUP_DIR$f" 2>/dev/null \
|
|
1125
|
+
&& [ "$(wc -c <"$f" 2>/dev/null)" = "$(wc -c <"$BACKUP_DIR$f" 2>/dev/null)" ]; }; then
|
|
1126
|
+
rm -f "$BACKUP_DIR$f" 2>/dev/null # a partial copy must not look backed up
|
|
1127
|
+
UNBACKED=$((UNBACKED + 1))
|
|
1128
|
+
log "warning: could not back up $f — leaving it in place"
|
|
1129
|
+
continue
|
|
1130
|
+
fi
|
|
821
1131
|
fi
|
|
822
|
-
|
|
1132
|
+
run rm -f "$f"; [ "$DRY_RUN" = 1 ] || info "removed $f"
|
|
823
1133
|
done < "$MANIFEST"
|
|
824
1134
|
{ [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ] && info "staged for the archive: $BACKUP_DIR"; } || true
|
|
825
|
-
|
|
1135
|
+
# The manifest survives whenever something it names is still on disk: it is the only
|
|
1136
|
+
# record of what to finish removing, and deleting it would strand those files unowned.
|
|
1137
|
+
if [ "$UNBACKED" -gt 0 ]; then
|
|
1138
|
+
log "warning: $UNBACKED deployed file(s) could not be backed up and were LEFT ON DISK;"
|
|
1139
|
+
log " keeping $MANIFEST so a later uninstall can finish the job"
|
|
1140
|
+
else
|
|
1141
|
+
[ "$DRY_RUN" = 1 ] || rm -f "$MANIFEST"
|
|
1142
|
+
fi
|
|
826
1143
|
else
|
|
827
1144
|
log "no manifest at $MANIFEST; removing known deploy targets"
|
|
828
1145
|
# The two entry files are deliberately NOT in this list. `$CLAUDE_DIR/CLAUDE.md` is seeded
|
|
@@ -837,6 +1154,27 @@ cmd_uninstall() {
|
|
|
837
1154
|
"$LAUNCH_DIR/profiles.toml" "$LAUNCH_DIR/shell.zsh" "$BIN_DIR/agent-launch"; do
|
|
838
1155
|
[ -f "$p" ] && { run rm -f "$p"; [ "$DRY_RUN" = 1 ] || info "removed $p"; }
|
|
839
1156
|
done
|
|
1157
|
+
# Skills: only the names the shipped tree carries, mirrored under each host's skills
|
|
1158
|
+
# directory, and only where the file IS ours — byte-identical to what this package
|
|
1159
|
+
# ships. Without a manifest, sameness of content is the one ownership evidence left:
|
|
1160
|
+
# a user who authored their own `skills/repo-charter/SKILL.md` before ever installing
|
|
1161
|
+
# would otherwise lose it here with no backup. Never the directory wholesale either —
|
|
1162
|
+
# a sibling skill of theirs shares the parent.
|
|
1163
|
+
local skill sf rel
|
|
1164
|
+
for skill in $(shipped_skills); do
|
|
1165
|
+
while IFS= read -r sf; do
|
|
1166
|
+
rel="${sf#"$REPO/claude/skills/$skill"/}"
|
|
1167
|
+
for p in "$CLAUDE_DIR/skills/$skill/$rel" "$CODEX_DIR/skills/$skill/$rel"; do
|
|
1168
|
+
if [ -f "$p" ]; then
|
|
1169
|
+
if cmp -s "$sf" "$p"; then
|
|
1170
|
+
run rm -f "$p"; [ "$DRY_RUN" = 1 ] || info "removed $p"
|
|
1171
|
+
else
|
|
1172
|
+
log "kept $p (differs from the shipped file and no manifest says we wrote it)"
|
|
1173
|
+
fi
|
|
1174
|
+
fi
|
|
1175
|
+
done
|
|
1176
|
+
done < <(find "$REPO/claude/skills/$skill" -type f | LC_ALL=C sort)
|
|
1177
|
+
done
|
|
840
1178
|
fi
|
|
841
1179
|
local d
|
|
842
1180
|
# `central/` and its subdirectories are entirely ours — the assembler creates them and the
|
|
@@ -850,10 +1188,24 @@ cmd_uninstall() {
|
|
|
850
1188
|
"$CODEX_DIR/guides" "$CODEX_DIR/agents" "$CODEX_DIR/bin" "$LAUNCH_DIR"; do
|
|
851
1189
|
[ -d "$d" ] && rmdir "$d" 2>/dev/null && info "removed empty $d" || true
|
|
852
1190
|
done
|
|
1191
|
+
local skill
|
|
1192
|
+
for skill in $(shipped_skills); do rmdir_skill_dirs "$skill"; done
|
|
853
1193
|
remove_zsh_hook
|
|
854
1194
|
archive_and_purge
|
|
855
1195
|
log ""
|
|
856
|
-
|
|
1196
|
+
if [ "${UNBACKED:-0}" -gt 0 ]; then
|
|
1197
|
+
log "Uninstalled: the zsh hook, state, backups, cache, and the managed venv — and every"
|
|
1198
|
+
log "deployed file EXCEPT the $UNBACKED named above, which had no recoverable copy."
|
|
1199
|
+
else
|
|
1200
|
+
log "Uninstalled: deployed files, the zsh hook, state, backups, cache, and the managed venv."
|
|
1201
|
+
fi
|
|
1202
|
+
if [ "${CLEANUP_FAILED:-0}" = 1 ]; then
|
|
1203
|
+
log ""
|
|
1204
|
+
log "NOT removed: the registrations and instruction region this repo merged into files you"
|
|
1205
|
+
log "own — see the warning above. Re-run uninstall once that is resolved, or remove the"
|
|
1206
|
+
log "agent-bios entries from settings.json and the marked AGENTS.md region by hand."
|
|
1207
|
+
return 1
|
|
1208
|
+
fi
|
|
857
1209
|
}
|
|
858
1210
|
|
|
859
1211
|
# Uninstall is a SECURITY operation — nothing of ours may survive it on the machine. That
|
|
@@ -915,13 +1267,31 @@ archive_and_purge() {
|
|
|
915
1267
|
while IFS= read -r -d '' rel; do rm -f "$rel" 2>/dev/null; done <"$staged"
|
|
916
1268
|
rm -f "$list" "$staged"
|
|
917
1269
|
rm -rf "$stage"
|
|
918
|
-
|
|
1270
|
+
# The state dir holds the manifest, and the manifest is the only record of what is still
|
|
1271
|
+
# deployed. When cmd_uninstall left a file behind because it could not be backed up,
|
|
1272
|
+
# purging that record here would strand the file unowned — so the same rule that keeps
|
|
1273
|
+
# the file keeps the thing that names it. Everything else goes either way.
|
|
1274
|
+
if [ "${UNBACKED:-0}" -eq 0 ]; then
|
|
1275
|
+
rm -rf "$STATE_DIR"
|
|
1276
|
+
fi
|
|
1277
|
+
rm -rf "$HOME/.cache/agent-launch" \
|
|
919
1278
|
"${AGENT_LAUNCH_VENV:-$HOME/.local/share/agent-launch}"
|
|
920
1279
|
log ""
|
|
921
1280
|
log "Everything removed is in ONE archive: $out"
|
|
922
1281
|
if [ "$skipped" -gt 0 ]; then
|
|
923
1282
|
log "Move it somewhere central or delete it. $skipped backup copy(ies) could not be staged,"
|
|
924
1283
|
log "so they were LEFT ON DISK rather than deleted with nothing to restore them from."
|
|
1284
|
+
elif [ "${UNBACKED:-0}" -gt 0 ]; then
|
|
1285
|
+
# Not "nothing of ours is left": cmd_uninstall said the opposite a moment ago, and two
|
|
1286
|
+
# lines of the same summary disagreeing is how an operator stops reading either.
|
|
1287
|
+
log "Move it somewhere central or delete it. $UNBACKED deployed file(s) and the manifest"
|
|
1288
|
+
log "naming them are still on this machine — re-run uninstall once the copy can be made."
|
|
1289
|
+
elif [ "${CLEANUP_FAILED:-0}" = 1 ]; then
|
|
1290
|
+
# The spans merged into files the user owns are still there — cmd_uninstall says so a
|
|
1291
|
+
# few lines below, and this line saying the opposite in the same summary is how an
|
|
1292
|
+
# operator learns to read neither.
|
|
1293
|
+
log "Move it somewhere central or delete it. What this repo merged into files you own"
|
|
1294
|
+
log "could not be removed — see the warning above."
|
|
925
1295
|
else
|
|
926
1296
|
log "Move it somewhere central or delete it — nothing of ours is left on this machine."
|
|
927
1297
|
fi
|
|
@@ -966,14 +1336,44 @@ cmd_onboard() {
|
|
|
966
1336
|
fi
|
|
967
1337
|
DOMAINS_ARG="$sel"; DOMAINS_SET=1
|
|
968
1338
|
log "selection: ${sel:-<core+infra only>}"
|
|
969
|
-
|
|
1339
|
+
# Subshelled so a failing install can still record its outcome: the launcher's
|
|
1340
|
+
# corpus checklist reads `last_apply` from corpus-status.json, and an exit with
|
|
1341
|
+
# nothing recorded reads as "nothing happened". cmd_install's shell state stays
|
|
1342
|
+
# in the subshell; everything after here uses only top-level globals.
|
|
1343
|
+
local apply_rc=0
|
|
1344
|
+
# NOT `( cmd_install ) || apply_rc=$?`: a command list that tests the subshell
|
|
1345
|
+
# suppresses errexit for everything inside it, so an unguarded failure in
|
|
1346
|
+
# cmd_install would run through to a zero return (probed on bash 3.2/5.x).
|
|
1347
|
+
# Toggling -e around a STANDALONE subshell keeps errexit live inside while the
|
|
1348
|
+
# outer shell survives to record the outcome.
|
|
1349
|
+
set +e
|
|
1350
|
+
( set -e; cmd_install )
|
|
1351
|
+
apply_rc=$?
|
|
1352
|
+
set -e
|
|
1353
|
+
if [ "$apply_rc" -ne 0 ]; then
|
|
1354
|
+
python3 "$REPO/compose/corpus-state.py" record-apply \
|
|
1355
|
+
--requested "$sel" --outcome install_failed >/dev/null 2>&1 || true
|
|
1356
|
+
exit "$apply_rc"
|
|
1357
|
+
fi
|
|
970
1358
|
log ""
|
|
971
1359
|
log "Activation canary (proves the bundle loads in a live session)..."
|
|
972
1360
|
if [ "$DRY_RUN" = 1 ]; then info "[dry-run] skip canary probe"; return; fi
|
|
973
|
-
|
|
1361
|
+
local canary_rc=0
|
|
1362
|
+
bash "$REPO/compose/canary.sh" || canary_rc=$?
|
|
1363
|
+
if [ "$canary_rc" -ne 0 ]; then
|
|
1364
|
+
# rc=1 is a real probe that answered "not loading"; rc=3 is "could not
|
|
1365
|
+
# probe" (no CLI/auth). Both leave the apply unproven, so both record as
|
|
1366
|
+
# canary_failed — the tail carries which, so the panel's loud line does
|
|
1367
|
+
# not send the operator to debug imports over an auth problem.
|
|
1368
|
+
python3 "$REPO/compose/corpus-state.py" record-apply \
|
|
1369
|
+
--requested "$sel" --outcome canary_failed \
|
|
1370
|
+
--error-tail "canary exit $canary_rc$([ "$canary_rc" = 3 ] && printf ' (could not probe)')" \
|
|
1371
|
+
>/dev/null 2>&1 || true
|
|
974
1372
|
log "ONBOARDING INCOMPLETE: the bundle is installed but not loading — fix the cause above and re-run: agent-bios verify"
|
|
975
1373
|
exit 1
|
|
976
|
-
|
|
1374
|
+
fi
|
|
1375
|
+
python3 "$REPO/compose/corpus-state.py" record-apply \
|
|
1376
|
+
--requested "$sel" --outcome applied >/dev/null 2>&1 || true
|
|
977
1377
|
# The prune is authorized by the canary's proof, and cmd_install ran before the canary existed
|
|
978
1378
|
# for this bundle rev — so it kept everything. Now that loading is proven, run it for real.
|
|
979
1379
|
migrate_learnings
|
|
@@ -1053,7 +1453,7 @@ drift_state() { # prints: match | drift | unknown
|
|
|
1053
1453
|
# (bytes no commit describes; parsed via json_true, never byte-matched,
|
|
1054
1454
|
# because json.dump's spacing defeated the compact grep) both make the
|
|
1055
1455
|
# correspondence unprovable. Drift is a PROVEN mismatch and makes verify
|
|
1056
|
-
# fail; branding the unprovable as drift made D-
|
|
1456
|
+
# fail; branding the unprovable as drift made D-20260809-7954fd's accepted install path
|
|
1057
1457
|
# fail its own verification. Clone deployments stay version-compared — git
|
|
1058
1458
|
# itself is their live provenance, and they persist no commit by design.
|
|
1059
1459
|
if [ "$(json_field "$STATE_DIR/version.json" source 2>/dev/null || true)" = "package" ] \
|
|
@@ -1106,7 +1506,7 @@ cmd_status() {
|
|
|
1106
1506
|
info "UNBOUND provenance.json is unreadable or names no commit — this artifact is not usably bound"
|
|
1107
1507
|
fi
|
|
1108
1508
|
else
|
|
1109
|
-
# The absence is the finding: D-
|
|
1509
|
+
# The absence is the finding: D-20260809-7954fd accepts lifecycle-disabled packs as
|
|
1110
1510
|
# residual BECAUSE this line makes an unbound artifact visible — a silent
|
|
1111
1511
|
# skip here would unmake that decision's premise.
|
|
1112
1512
|
info "UNBOUND no provenance stamp — packed with lifecycle scripts disabled, or a pre-provenance release; this artifact names no commit"
|
|
@@ -1156,6 +1556,8 @@ agent-bios — deploy the Claude/Codex instruction SSOT into $HOME (by copy).
|
|
|
1156
1556
|
stdin; this is what the learn! flow calls, and it
|
|
1157
1557
|
works from any directory, unlike a repo-relative path)
|
|
1158
1558
|
agent-bios status show what is installed and where
|
|
1559
|
+
agent-bios cost the session cost / context meter (session-cost.py), from any
|
|
1560
|
+
directory: agent-bios cost [--context [--budget N]] <transcript>
|
|
1159
1561
|
agent-bios update git pull + reinstall (clone), or print the npm update line
|
|
1160
1562
|
agent-bios uninstall remove deployed files and the zsh hook
|
|
1161
1563
|
agent-bios help
|
|
@@ -1205,6 +1607,16 @@ if [ "$CMD" = "learn" ]; then
|
|
|
1205
1607
|
[ -f "$collector" ] || { log "learn: collector missing at $collector"; exit 1; }
|
|
1206
1608
|
exec python3 "$collector" "$@" <&3
|
|
1207
1609
|
fi
|
|
1610
|
+
# `cost` is the same shape for the same reason: the guides tell an installed user to
|
|
1611
|
+
# measure with session-cost.py, and on a packaged install that file lives inside the
|
|
1612
|
+
# npm package where no PATH reaches it — the instruction resolved to "command not
|
|
1613
|
+
# found" everywhere but a clone. Its arguments (--context, --budget, transcript
|
|
1614
|
+
# paths) are the meter's, so it bypasses the flag parser below.
|
|
1615
|
+
if [ "$CMD" = "cost" ]; then
|
|
1616
|
+
meter="$REPO/session-cost.py"
|
|
1617
|
+
[ -f "$meter" ] || { log "cost: meter missing at $meter"; exit 1; }
|
|
1618
|
+
exec python3 "$meter" "$@" <&3
|
|
1619
|
+
fi
|
|
1208
1620
|
|
|
1209
1621
|
WITH=""
|
|
1210
1622
|
DOMAINS_ARG=""
|