agent-bios 0.9.8 → 0.10.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 +19 -19
- package/README.md +43 -12
- package/claude/CLAUDE.md +5 -41
- package/claude/guides/claude-prompting.md +1 -1
- package/claude/guides/cli-multi-model-workflow.md +22 -4
- package/claude/guides/coding-staged-workflow.md +49 -15
- package/claude/guides/concept-economy.md +187 -0
- package/claude/guides/documentation-hygiene.md +112 -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/review-request.md +9 -7
- package/claude/guides/session-distill-workflow.md +19 -9
- package/claude/guides/tooling-gotchas.md +26 -0
- package/claude/guides/verification-discipline.md +166 -0
- package/claude/hooks/tooling-gotchas-hook.py +329 -12
- package/codex/AGENTS.md +5 -41
- package/codex/guides/claude-prompting.md +1 -1
- package/codex/guides/cli-multi-model-workflow.md +22 -4
- package/codex/guides/coding-staged-workflow.md +49 -15
- package/codex/guides/concept-economy.md +187 -0
- package/codex/guides/documentation-hygiene.md +112 -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/review-request.md +9 -7
- package/codex/guides/session-distill-workflow.md +19 -9
- package/codex/guides/tooling-gotchas.md +26 -0
- package/codex/guides/verification-discipline.md +166 -0
- package/{scripts → compose}/assemble.py +194 -17
- package/{scripts → compose}/canary.sh +14 -5
- package/compose/check-domains.py +1178 -0
- package/{config → compose}/domains.json +11 -44
- package/{scripts → compose}/pkgid.py +8 -1
- package/compose/prune-backups.py +204 -0
- package/{scripts → compose}/register-hooks.py +3 -3
- package/install.sh +1233 -0
- 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 +32 -27
- package/provenance.json +1 -0
- 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-domains.py +0 -296
- package/scripts/check-parity.sh +0 -2003
- package/scripts/install.sh +0 -819
- /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
package/scripts/install.sh
DELETED
|
@@ -1,819 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# agent-bios installer.
|
|
3
|
-
#
|
|
4
|
-
# Deploys the single-source-of-truth (globals, scoped guides, Codex agent
|
|
5
|
-
# templates, Codex wrappers, the launch profile/shell/bin, a managed Textual
|
|
6
|
-
# venv, and the zsh hook) into $HOME by COPY — idempotent, backed up before
|
|
7
|
-
# overwrite, and reversible. Distributed as an npm bin; the actual $HOME
|
|
8
|
-
# deployment is this explicit command (never a postinstall side effect).
|
|
9
|
-
#
|
|
10
|
-
# Usage:
|
|
11
|
-
# agent-bios install deploy into this environment (backs up + verifies)
|
|
12
|
-
# agent-bios verify check the deployed state matches the source
|
|
13
|
-
# agent-bios status show what is installed and where
|
|
14
|
-
# agent-bios update git pull + reinstall (clone), or print the npm update line
|
|
15
|
-
# agent-bios uninstall remove deployed files and the zsh hook
|
|
16
|
-
# agent-bios help
|
|
17
|
-
#
|
|
18
|
-
# Flags: --dry-run (print actions, change nothing).
|
|
19
|
-
# Env overrides: CLAUDE_CONFIG_DIR, CODEX_HOME, AGENT_LAUNCH_VENV, ZDOTDIR.
|
|
20
|
-
set -euo pipefail
|
|
21
|
-
|
|
22
|
-
# This installer is non-interactive: every input arrives as a subcommand, flag,
|
|
23
|
-
# or env var. Detach stdin so no child (the codex-helm dry-run, pip, git) can
|
|
24
|
-
# block forever on an inherited idle stdin — that is what hangs an install under
|
|
25
|
-
# CI, pipes, and background runs, where stdin stays open but never delivers.
|
|
26
|
-
# `learn` is the one subcommand whose payload IS stdin, so keep the caller's on
|
|
27
|
-
# fd 3 first and hand it back only there; every other path still sees /dev/null.
|
|
28
|
-
exec 3<&0 2>/dev/null || exec 3</dev/null # tolerate a caller that closed fd 0
|
|
29
|
-
exec </dev/null
|
|
30
|
-
|
|
31
|
-
# Resolve this script through symlinks before locating the package: npm links the
|
|
32
|
-
# bin into node_modules/.bin and the global bin dir, so $0 is a link and its
|
|
33
|
-
# dirname is the link's directory, not the package. Without this the source tree
|
|
34
|
-
# resolves to the bin dir's parent (e.g. /opt/homebrew) and every deploy fails.
|
|
35
|
-
SOURCE="${BASH_SOURCE[0]}"
|
|
36
|
-
while [ -L "$SOURCE" ]; do
|
|
37
|
-
LINKDIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
38
|
-
SOURCE="$(readlink "$SOURCE")"
|
|
39
|
-
case "$SOURCE" in
|
|
40
|
-
/*) ;;
|
|
41
|
-
*) SOURCE="$LINKDIR/$SOURCE" ;;
|
|
42
|
-
esac
|
|
43
|
-
done
|
|
44
|
-
SELF="$(cd -P "$(dirname "$SOURCE")" && pwd)"
|
|
45
|
-
REPO="$(cd "$SELF/.." && pwd)"
|
|
46
|
-
|
|
47
|
-
CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
|
|
48
|
-
CODEX_DIR="${CODEX_HOME:-$HOME/.codex}"
|
|
49
|
-
LAUNCH_DIR="$HOME/.config/agent-launch"
|
|
50
|
-
USER_PRESETS_NAME="presets.local.toml" # user-owned; never deployed or verified
|
|
51
|
-
BIN_DIR="$HOME/.local/bin"
|
|
52
|
-
STATE_DIR="$HOME/.local/share/agent-bios"
|
|
53
|
-
LEGACY_STATE_DIR="$HOME/.local/share/agent-dotfiles" # pre-rename state; migrated on first run
|
|
54
|
-
MANIFEST="$STATE_DIR/manifest.txt"
|
|
55
|
-
ZSHRC="${ZDOTDIR:-$HOME}/.zshrc"
|
|
56
|
-
ZSH_HOOK='[ -r "$HOME/.config/agent-launch/shell.zsh" ] && source "$HOME/.config/agent-launch/shell.zsh"'
|
|
57
|
-
HOOK_MARK='agent-launch/shell.zsh'
|
|
58
|
-
|
|
59
|
-
DRY_RUN=0
|
|
60
|
-
BACKUP_DIR=""
|
|
61
|
-
|
|
62
|
-
log() { printf '%s\n' "$*"; }
|
|
63
|
-
info() { printf ' %s\n' "$*"; }
|
|
64
|
-
run() { if [ "$DRY_RUN" = 1 ]; then printf ' [dry-run] %s\n' "$*"; else "$@"; fi; }
|
|
65
|
-
|
|
66
|
-
# ---- prerequisites -------------------------------------------------------
|
|
67
|
-
check_prereqs() {
|
|
68
|
-
local ok=0
|
|
69
|
-
command -v git >/dev/null 2>&1 || { log "missing prerequisite: git"; ok=1; }
|
|
70
|
-
command -v python3 >/dev/null 2>&1 || { log "missing prerequisite: python3"; ok=1; }
|
|
71
|
-
if command -v python3 >/dev/null 2>&1; then
|
|
72
|
-
python3 -c 'import sys; sys.exit(0 if sys.version_info>=(3,11) else 1)' 2>/dev/null \
|
|
73
|
-
|| { log "python3 >= 3.11 required (tomllib)"; ok=1; }
|
|
74
|
-
fi
|
|
75
|
-
command -v zsh >/dev/null 2>&1 || log "note: zsh not found; the shell hook targets zsh"
|
|
76
|
-
command -v codex >/dev/null 2>&1 || log "note: codex CLI not found; Codex-side steps will be skipped"
|
|
77
|
-
command -v claude >/dev/null 2>&1 || log "note: claude CLI not found"
|
|
78
|
-
return $ok
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
# ---- copy with backup + manifest ----------------------------------------
|
|
82
|
-
deploy_file() {
|
|
83
|
-
local src="$1" dst="$2" mode="${3:-}"
|
|
84
|
-
[ -f "$src" ] || { log "source missing: $src"; return 1; }
|
|
85
|
-
if [ -f "$dst" ] && cmp -s "$src" "$dst"; then
|
|
86
|
-
info "unchanged $dst"
|
|
87
|
-
else
|
|
88
|
-
if [ -f "$dst" ] && [ -n "$BACKUP_DIR" ]; then
|
|
89
|
-
run mkdir -p "$(dirname "$BACKUP_DIR$dst")"
|
|
90
|
-
run cp "$dst" "$BACKUP_DIR$dst"
|
|
91
|
-
fi
|
|
92
|
-
run mkdir -p "$(dirname "$dst")"
|
|
93
|
-
run cp "$src" "$dst"
|
|
94
|
-
if [ -n "$mode" ]; then run chmod "$mode" "$dst"; fi
|
|
95
|
-
[ "$DRY_RUN" = 1 ] || info "installed $dst"
|
|
96
|
-
fi
|
|
97
|
-
[ "$DRY_RUN" = 1 ] || printf '%s\n' "$dst" >> "$MANIFEST"
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
deploy_glob() {
|
|
101
|
-
local srcdir="$1" pat="$2" dstdir="$3" mode="${4:-}" f
|
|
102
|
-
for f in "$srcdir"/$pat; do
|
|
103
|
-
[ -f "$f" ] || continue
|
|
104
|
-
deploy_file "$f" "$dstdir/$(basename "$f")" "$mode"
|
|
105
|
-
done
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
# ---- packaged (domain-selected) corpus deploy ----------------------------
|
|
109
|
-
# Active when --domains is passed or a prior selection exists in STATE_DIR.
|
|
110
|
-
# assemble.py owns the corpus surfaces (central tree, entry seeding, codex
|
|
111
|
-
# marker region, settings merge); the entry CLAUDE.md and AGENTS.md are NOT
|
|
112
|
-
# manifested — the entry is user-owned after seeding, AGENTS.md holds a
|
|
113
|
-
# personal region — so uninstall never deletes them.
|
|
114
|
-
# Full mode never runs the assembler, so nothing registered the hooks it
|
|
115
|
-
# deployed — the files landed and never fired. Reuse the assembler's own merge
|
|
116
|
-
# so both install paths register identically, under one ownership rule.
|
|
117
|
-
register_hooks_full() {
|
|
118
|
-
[ "$DRY_RUN" = 1 ] && { info "[dry-run] register central hooks"; return 0; }
|
|
119
|
-
python3 "$REPO/scripts/register-hooks.py" "$REPO" "$CLAUDE_DIR" \
|
|
120
|
-
|| log "note: hook registration failed; the deployed hooks will not fire"
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
packaged_mode() { [ "${DOMAINS_SET:-0}" = 1 ] || [ -f "$STATE_DIR/selection.json" ]; }
|
|
124
|
-
|
|
125
|
-
assemble_packaged() {
|
|
126
|
-
local args=(--claude-dir "$CLAUDE_DIR" --codex-dir "$CODEX_DIR" --state-dir "$STATE_DIR") rc=0
|
|
127
|
-
[ "${DOMAINS_SET:-0}" = 1 ] && args+=(--domains "$DOMAINS_ARG")
|
|
128
|
-
[ "$DRY_RUN" = 1 ] && args+=(--dry-run)
|
|
129
|
-
python3 "$REPO/scripts/assemble.py" "${args[@]}" || rc=$?
|
|
130
|
-
if [ "$rc" = 2 ]; then
|
|
131
|
-
log "packaged: entry file needs user action (import line missing); central content will not load until it is added"
|
|
132
|
-
elif [ "$rc" != 0 ]; then
|
|
133
|
-
return 1
|
|
134
|
-
fi
|
|
135
|
-
if [ "$DRY_RUN" != 1 ]; then
|
|
136
|
-
find "$CLAUDE_DIR/central" "$CODEX_DIR/guides" -type f 2>/dev/null >> "$MANIFEST"
|
|
137
|
-
fi
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
add_zsh_hook() {
|
|
141
|
-
if [ -f "$ZSHRC" ] && grep -qF "$HOOK_MARK" "$ZSHRC"; then
|
|
142
|
-
info "zsh hook present $ZSHRC"
|
|
143
|
-
return
|
|
144
|
-
fi
|
|
145
|
-
if [ "$DRY_RUN" = 1 ]; then info "[dry-run] append zsh hook to $ZSHRC"; return; fi
|
|
146
|
-
printf '%s\n' "$ZSH_HOOK" >> "$ZSHRC"
|
|
147
|
-
info "added zsh hook $ZSHRC"
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
# Carry state written under the pre-rename directory so an existing install keeps
|
|
151
|
-
# its manifest and backups instead of stranding them.
|
|
152
|
-
migrate_state() {
|
|
153
|
-
if [ -d "$STATE_DIR" ] || [ ! -d "$LEGACY_STATE_DIR" ]; then
|
|
154
|
-
return
|
|
155
|
-
fi
|
|
156
|
-
if [ "$DRY_RUN" = 1 ]; then
|
|
157
|
-
info "[dry-run] migrate state $LEGACY_STATE_DIR -> $STATE_DIR"
|
|
158
|
-
return
|
|
159
|
-
fi
|
|
160
|
-
mkdir -p "$(dirname "$STATE_DIR")"
|
|
161
|
-
mv "$LEGACY_STATE_DIR" "$STATE_DIR" && info "migrated state $LEGACY_STATE_DIR -> $STATE_DIR"
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
# ---- codex live-config additions -----------------------------------------
|
|
165
|
-
# The live ~/.codex/config.toml is user/runtime-owned; agent-bios never
|
|
166
|
-
# deploys or overwrites it. codex/config-additions.toml declares the only
|
|
167
|
-
# content agent-bios manages there — one marked [agents.*] block plus a tagged
|
|
168
|
-
# features.multi_agent line — and this helper merges (install), checks
|
|
169
|
-
# (verify), or removes (uninstall) exactly that content, backed up and
|
|
170
|
-
# tomllib-validated before any write. Modes: merge | check | remove.
|
|
171
|
-
codex_config_additions() {
|
|
172
|
-
AB_MODE="$1" AB_CODEX_DIR="$CODEX_DIR" AB_FRAGMENT="$REPO/codex/config-additions.toml" \
|
|
173
|
-
AB_BACKUP="${BACKUP_DIR:-}" AB_DRY="$DRY_RUN" python3 - <<'PY'
|
|
174
|
-
import os, pathlib, sys, tomllib
|
|
175
|
-
|
|
176
|
-
mode = os.environ["AB_MODE"]
|
|
177
|
-
codex_dir = pathlib.Path(os.environ["AB_CODEX_DIR"])
|
|
178
|
-
fragment_path = pathlib.Path(os.environ["AB_FRAGMENT"])
|
|
179
|
-
backup_root = os.environ.get("AB_BACKUP", "")
|
|
180
|
-
dry = os.environ.get("AB_DRY") == "1"
|
|
181
|
-
target = codex_dir / "config.toml"
|
|
182
|
-
BEGIN = "# >>> agent-bios additions >>>"
|
|
183
|
-
END = "# <<< agent-bios additions <<<"
|
|
184
|
-
TAG = "# agent-bios"
|
|
185
|
-
|
|
186
|
-
def info(msg): print(f" {msg}")
|
|
187
|
-
def fail(msg): print(msg); sys.exit(1)
|
|
188
|
-
|
|
189
|
-
frag_text = fragment_path.read_text().replace("${CODEX_HOME}", str(codex_dir))
|
|
190
|
-
want_agents = tomllib.loads(frag_text)["agents"]
|
|
191
|
-
live_text = target.read_text() if target.is_file() else ""
|
|
192
|
-
try:
|
|
193
|
-
live = tomllib.loads(live_text) if live_text else {}
|
|
194
|
-
except Exception as exc:
|
|
195
|
-
fail(f"live codex config does not parse; not touching it: {target} ({exc})")
|
|
196
|
-
|
|
197
|
-
def state_ok():
|
|
198
|
-
if live.get("features", {}).get("multi_agent") is not True:
|
|
199
|
-
return False
|
|
200
|
-
return all(
|
|
201
|
-
live.get("agents", {}).get(name, {}).get(key) == spec[key]
|
|
202
|
-
for name, spec in want_agents.items()
|
|
203
|
-
for key in ("description", "config_file")
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
if mode == "check":
|
|
207
|
-
problems = []
|
|
208
|
-
if live.get("features", {}).get("multi_agent") is not True:
|
|
209
|
-
problems.append("features.multi_agent is not true")
|
|
210
|
-
for name, spec in want_agents.items():
|
|
211
|
-
if live.get("agents", {}).get(name, {}).get("config_file") != spec["config_file"]:
|
|
212
|
-
problems.append(f"agents.{name}.config_file drifted or missing")
|
|
213
|
-
elif not pathlib.Path(spec["config_file"]).is_file():
|
|
214
|
-
problems.append(f"agents.{name} template missing: {spec['config_file']}")
|
|
215
|
-
if problems:
|
|
216
|
-
fail(f"codex config additions: {'; '.join(problems)} ({target})")
|
|
217
|
-
info("codex config additions OK")
|
|
218
|
-
sys.exit(0)
|
|
219
|
-
|
|
220
|
-
def strip_managed(text):
|
|
221
|
-
out, skipping = [], False
|
|
222
|
-
for line in text.splitlines(keepends=True):
|
|
223
|
-
s = line.strip()
|
|
224
|
-
if s == BEGIN: skipping = True; continue
|
|
225
|
-
if s == END: skipping = False; continue
|
|
226
|
-
if skipping or s.endswith(TAG): continue
|
|
227
|
-
out.append(line)
|
|
228
|
-
return "".join(out)
|
|
229
|
-
|
|
230
|
-
if mode == "remove":
|
|
231
|
-
if not target.is_file():
|
|
232
|
-
sys.exit(0)
|
|
233
|
-
stripped = strip_managed(live_text)
|
|
234
|
-
if stripped == live_text:
|
|
235
|
-
info(f"no agent-bios additions in {target}")
|
|
236
|
-
sys.exit(0)
|
|
237
|
-
try:
|
|
238
|
-
tomllib.loads(stripped)
|
|
239
|
-
except Exception as exc:
|
|
240
|
-
fail(f"refusing removal; result would not parse: {exc}")
|
|
241
|
-
if dry:
|
|
242
|
-
info(f"[dry-run] remove agent-bios additions from {target}")
|
|
243
|
-
sys.exit(0)
|
|
244
|
-
backup = target.with_name(target.name + ".bak-agent-bios-uninstall")
|
|
245
|
-
backup.write_text(live_text)
|
|
246
|
-
target.write_text(stripped)
|
|
247
|
-
info(f"removed additions {target} (backup: {backup.name})")
|
|
248
|
-
sys.exit(0)
|
|
249
|
-
|
|
250
|
-
# mode == merge
|
|
251
|
-
if state_ok():
|
|
252
|
-
info(f"unchanged {target} (additions present)")
|
|
253
|
-
sys.exit(0)
|
|
254
|
-
|
|
255
|
-
# A drifted [agents.<tier>] outside our markers would become a duplicate
|
|
256
|
-
# table if we appended ours; that conflict needs the user, not a clobber.
|
|
257
|
-
base = strip_managed(live_text)
|
|
258
|
-
base_data = tomllib.loads(base) if base.strip() else {}
|
|
259
|
-
clash = [name for name in want_agents if name in base_data.get("agents", {})]
|
|
260
|
-
if clash:
|
|
261
|
-
fail(
|
|
262
|
-
f"unmanaged [agents.{'/'.join(clash)}] with drifted content in {target}; "
|
|
263
|
-
"align or remove them, then rerun install"
|
|
264
|
-
)
|
|
265
|
-
|
|
266
|
-
block_lines = [BEGIN]
|
|
267
|
-
if "features" not in base_data:
|
|
268
|
-
block_lines += ["[features]", f"multi_agent = true {TAG}"]
|
|
269
|
-
for name, spec in want_agents.items():
|
|
270
|
-
block_lines += [
|
|
271
|
-
f"[agents.{name}]",
|
|
272
|
-
f'description = "{spec["description"]}"',
|
|
273
|
-
f'config_file = "{spec["config_file"]}"',
|
|
274
|
-
]
|
|
275
|
-
block_lines.append(END)
|
|
276
|
-
block = "\n".join(block_lines) + "\n"
|
|
277
|
-
|
|
278
|
-
new_text = base
|
|
279
|
-
if "features" in base_data:
|
|
280
|
-
if base_data["features"].get("multi_agent") is None:
|
|
281
|
-
lines = new_text.splitlines(keepends=True)
|
|
282
|
-
for i, line in enumerate(lines):
|
|
283
|
-
if line.strip() == "[features]":
|
|
284
|
-
lines.insert(i + 1, f"multi_agent = true {TAG}\n")
|
|
285
|
-
break
|
|
286
|
-
new_text = "".join(lines)
|
|
287
|
-
elif base_data["features"].get("multi_agent") is not True:
|
|
288
|
-
info(f"note: features.multi_agent explicitly set in {target}; leaving it")
|
|
289
|
-
if new_text and not new_text.endswith("\n"):
|
|
290
|
-
new_text += "\n"
|
|
291
|
-
new_text += ("\n" if new_text else "") + block
|
|
292
|
-
|
|
293
|
-
try:
|
|
294
|
-
tomllib.loads(new_text)
|
|
295
|
-
except Exception as exc:
|
|
296
|
-
fail(f"merge result would not parse; live config untouched ({exc})")
|
|
297
|
-
if dry:
|
|
298
|
-
info(f"[dry-run] merge agent-bios additions into {target}")
|
|
299
|
-
sys.exit(0)
|
|
300
|
-
if live_text and backup_root:
|
|
301
|
-
bpath = pathlib.Path(backup_root + str(target))
|
|
302
|
-
bpath.parent.mkdir(parents=True, exist_ok=True)
|
|
303
|
-
bpath.write_text(live_text)
|
|
304
|
-
target.parent.mkdir(parents=True, exist_ok=True)
|
|
305
|
-
target.write_text(new_text)
|
|
306
|
-
info(f"merged additions {target}")
|
|
307
|
-
PY
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
# ---- optional dependencies -----------------------------------------------
|
|
311
|
-
# Capabilities are optional: a missing one only degrades the review routes that
|
|
312
|
-
# need it. config/agent-launch.toml is the single source for both the command
|
|
313
|
-
# that gates a route and the install line offered here.
|
|
314
|
-
capability_table() {
|
|
315
|
-
python3 - "$REPO/config/agent-launch.toml" <<'PY'
|
|
316
|
-
import sys, tomllib
|
|
317
|
-
for name, cap in tomllib.load(open(sys.argv[1], "rb")).get("capabilities", {}).items():
|
|
318
|
-
print("\t".join((name, cap.get("command", ""), cap.get("install", ""))))
|
|
319
|
-
PY
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
install_capability() {
|
|
323
|
-
local name="$1" line="$2"
|
|
324
|
-
log "Installing optional dependency $name: $line"
|
|
325
|
-
if [ "$DRY_RUN" = 1 ]; then info "[dry-run] $line"; return 0; fi
|
|
326
|
-
if sh -c "$line"; then info "installed $name"; else log "warning: installing $name failed; routes needing it stay degraded"; fi
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
handle_capabilities() {
|
|
330
|
-
local requested="$1" name command line
|
|
331
|
-
# Fail on a typo rather than silently installing nothing.
|
|
332
|
-
local known; known=$(capability_table | cut -f1)
|
|
333
|
-
local want
|
|
334
|
-
for want in ${requested//,/ }; do
|
|
335
|
-
printf '%s\n' "$known" | grep -qx "$want" || {
|
|
336
|
-
log "unknown --with capability: $want (known: $(printf '%s' "$known" | tr '\n' ' '))"; return 1; }
|
|
337
|
-
done
|
|
338
|
-
while IFS=$'\t' read -r name command line; do
|
|
339
|
-
[ -n "$name" ] || continue
|
|
340
|
-
if command -v "$command" >/dev/null 2>&1; then
|
|
341
|
-
info "capability present $name ($command)"
|
|
342
|
-
continue
|
|
343
|
-
fi
|
|
344
|
-
if printf '%s\n' "${requested//,/ }" | tr ' ' '\n' | grep -qx "$name"; then
|
|
345
|
-
[ -n "$line" ] && install_capability "$name" "$line" \
|
|
346
|
-
|| log "note: $name has no configured install line"
|
|
347
|
-
elif [ -n "$line" ] && [ -t 0 ] && [ "$DRY_RUN" != 1 ]; then
|
|
348
|
-
printf ' Install optional dependency %s? (%s) [y/N] ' "$name" "$line"
|
|
349
|
-
local answer=""; read -r answer </dev/tty || answer=""
|
|
350
|
-
case "$answer" in
|
|
351
|
-
[yY]*) install_capability "$name" "$line" ;;
|
|
352
|
-
*) info "skipped $name — install later: $line" ;;
|
|
353
|
-
esac
|
|
354
|
-
else
|
|
355
|
-
info "optional $name unavailable; routes needing it degrade${line:+ — install: $line}"
|
|
356
|
-
fi
|
|
357
|
-
done <<EOF
|
|
358
|
-
$(capability_table)
|
|
359
|
-
EOF
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
# Presets the launcher saved into the deployed profiles.toml (pre-split, or by hand)
|
|
363
|
-
# would be lost to the cp below; move them into the user-owned presets file first.
|
|
364
|
-
migrate_user_presets() {
|
|
365
|
-
local src="$REPO/config/agent-launch.toml"
|
|
366
|
-
local dst="$LAUNCH_DIR/profiles.toml"
|
|
367
|
-
local user="$LAUNCH_DIR/$USER_PRESETS_NAME"
|
|
368
|
-
[ -f "$dst" ] || return 0
|
|
369
|
-
python3 - "$src" "$dst" "$user" "$DRY_RUN" <<'PY' || { log "user preset migration failed; not overwriting $LAUNCH_DIR/profiles.toml"; return 1; }
|
|
370
|
-
import os, pathlib, re, sys, tomllib
|
|
371
|
-
|
|
372
|
-
src, dst, user, dry_run = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2]), pathlib.Path(sys.argv[3]), sys.argv[4] == "1"
|
|
373
|
-
dst_text = dst.read_text()
|
|
374
|
-
shipped = set(tomllib.loads(src.read_text()).get("presets", {}))
|
|
375
|
-
extra = set(tomllib.loads(dst_text).get("presets", {})) - shipped
|
|
376
|
-
if not extra:
|
|
377
|
-
sys.exit(0)
|
|
378
|
-
existing_text = user.read_text() if user.is_file() else ""
|
|
379
|
-
already = set(tomllib.loads(existing_text).get("presets", {})) if existing_text else set()
|
|
380
|
-
move = sorted(extra - already)
|
|
381
|
-
for name in sorted(extra & already):
|
|
382
|
-
print(f" kept in {user.name} {name} (already saved there)")
|
|
383
|
-
if not move:
|
|
384
|
-
sys.exit(0)
|
|
385
|
-
|
|
386
|
-
header = re.compile(r'^\[presets\.(?:"([^"]+)"|([A-Za-z0-9][A-Za-z0-9_-]*))(?:[.\]])')
|
|
387
|
-
blocks, current = {}, None
|
|
388
|
-
for line in dst_text.splitlines(keepends=True):
|
|
389
|
-
stripped = line.strip()
|
|
390
|
-
if stripped.startswith("["):
|
|
391
|
-
found = header.match(stripped)
|
|
392
|
-
current = (found.group(1) or found.group(2)) if found else None
|
|
393
|
-
if current in move:
|
|
394
|
-
blocks.setdefault(current, []).append(line)
|
|
395
|
-
# Refuse to deploy over a preset we could not carry across, rather than drop it.
|
|
396
|
-
missing = [name for name in move if name not in blocks]
|
|
397
|
-
if missing:
|
|
398
|
-
sys.exit(f"cannot extract preset block(s) from {dst}: {', '.join(missing)}")
|
|
399
|
-
|
|
400
|
-
out = existing_text.rstrip("\n") or (
|
|
401
|
-
"# agent-launch user presets, moved out of profiles.toml by agent-bios install.\n"
|
|
402
|
-
"# The installer never deploys or verifies this file, so presets here survive upgrades."
|
|
403
|
-
)
|
|
404
|
-
for name in move:
|
|
405
|
-
out += "\n\n" + "".join(blocks[name]).strip("\n")
|
|
406
|
-
for name in move:
|
|
407
|
-
print(f" {'[dry-run] ' if dry_run else ''}moved preset {name} -> {user}")
|
|
408
|
-
if dry_run:
|
|
409
|
-
sys.exit(0)
|
|
410
|
-
user.parent.mkdir(parents=True, exist_ok=True)
|
|
411
|
-
temporary = user.with_name(f".{user.name}.{os.getpid()}.tmp")
|
|
412
|
-
temporary.write_text(out + "\n")
|
|
413
|
-
os.replace(temporary, user)
|
|
414
|
-
PY
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
remove_zsh_hook() {
|
|
418
|
-
if [ ! -f "$ZSHRC" ] || ! grep -qF "$HOOK_MARK" "$ZSHRC"; then
|
|
419
|
-
info "no zsh hook to remove"
|
|
420
|
-
return
|
|
421
|
-
fi
|
|
422
|
-
if [ "$DRY_RUN" = 1 ]; then info "[dry-run] remove zsh hook from $ZSHRC"; return; fi
|
|
423
|
-
grep -vF "$HOOK_MARK" "$ZSHRC" > "$ZSHRC.agent-tmp" && mv "$ZSHRC.agent-tmp" "$ZSHRC"
|
|
424
|
-
info "removed zsh hook $ZSHRC"
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
# Promote -> migrate (collection loop, Phase 4): after the corpus is deployed,
|
|
428
|
-
# clear personal copies of learnings that have been promoted into the shared
|
|
429
|
-
# corpus AND are in this user's assembled bundle. Best-effort: a prune failure
|
|
430
|
-
# (or an absent manifest/script) never fails the install. Runs per host.
|
|
431
|
-
migrate_learnings() {
|
|
432
|
-
local script="$REPO/scripts/migrate-learnings.py"
|
|
433
|
-
{ [ -f "$script" ] && [ -f "$REPO/config/promotions.json" ]; } || return 0
|
|
434
|
-
local -a sel dry
|
|
435
|
-
if packaged_mode; then sel=(--selection-file "$STATE_DIR/selection.json"); else sel=(--full); fi
|
|
436
|
-
[ "$DRY_RUN" = 1 ] && dry=(--dry-run) || dry=()
|
|
437
|
-
# ${dry[@]+...}: expanding an empty array as "${dry[@]}" is an unbound-variable
|
|
438
|
-
# error under `set -u` on bash 3.2 (macOS default) and would abort the install.
|
|
439
|
-
python3 "$script" --host claude --config-dir "$CLAUDE_DIR" "${sel[@]}" ${dry[@]+"${dry[@]}"} \
|
|
440
|
-
|| info "learnings migrate (claude) skipped"
|
|
441
|
-
python3 "$script" --host codex --config-dir "$CODEX_DIR" "${sel[@]}" ${dry[@]+"${dry[@]}"} \
|
|
442
|
-
|| info "learnings migrate (codex) skipped"
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
# ---- subcommands ---------------------------------------------------------
|
|
446
|
-
cmd_install() {
|
|
447
|
-
check_prereqs || { log "resolve the prerequisites above and retry"; exit 1; }
|
|
448
|
-
migrate_state
|
|
449
|
-
if [ "$DRY_RUN" != 1 ]; then
|
|
450
|
-
mkdir -p "$STATE_DIR"
|
|
451
|
-
BACKUP_DIR="$STATE_DIR/backups/$(date +%Y%m%d-%H%M%S)"
|
|
452
|
-
: > "$MANIFEST"
|
|
453
|
-
fi
|
|
454
|
-
log "Deploying agent-bios from $REPO"
|
|
455
|
-
if packaged_mode; then
|
|
456
|
-
log "packaged mode: assembling selected domains (state: $STATE_DIR/selection.json)"
|
|
457
|
-
assemble_packaged || exit 1
|
|
458
|
-
else
|
|
459
|
-
deploy_file "$REPO/claude/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
|
|
460
|
-
deploy_glob "$REPO/claude/guides" "*.md" "$CLAUDE_DIR/guides"
|
|
461
|
-
deploy_glob "$REPO/claude/agents" "*.md" "$CLAUDE_DIR/agents"
|
|
462
|
-
# Our hooks go under central/: <claude>/hooks is shared with other tools
|
|
463
|
-
# files, caches, and state, and we must not own a directory we share.
|
|
464
|
-
deploy_glob "$REPO/claude/hooks" "*.py" "$CLAUDE_DIR/central/hooks" "+x"
|
|
465
|
-
register_hooks_full
|
|
466
|
-
deploy_file "$REPO/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md"
|
|
467
|
-
deploy_glob "$REPO/codex/guides" "*.md" "$CODEX_DIR/guides"
|
|
468
|
-
fi
|
|
469
|
-
migrate_learnings # Phase 4: clear personal copies now absorbed by the corpus
|
|
470
|
-
deploy_glob "$REPO/codex/agents" "*.toml" "$CODEX_DIR/agents"
|
|
471
|
-
codex_config_additions merge || exit 1
|
|
472
|
-
deploy_file "$REPO/scripts/codex-run.sh" "$CODEX_DIR/bin/codex-run" "+x"
|
|
473
|
-
deploy_file "$REPO/scripts/codex-helm.sh" "$CODEX_DIR/bin/codex-helm" "+x"
|
|
474
|
-
migrate_user_presets || exit 1 # must precede the deploy below, which overwrites profiles.toml
|
|
475
|
-
deploy_file "$REPO/config/agent-launch.toml" "$LAUNCH_DIR/profiles.toml"
|
|
476
|
-
deploy_file "$REPO/shell/agent-launch.zsh" "$LAUNCH_DIR/shell.zsh"
|
|
477
|
-
deploy_file "$REPO/scripts/agent-launch.py" "$BIN_DIR/agent-launch" "+x"
|
|
478
|
-
log ""
|
|
479
|
-
log "Optional dependencies (missing ones only degrade the routes that need them)..."
|
|
480
|
-
handle_capabilities "$WITH" || exit 1
|
|
481
|
-
log ""
|
|
482
|
-
if [ "$DRY_RUN" = 1 ]; then
|
|
483
|
-
info "[dry-run] provision managed Textual venv"
|
|
484
|
-
else
|
|
485
|
-
bash "$REPO/scripts/provision-venv.sh" && info "managed venv OK" \
|
|
486
|
-
|| log "warning: venv provisioning failed (numbered-prompt fallback applies)"
|
|
487
|
-
fi
|
|
488
|
-
add_zsh_hook
|
|
489
|
-
if python3 "$REPO/scripts/session-distill/corpus-state.py" project --repo "$REPO" >/dev/null 2>&1; then
|
|
490
|
-
info "corpus-status projected"
|
|
491
|
-
else
|
|
492
|
-
log "note: corpus-status projection unavailable (versions.json/ledger missing?)"
|
|
493
|
-
fi
|
|
494
|
-
# Deploy/system version marker for the launcher's TUI version line, read from
|
|
495
|
-
# package.json (version + releaseDate) — distinct from the corpus content
|
|
496
|
-
# version. Best-effort: a failure here never fails the install.
|
|
497
|
-
if [ "$DRY_RUN" != 1 ]; then
|
|
498
|
-
if python3 - "$REPO/package.json" "$STATE_DIR/version.json" <<'PY' 2>/dev/null
|
|
499
|
-
import json, sys
|
|
500
|
-
pkg = json.load(open(sys.argv[1]))
|
|
501
|
-
with open(sys.argv[2], "w") as f:
|
|
502
|
-
json.dump({"version": pkg.get("version"), "releaseDate": pkg.get("releaseDate")}, f)
|
|
503
|
-
f.write("\n")
|
|
504
|
-
PY
|
|
505
|
-
then
|
|
506
|
-
printf '%s\n' "$STATE_DIR/version.json" >> "$MANIFEST"
|
|
507
|
-
info "version marker written ($STATE_DIR/version.json)"
|
|
508
|
-
else
|
|
509
|
-
log "note: version marker not written (package.json unreadable)"
|
|
510
|
-
fi
|
|
511
|
-
fi
|
|
512
|
-
log ""
|
|
513
|
-
log "Verifying deployment..."
|
|
514
|
-
if cmd_verify; then
|
|
515
|
-
log ""
|
|
516
|
-
log "Done. Open a new shell (or: source \"$ZSHRC\") to activate the zero-arg launcher."
|
|
517
|
-
# An untouched backup dir means nothing was replaced; that healthy state
|
|
518
|
-
# must not become a nonzero exit under set -e.
|
|
519
|
-
{ [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ] && log "Replaced files were backed up under $BACKUP_DIR"; } || true
|
|
520
|
-
else
|
|
521
|
-
log "VERIFY FAILED after install — see messages above"
|
|
522
|
-
exit 1
|
|
523
|
-
fi
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
verify_match() { if cmp -s "$1" "$2"; then info "match $2"; else log "MISMATCH/absent $2"; return 1; fi; }
|
|
527
|
-
verify_present() { if [ -f "$1" ]; then return 0; else log "missing $1"; return 1; fi; }
|
|
528
|
-
|
|
529
|
-
cmd_verify() {
|
|
530
|
-
local fail=0 gp
|
|
531
|
-
if [ ! -d "$REPO/.git" ] && [ "$(drift_state)" = "drift" ]; then
|
|
532
|
-
log "deploy drift: deployed $(deployed_version), package $(source_version) — run: agent-bios install"
|
|
533
|
-
fail=1
|
|
534
|
-
fi
|
|
535
|
-
if packaged_mode; then
|
|
536
|
-
# Packaged: corpus surfaces are selection-derived, not repo-identical.
|
|
537
|
-
# The entry file is user-owned — READ-check the import line, never rewrite.
|
|
538
|
-
python3 "$REPO/scripts/check-domains.py" >/dev/null 2>&1 && info "domains gate OK" || { log "domains gate FAILED"; fail=1; }
|
|
539
|
-
verify_present "$CLAUDE_DIR/central/bundle.md" || fail=1
|
|
540
|
-
if grep -qF '@central/bundle.md' "$CLAUDE_DIR/CLAUDE.md" 2>/dev/null; then
|
|
541
|
-
info "entry import line present"
|
|
542
|
-
else
|
|
543
|
-
log "entry $CLAUDE_DIR/CLAUDE.md lacks '@central/bundle.md' — central corpus is NOT loading"; fail=1
|
|
544
|
-
fi
|
|
545
|
-
if grep -qF 'agent-bios:central:start' "$CODEX_DIR/AGENTS.md" 2>/dev/null; then
|
|
546
|
-
info "codex central region present"
|
|
547
|
-
else
|
|
548
|
-
log "codex AGENTS.md central region missing"; fail=1
|
|
549
|
-
fi
|
|
550
|
-
else
|
|
551
|
-
verify_match "$REPO/claude/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md" || fail=1
|
|
552
|
-
verify_match "$REPO/codex/AGENTS.md" "$CODEX_DIR/AGENTS.md" || fail=1
|
|
553
|
-
for gp in "$REPO"/claude/guides/*.md; do verify_present "$CLAUDE_DIR/guides/$(basename "$gp")" || fail=1; done
|
|
554
|
-
for gp in "$REPO"/claude/agents/*.md; do verify_present "$CLAUDE_DIR/agents/$(basename "$gp")" || fail=1; done
|
|
555
|
-
for gp in "$REPO"/claude/hooks/*.py; do verify_present "$CLAUDE_DIR/central/hooks/$(basename "$gp")" || fail=1; done
|
|
556
|
-
for gp in "$REPO"/codex/guides/*.md; do verify_present "$CODEX_DIR/guides/$(basename "$gp")" || fail=1; done
|
|
557
|
-
fi
|
|
558
|
-
verify_match "$REPO/scripts/agent-launch.py" "$BIN_DIR/agent-launch" || fail=1
|
|
559
|
-
verify_match "$REPO/config/agent-launch.toml" "$LAUNCH_DIR/profiles.toml" || fail=1
|
|
560
|
-
verify_match "$REPO/shell/agent-launch.zsh" "$LAUNCH_DIR/shell.zsh" || fail=1
|
|
561
|
-
python3 - "$CODEX_DIR/agents" <<'PY' && info "agent TOMLs OK" || fail=1
|
|
562
|
-
import sys, pathlib, tomllib
|
|
563
|
-
root = pathlib.Path(sys.argv[1])
|
|
564
|
-
required = {"frontier.toml", "workhorse.toml", "sweep.toml", "reviewer.toml"}
|
|
565
|
-
missing = required - {p.name for p in root.glob("*.toml")}
|
|
566
|
-
assert not missing, f"missing agent TOMLs in {root}: {sorted(missing)}"
|
|
567
|
-
for p in sorted(root.glob("*.toml")):
|
|
568
|
-
tomllib.loads(p.read_text())
|
|
569
|
-
PY
|
|
570
|
-
codex_config_additions check || fail=1
|
|
571
|
-
if command -v codex >/dev/null 2>&1 && [ -x "$CODEX_DIR/bin/codex-helm" ]; then
|
|
572
|
-
if "$CODEX_DIR/bin/codex-helm" --dry-run --mode review "probe" >/dev/null 2>&1; then
|
|
573
|
-
info "codex-helm dry-run OK"
|
|
574
|
-
else
|
|
575
|
-
log "codex-helm dry-run FAILED"; fail=1
|
|
576
|
-
fi
|
|
577
|
-
fi
|
|
578
|
-
local vpy="${AGENT_LAUNCH_VENV:-$HOME/.local/share/agent-launch/venv}/bin/python"
|
|
579
|
-
if [ -x "$vpy" ] && "$vpy" -c 'import textual' 2>/dev/null; then
|
|
580
|
-
info "managed venv (textual) OK"
|
|
581
|
-
else
|
|
582
|
-
log "note: managed venv/textual unavailable (numbered-prompt fallback applies)"
|
|
583
|
-
fi
|
|
584
|
-
# A file this installer executes but never ships is invisible from a clone and
|
|
585
|
-
# fatal on npm, so the payload gate runs wherever it exists (maintainer-side).
|
|
586
|
-
if [ -x "$REPO/scripts/check-package.sh" ]; then
|
|
587
|
-
if "$REPO/scripts/check-package.sh" >/dev/null 2>&1; then
|
|
588
|
-
info "npm payload OK"
|
|
589
|
-
else
|
|
590
|
-
log "npm payload incomplete; run scripts/check-package.sh"
|
|
591
|
-
fail=1
|
|
592
|
-
fi
|
|
593
|
-
fi
|
|
594
|
-
# Repo-internal mirror parity is a maintainer gate; only meaningful from a clone.
|
|
595
|
-
if [ -d "$REPO/ko" ] && [ -x "$REPO/scripts/check-parity.sh" ]; then
|
|
596
|
-
if "$REPO/scripts/check-parity.sh" >/dev/null 2>&1; then info "repo mirror parity OK"; else log "repo mirror parity FAILED"; fail=1; fi
|
|
597
|
-
fi
|
|
598
|
-
# Prompting guides name concrete models, so they go stale on a model change
|
|
599
|
-
# rather than degrading quietly; this checks them against the launch config.
|
|
600
|
-
if [ -x "$REPO/scripts/check-prompting-targets.sh" ]; then
|
|
601
|
-
if "$REPO/scripts/check-prompting-targets.sh" >/dev/null 2>&1; then
|
|
602
|
-
info "prompting targets OK"
|
|
603
|
-
else
|
|
604
|
-
log "prompting guides do not cover a configured model; run scripts/check-prompting-targets.sh"
|
|
605
|
-
fail=1
|
|
606
|
-
fi
|
|
607
|
-
fi
|
|
608
|
-
return $fail
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
cmd_uninstall() {
|
|
612
|
-
migrate_state
|
|
613
|
-
codex_config_additions remove || log "warning: could not remove codex config additions"
|
|
614
|
-
if [ -f "$MANIFEST" ]; then
|
|
615
|
-
local f
|
|
616
|
-
while IFS= read -r f; do
|
|
617
|
-
[ -n "$f" ] || continue
|
|
618
|
-
[ -f "$f" ] && { run rm -f "$f"; [ "$DRY_RUN" = 1 ] || info "removed $f"; }
|
|
619
|
-
done < "$MANIFEST"
|
|
620
|
-
[ "$DRY_RUN" = 1 ] || rm -f "$MANIFEST"
|
|
621
|
-
else
|
|
622
|
-
log "no manifest at $MANIFEST; removing known deploy targets"
|
|
623
|
-
local p
|
|
624
|
-
for p in "$CLAUDE_DIR/CLAUDE.md" "$CODEX_DIR/AGENTS.md" \
|
|
625
|
-
"$CODEX_DIR/bin/codex-run" "$CODEX_DIR/bin/codex-helm" \
|
|
626
|
-
"$LAUNCH_DIR/profiles.toml" "$LAUNCH_DIR/shell.zsh" "$BIN_DIR/agent-launch"; do
|
|
627
|
-
[ -f "$p" ] && { run rm -f "$p"; [ "$DRY_RUN" = 1 ] || info "removed $p"; }
|
|
628
|
-
done
|
|
629
|
-
fi
|
|
630
|
-
local d
|
|
631
|
-
for d in "$CLAUDE_DIR/guides" "$CLAUDE_DIR/agents" "$CODEX_DIR/guides" "$CODEX_DIR/agents" "$CODEX_DIR/bin" "$LAUNCH_DIR"; do
|
|
632
|
-
[ -d "$d" ] && rmdir "$d" 2>/dev/null && info "removed empty $d" || true
|
|
633
|
-
done
|
|
634
|
-
remove_zsh_hook
|
|
635
|
-
log ""
|
|
636
|
-
log "Uninstalled deployed files and the zsh hook."
|
|
637
|
-
log "Kept: managed venv + backups under $STATE_DIR (rm -rf \"$STATE_DIR\" to purge)."
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
cmd_onboard() {
|
|
641
|
-
log "agent-bios onboarding — pick your domain packages (core + infra always install)"
|
|
642
|
-
local names=() line i=1 choice sel="" n picks
|
|
643
|
-
while IFS= read -r line; do names+=("$line"); done \
|
|
644
|
-
< <(python3 -c "import json;print('\n'.join(sorted(json.load(open('$REPO/config/domains.json'))['domains'])))")
|
|
645
|
-
[ "${#names[@]}" -ge 1 ] || { log "no domains found in config/domains.json"; exit 1; }
|
|
646
|
-
if [ "$DOMAINS_SET" = 1 ]; then
|
|
647
|
-
# Non-interactive path, per this installer's input contract (stdin is
|
|
648
|
-
# detached at the top of the script): selection arrives as domain names.
|
|
649
|
-
[ "$DOMAINS_ARG" = none ] && DOMAINS_ARG=""
|
|
650
|
-
sel="$DOMAINS_ARG"
|
|
651
|
-
elif ( : </dev/tty ) 2>/dev/null; then
|
|
652
|
-
for line in "${names[@]}"; do info "$i) $line"; i=$((i+1)); done
|
|
653
|
-
printf 'Select by number, comma-separated (empty = core+infra only): '
|
|
654
|
-
read -r choice </dev/tty || choice=""
|
|
655
|
-
if [ -n "$choice" ]; then
|
|
656
|
-
# bash 3.2 + set -u: expanding an EMPTY array errors, so split only
|
|
657
|
-
# when there is input; empty input means core+infra only.
|
|
658
|
-
IFS=',' read -ra picks <<<"$choice"
|
|
659
|
-
for n in "${picks[@]}"; do
|
|
660
|
-
n="${n// /}"; [ -n "$n" ] || continue
|
|
661
|
-
case "$n" in (*[!0-9]*) log "invalid selection: $n"; exit 2 ;; esac
|
|
662
|
-
[ "$n" -ge 1 ] && [ "$n" -le "${#names[@]}" ] || { log "selection out of range: $n"; exit 2; }
|
|
663
|
-
sel="$sel${sel:+,}${names[$((n-1))]}"
|
|
664
|
-
done
|
|
665
|
-
fi
|
|
666
|
-
else
|
|
667
|
-
log "onboard needs a terminal or an explicit selection — run:"
|
|
668
|
-
log " agent-bios onboard --domains a,b (or --domains none for core+infra only)"
|
|
669
|
-
exit 2
|
|
670
|
-
fi
|
|
671
|
-
DOMAINS_ARG="$sel"; DOMAINS_SET=1
|
|
672
|
-
log "selection: ${sel:-<core+infra only>}"
|
|
673
|
-
cmd_install
|
|
674
|
-
log ""
|
|
675
|
-
log "Activation canary (proves the bundle loads in a live session)..."
|
|
676
|
-
if [ "$DRY_RUN" = 1 ]; then info "[dry-run] skip canary probe"; return; fi
|
|
677
|
-
bash "$REPO/scripts/canary.sh" || {
|
|
678
|
-
log "ONBOARDING INCOMPLETE: the bundle is installed but not loading — fix the cause above and re-run: agent-bios verify"
|
|
679
|
-
exit 1
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
# ---- deploy-chain drift ---------------------------------------------------
|
|
684
|
-
# A repo edit is inert until it is published AND globally installed AND
|
|
685
|
-
# deployed. The middle two are checkable: the installer stamps the package
|
|
686
|
-
# version it deployed into the state dir, so a stamp older than the package now
|
|
687
|
-
# running means someone updated the package and never re-deployed. Reading the
|
|
688
|
-
# registry cannot see this, which is why it went unnoticed three times.
|
|
689
|
-
json_field() { # $1=file $2=key
|
|
690
|
-
[ -f "$1" ] || return 1
|
|
691
|
-
python3 -c 'import json,sys
|
|
692
|
-
try:
|
|
693
|
-
v=json.load(open(sys.argv[1])).get(sys.argv[2])
|
|
694
|
-
except Exception:
|
|
695
|
-
sys.exit(1)
|
|
696
|
-
sys.exit(0) if v is None else print(v)' "$1" "$2" 2>/dev/null
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
deployed_version() { json_field "$STATE_DIR/version.json" version; }
|
|
700
|
-
source_version() { json_field "$REPO/package.json" version; }
|
|
701
|
-
|
|
702
|
-
drift_state() { # prints: match | drift | unknown
|
|
703
|
-
local d s
|
|
704
|
-
d="$(deployed_version)" || { echo unknown; return; }
|
|
705
|
-
s="$(source_version)" || { echo unknown; return; }
|
|
706
|
-
[ -n "$d" ] && [ -n "$s" ] || { echo unknown; return; }
|
|
707
|
-
[ "$d" = "$s" ] && echo match || echo drift
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
cmd_status() {
|
|
711
|
-
local version p
|
|
712
|
-
if [ -d "$REPO/.git" ]; then
|
|
713
|
-
version="$(git -C "$REPO" describe --tags --always --dirty 2>/dev/null || git -C "$REPO" rev-parse --short HEAD 2>/dev/null || echo '?')"
|
|
714
|
-
log "agent-bios (git clone: $version)"
|
|
715
|
-
elif [ -f "$REPO/package.json" ]; then
|
|
716
|
-
version="$(node -e "try{process.stdout.write(require('$REPO/package.json').version)}catch(e){process.stdout.write('?')}" 2>/dev/null || echo '?')"
|
|
717
|
-
log "agent-bios (npm package: $version)"
|
|
718
|
-
else
|
|
719
|
-
log "agent-bios"
|
|
720
|
-
fi
|
|
721
|
-
log " source: $REPO"
|
|
722
|
-
case "$(drift_state)" in
|
|
723
|
-
match) info "deployed version $(deployed_version) (matches this package)" ;;
|
|
724
|
-
drift) log "DRIFT deployed $(deployed_version) but this package is $(source_version) — run: agent-bios install" ;;
|
|
725
|
-
unknown) info "deployed version unknown (no state marker yet)" ;;
|
|
726
|
-
esac
|
|
727
|
-
for p in "$CLAUDE_DIR/CLAUDE.md" "$CODEX_DIR/AGENTS.md" "$BIN_DIR/agent-launch" \
|
|
728
|
-
"$LAUNCH_DIR/profiles.toml" "$LAUNCH_DIR/shell.zsh"; do
|
|
729
|
-
if [ -e "$p" ]; then info "present $p"; else info "MISSING $p"; fi
|
|
730
|
-
done
|
|
731
|
-
if [ -f "$ZSHRC" ] && grep -qF "$HOOK_MARK" "$ZSHRC"; then info "zsh hook present"; else info "zsh hook absent"; fi
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
cmd_update() {
|
|
735
|
-
if [ -d "$REPO/.git" ]; then
|
|
736
|
-
log "Updating from git..."
|
|
737
|
-
run git -C "$REPO" pull --ff-only
|
|
738
|
-
cmd_install
|
|
739
|
-
else
|
|
740
|
-
log "Installed as an npm package. Update with:"
|
|
741
|
-
log " npm install -g agent-bios@latest && agent-bios install"
|
|
742
|
-
log "Then confirm what actually landed — right after a publish the cached"
|
|
743
|
-
log "packument can serve the PREVIOUS version at exit 0:"
|
|
744
|
-
log " agent-bios status # must show the version you expected, and no DRIFT"
|
|
745
|
-
fi
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
usage() {
|
|
749
|
-
cat <<'EOF'
|
|
750
|
-
agent-bios — deploy the Claude/Codex instruction SSOT into $HOME (by copy).
|
|
751
|
-
|
|
752
|
-
agent-bios install deploy into this environment (backs up + verifies)
|
|
753
|
-
agent-bios onboard interactive domain selection + packaged install + activation canary
|
|
754
|
-
agent-bios verify check the deployed state matches the source
|
|
755
|
-
agent-bios learn submit a session learning (reads the JSON record on
|
|
756
|
-
stdin; this is what the learn! flow calls, and it
|
|
757
|
-
works from any directory, unlike a repo-relative path)
|
|
758
|
-
agent-bios status show what is installed and where
|
|
759
|
-
agent-bios update git pull + reinstall (clone), or print the npm update line
|
|
760
|
-
agent-bios uninstall remove deployed files and the zsh hook
|
|
761
|
-
agent-bios help
|
|
762
|
-
|
|
763
|
-
Flags: --dry-run print actions without changing anything
|
|
764
|
-
--domains a,b packaged mode (install/onboard): assemble ONLY the named
|
|
765
|
-
domain packages (plus core+infra) instead of the full
|
|
766
|
-
corpus; with onboard, 'none' means core+infra only. The
|
|
767
|
-
selection persists in the state dir, so later
|
|
768
|
-
installs/updates stay packaged until the selection file
|
|
769
|
-
is removed. Default (no flag, no saved selection) keeps
|
|
770
|
-
today's full-corpus deploy.
|
|
771
|
-
--with a,b also install the named optional dependencies (install only).
|
|
772
|
-
Without it, install offers each missing one when the terminal
|
|
773
|
-
is interactive, and otherwise just prints its install line.
|
|
774
|
-
Known: onto, ultracode. Missing ones are not fatal — they only
|
|
775
|
-
degrade the review routes that need them.
|
|
776
|
-
Env: CLAUDE_CONFIG_DIR, CODEX_HOME, AGENT_LAUNCH_VENV, ZDOTDIR
|
|
777
|
-
EOF
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
# ---- dispatch ------------------------------------------------------------
|
|
781
|
-
CMD="${1:-help}"
|
|
782
|
-
if [ $# -gt 0 ]; then shift; fi
|
|
783
|
-
|
|
784
|
-
# `learn` forwards its arguments and stdin straight to the collector, so it must
|
|
785
|
-
# bypass the flag parser below (which rejects anything it does not know). This
|
|
786
|
-
# subcommand is the only PATH-reachable entry to capture: the corpus guide used
|
|
787
|
-
# to invoke scripts/collect-learning.py relative to the cwd, which works from a
|
|
788
|
-
# clone and silently fails for every other install.
|
|
789
|
-
if [ "$CMD" = "learn" ]; then
|
|
790
|
-
collector="$REPO/scripts/collect-learning.py"
|
|
791
|
-
[ -f "$collector" ] || { log "learn: collector missing at $collector"; exit 1; }
|
|
792
|
-
exec python3 "$collector" "$@" <&3
|
|
793
|
-
fi
|
|
794
|
-
|
|
795
|
-
WITH=""
|
|
796
|
-
DOMAINS_ARG=""
|
|
797
|
-
DOMAINS_SET=0
|
|
798
|
-
while [ $# -gt 0 ]; do
|
|
799
|
-
case "$1" in
|
|
800
|
-
--dry-run) DRY_RUN=1 ;;
|
|
801
|
-
--with) shift; WITH="${1:-}"; [ -n "$WITH" ] || { log "--with needs a comma-separated capability list"; exit 2; } ;;
|
|
802
|
-
--with=*) WITH="${1#--with=}"; [ -n "$WITH" ] || { log "--with needs a comma-separated capability list"; exit 2; } ;;
|
|
803
|
-
--domains) shift; DOMAINS_ARG="${1:-}"; DOMAINS_SET=1; [ -n "$DOMAINS_ARG" ] || { log "--domains needs a comma-separated domain list (use onboard for core-only)"; exit 2; } ;;
|
|
804
|
-
--domains=*) DOMAINS_ARG="${1#--domains=}"; DOMAINS_SET=1; [ -n "$DOMAINS_ARG" ] || { log "--domains needs a comma-separated domain list (use onboard for core-only)"; exit 2; } ;;
|
|
805
|
-
*) log "unknown flag: $1"; exit 2 ;;
|
|
806
|
-
esac
|
|
807
|
-
shift
|
|
808
|
-
done
|
|
809
|
-
|
|
810
|
-
case "$CMD" in
|
|
811
|
-
install) cmd_install ;;
|
|
812
|
-
onboard) cmd_onboard ;;
|
|
813
|
-
update) cmd_update ;;
|
|
814
|
-
uninstall) cmd_uninstall ;;
|
|
815
|
-
verify) if cmd_verify; then log "VERIFY OK"; else log "VERIFY FAILED"; exit 1; fi ;;
|
|
816
|
-
status) cmd_status ;;
|
|
817
|
-
help|-h|--help) usage ;;
|
|
818
|
-
*) log "unknown command: $CMD"; usage; exit 2 ;;
|
|
819
|
-
esac
|