Skip to content

fix(axi): isolate AXI suite installs per vessel npm prefix - #36

Merged
Freudator86 merged 8 commits into
mainfrom
fm/fm-axi-isolated-prefixes
Jul 28, 2026
Merged

fix(axi): isolate AXI suite installs per vessel npm prefix#36
Freudator86 merged 8 commits into
mainfrom
fm/fm-axi-isolated-prefixes

Conversation

@Freudator86

Copy link
Copy Markdown
Owner

Intent

Give each Firstmate vessel an isolated AXI npm install prefix derived without configuration from its FM_HOME, eliminating cross-home writes and races from independent update clocks. Use isolated per-vessel prefixes rather than a shared owner or lock, accepting duplicate installs. Preserve the managed tool set and patch/minor auto-update policy; do not uninstall or modify existing shared installs. Reinstall existing versions per vessel as needed, leave shared copies untouched as fallback, make PATH ordering unambiguous so the primary, crew, and secondmate resolve their owning vessel's copy first, cover concurrent two-home updates, and verify the per-home currency clock still works after cutover.

What Changed

  • Each operational home now owns an npm prefix derived purely from FM_HOME ($FM_HOME/.local/axi, gitignored) via the new bin/fm-axi-path-lib.sh. Bootstrap, session start, spawn, and dispatch-select prepend that bin directory to PATH, bin/fm-spawn.sh types the owning vessel's export into each crewmate pane (the secondmate home's own prefix for secondmate launches), and the README launch lines prepend it before the harness starts — so the primary, crew, and secondmate resolve their vessel's copy first and fall back to an inherited external install only before cutover.
  • bin/fm-axi-suite.sh seeds each home's prefix on its first currency check — re-installing the already-installed version, or an eligible patch/minor release, without touching or removing any existing shared install — keeping the managed tool set and patch/minor auto-update policy unchanged. state/axi-suite-prefix-v1.cutover records the attempt so a pre-cutover cadence stamp can't postpone it and a partial seed still honours the 24h cadence; a vessel copy that answers but reports no version is discarded and reseeded, one that never answers is reported and left alone, and a locally-ahead build the registry can't supply is reported as AXI_SUITE_REVIEW: instead of a recurring failure. Registry/update work, local version probes (FM_AXI_SUITE_PROBE_TIMEOUT, 15s), and one-time seeding installs (FM_AXI_SUITE_SEED_TIMEOUT, 120s) are now charged to separate budgets, and a seed cut short by an exhausted budget is reported identically from both install paths. Bootstrap's MISSING: install hints now target the home prefix and invoke setup hooks by tool name so the shared user-global hook config records a portable command.
  • tests/fm-axi-suite.test.sh and tests/fm-bootstrap.test.sh cover concurrent two-home updates against distinct prefixes, vessel-prefix PATH precedence, the per-home currency clock surviving cutover, unreadable/hung vessel copies, budget-stalled cutovers, and the approved-install path using the vessel prefix. The Document check flagged three remaining prose surfaces (the external admiralty repo's protected-path list, the illustrative gitignore lists in AGENTS.md/CONTRIBUTING.md, and the partial FM_* env-var inventory in docs/configuration.md) as deliberately out of scope.

Risk Assessment

✅ Low: Every finding raised across five prior rounds is now resolved with a matching regression test, the three budget accumulators and the repair/fallback path are well bounded and covered, docs and the bootstrap-diagnostics skill are in sync with the emitted tokens, and the only residual - the shared user-global setup hooks write that no prefix can isolate - was explicitly accepted by the captain and is documented as a known boundary.

Testing

I ran the targeted suites for the changed scripts (axi-suite, bootstrap, spawn dispatch profile, runtime-ignore, gotmp, dispatch-select) — all green — and then built a manual end-to-end demo that drives the real fm-axi-suite/fm-bootstrap/fm-spawn for two vessels sharing one host with a pre-existing global AXI install; the captured transcript shows disjoint per-home npm prefixes under concurrent updates, the shared copy left intact as fallback, vessel-first PATH resolution for primary, crew and secondmate launches, the per-home currency clock still gating then resuming after cutover, and major releases still held for review. This is a shell/CLI change with no rendered UI surface, so the reviewer-visible evidence is the CLI transcript rather than a screenshot. Transient demo workspaces were removed and the worktree is clean.

Evidence: End-to-end transcript: two vessels, isolated AXI prefixes

1. BEFORE CUTOVER $ command -v demo-axi <host>/shared/npm-global/bin/demo-axi (1.0.0) 2. BOTH VESSELS RUN fm-axi-suite.sh --force CONCURRENTLY vessel-alpha: AXI_SUITE_UPDATED: demo-axi 1.0.0 -> 1.0.1 / gh-axi 1.0.0 installed in vessel prefix install -g --prefix <demo>/vessel-alpha/.local/axi demo-axi@1.0.1 install -g --prefix <demo>/vessel-alpha/.local/axi gh-axi@1.0.0 vessel-bravo: AXI_SUITE_UPDATED: demo-axi 1.0.0 -> 1.0.1 / gh-axi 1.0.0 installed in vessel prefix install -g --prefix <demo>/vessel-bravo/.local/axi demo-axi@1.0.1 install -g --prefix <demo>/vessel-bravo/.local/axi gh-axi@1.0.0 3. NO CROSS-HOME WRITES, NO SHARED-INSTALL MUTATION no - every npm install targeted only the calling vessel prefix no - the shared prefix never appears; no uninstall or modify of the shared copy alpha 1.0.1 | bravo 1.0.1 | shared 1.0.0 (untouched fallback) 4. PATH ORDERING PATH="$FM_HOME/.local/axi/bin:$PATH" command -v demo-axi -> <demo>/vessel-alpha/.local/axi/bin/demo-axi (1.0.1) shared bin listed FIRST + fm_axi_prepend_path -> still <demo>/vessel-alpha/.local/axi/bin/demo-axi (1.0.1) 5. CREW AND SECONDMATE LAUNCHES (real fm-spawn.sh, fake pane) crew: typed: export PATH='<demo>/vessel-alpha/.local/axi/bin':$PATH secondmate: typed: export PATH='<demo>/secondmate-home/.local/axi/bin':$PATH 6. PER-HOME CURRENCY CLOCK AFTER CUTOVER state/: axi-suite-prefix-v1.cutover, axi-suite-update.checked cached run inside the window: <silent>, npm calls before=2 after=2 (registry had published 1.0.2) --force: AXI_SUITE_UPDATED: demo-axi 1.0.1 -> 1.0.2 | alpha 1.0.2, bravo 1.0.1, shared 1.0.0 7. POLICY PRESERVED AXI_SUITE_REVIEW: demo-axi major update 1.0.2 -> 2.0.0 (install: npm install -g --prefix <alpha>/.local/axi demo-axi@2.0.0) vessel copy stayed at 1.0.2 8. BOOTSTRAP MISSING: tasks-axi (install: npm install -g --prefix <home>/.local/axi tasks-axi) MISSING: gh-axi (install: npm install -g --prefix <home>/.local/axi gh-axi && PATH=<home>/.local/axi/bin:$PATH gh-axi setup hooks) bin/fm-bootstrap.sh install gh-axi -> installed into <home>/.local/axi/bin, hook setup ran


================================================================
1. BEFORE CUTOVER - one shared install serves every vessel on this host
================================================================
$ command -v demo-axi
/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/shared/npm-global/bin/demo-axi
$ demo-axi --version
1.0.0
$ ls $FM_HOME/.local/axi  (alpha)
ls: cannot access '/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi': No such file or directory
$ ls $FM_HOME/.local/axi  (bravo)
ls: cannot access '/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-bravo/.local/axi': No such file or directory

================================================================
2. TWO VESSELS RUN THEIR INDEPENDENT UPDATE CLOCKS AT THE SAME MOMENT
================================================================
registry latest: demo-axi 1.0.1 (patch), gh-axi 1.0.0 (current)
both vessels start bin/fm-axi-suite.sh --force concurrently ...


--- vessel-alpha stdout (what bootstrap surfaces to the captain)
AXI_SUITE_UPDATED: demo-axi 1.0.0 -> 1.0.1
AXI_SUITE_UPDATED: gh-axi 1.0.0 installed in vessel prefix

--- vessel-alpha stderr (seeding progress + closing summary)
fm-axi-suite.sh: seeding this vessel AXI prefix (/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi) for the first time; these installs run under a separate 120s seeding budget, so this check takes longer than a steady-state one
fm-axi-suite.sh: installing demo-axi@1.0.1 into /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi (120s of the seeding budget left)
fm-axi-suite.sh: installing gh-axi@1.0.0 into /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi (120s of the seeding budget left)
fm-axi-suite.sh: seeding pass finished: 2 install(s) attempted into /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi, 0s of the 120s seeding budget used

--- vessel-alpha npm calls
install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi demo-axi@1.0.1
install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi gh-axi@1.0.0

--- vessel-bravo stdout (what bootstrap surfaces to the captain)
AXI_SUITE_UPDATED: demo-axi 1.0.0 -> 1.0.1
AXI_SUITE_UPDATED: gh-axi 1.0.0 installed in vessel prefix

--- vessel-bravo stderr (seeding progress + closing summary)
fm-axi-suite.sh: seeding this vessel AXI prefix (/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-bravo/.local/axi) for the first time; these installs run under a separate 120s seeding budget, so this check takes longer than a steady-state one
fm-axi-suite.sh: installing demo-axi@1.0.1 into /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-bravo/.local/axi (120s of the seeding budget left)
fm-axi-suite.sh: installing gh-axi@1.0.0 into /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-bravo/.local/axi (120s of the seeding budget left)
fm-axi-suite.sh: seeding pass finished: 2 install(s) attempted into /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-bravo/.local/axi, 0s of the 120s seeding budget used

--- vessel-bravo npm calls
install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-bravo/.local/axi demo-axi@1.0.1
install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-bravo/.local/axi gh-axi@1.0.0

================================================================
3. NO CROSS-HOME WRITES, NO SHARED-INSTALL MUTATION
================================================================

--- does alpha's npm log mention bravo's prefix (or vice versa)?
no - every npm install targeted only the calling vessel prefix

--- does any vessel npm call touch the shared global prefix?
no - '/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/shared/npm-global/bin' never appears; no uninstall or modify of the shared copy

--- resulting versions on disk
$ alpha/.local/axi/bin/demo-axi --version
1.0.1
$ bravo/.local/axi/bin/demo-axi --version
1.0.1
$ shared demo-axi --version (untouched fallback)
1.0.0
$ alpha/.local/axi/bin listing
demo-axi
gh-axi
$ bravo/.local/axi/bin listing
demo-axi
gh-axi

--- gh-axi hook setup (shared user-global surface, invoked by portable name)
hook setup ran via: /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi/bin/gh-axi (argv0=/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi/bin/gh-axi)
hook setup ran via: /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-bravo/.local/axi/bin/gh-axi (argv0=/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-bravo/.local/axi/bin/gh-axi)

================================================================
4. PATH ORDERING - who resolves which copy
================================================================

--- primary launch exactly as README documents it
$ PATH="$FM_HOME/.local/axi/bin:$PATH" command -v demo-axi   (alpha)
/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi/bin/demo-axi
$ PATH="$FM_HOME/.local/axi/bin:$PATH" demo-axi --version    (alpha)
1.0.1
$ PATH="$FM_HOME/.local/axi/bin:$PATH" demo-axi --version    (bravo)
1.0.1

--- a firstmate entrypoint prepends its own vessel bin even when the shared bin came first
$ PATH=<shared>:<vessel>:... ; . bin/fm-axi-path-lib.sh; fm_axi_prepend_path; command -v demo-axi
/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi/bin/demo-axi
1.0.1

================================================================
5. CREW AND SECONDMATE LAUNCHES GET THEIR OWNING VESSEL'S BIN FIRST
================================================================

--- crewmate spawned by vessel alpha - lines typed into the pane
typed: export PATH='/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi/bin':$PATH
typed: CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions --settings '/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/wt/.claude/settings.fm-task.json' "$('/home/coditan/.no-mistakes/worktrees/11db93185ab5/01KYKBWGDZ23561ZMKMS4WJ132/bin/fm-operational-input.sh' encode launch-brief < '/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/data/crew-1/brief.md')"

--- secondmate spawned by vessel alpha - lines typed into the pane
typed: export PATH='/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/secondmate-home/.local/axi/bin':$PATH
typed: FM_ROOT_OVERRIDE= FM_STATE_OVERRIDE= FM_DATA_OVERRIDE= FM_PROJECTS_OVERRIDE= FM_CONFIG_OVERRIDE= FM_HOME='/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/secondmate-home' CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude --dangerously-skip-permissions "$('/home/coditan/.no-mistakes/worktrees/11db93185ab5/01KYKBWGDZ23561ZMKMS4WJ132/bin/fm-operational-input.sh' encode launch-brief < '/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/secondmate-home/data/charter.md')"

(alpha home = /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha)
(secondmate home = /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/secondmate-home)

================================================================
6. THE PER-HOME CURRENCY CLOCK STILL WORKS AFTER CUTOVER
================================================================

--- state written by the first pass
$ ls $FM_HOME/state
axi-suite-prefix-v1.cutover
axi-suite-update.checked

--- a second run inside the 24h window (no --force): silent, no npm traffic
output: <silent>
npm calls before=2 after=2 (registry meanwhile published 1.0.2)

--- the same check once the window is open (simulated by --force)
$ bin/fm-axi-suite.sh --force
AXI_SUITE_UPDATED: demo-axi 1.0.1 -> 1.0.2
$ alpha vessel copy after the update
1.0.2
$ bravo vessel copy is unaffected by alpha's clock
1.0.1
$ shared copy still untouched
1.0.0

================================================================
7. UPDATE POLICY PRESERVED: MAJOR STILL WAITS FOR CAPTAIN REVIEW
================================================================
$ bin/fm-axi-suite.sh --force (registry now offers demo-axi 2.0.0)
AXI_SUITE_REVIEW: demo-axi major update 1.0.2 -> 2.0.0 (install: npm install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/vessel-alpha/.local/axi demo-axi@2.0.0)
$ vessel copy stayed on the reviewed-safe version
1.0.2

================================================================
8. BOOTSTRAP DISCLOSES AND PERFORMS APPROVED INSTALLS IN THE VESSEL PREFIX
================================================================
$ bin/fm-bootstrap.sh on a fresh vessel (AXI suite absent)
MISSING: gh-axi (install: npm install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi gh-axi && PATH=/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi/bin:$PATH gh-axi setup hooks)
MISSING: chrome-devtools-axi (install: npm install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi chrome-devtools-axi && PATH=/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi/bin:$PATH chrome-devtools-axi setup hooks)
MISSING: lavish-axi (install: npm install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi lavish-axi && PATH=/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi/bin:$PATH lavish-axi setup hooks)
MISSING: tasks-axi (install: npm install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi tasks-axi)
MISSING: quota-axi (install: npm install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi quota-axi)
$ bin/fm-bootstrap.sh install gh-axi   (captain approves)
installing gh-axi: npm install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi gh-axi && PATH=/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi/bin:$PATH gh-axi setup hooks
$ npm calls it made
install -g --prefix /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi gh-axi
$ where the binary landed
gh-axi
$ hook setup it ran
hook setup ran via: /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi/bin/gh-axi (argv0=/home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG/bootstrap-home/.local/axi/bin/gh-axi)

(demo workspace: /home/coditan/.cache/no-mistakes-tmp/fm-axi-demo.4zDRXG)
Evidence: Reproduction script for the demo
#!/usr/bin/env bash
# End-to-end demonstration of isolated per-vessel AXI npm prefixes.
#
# Two Firstmate vessels (alpha, bravo) share one host that already has a
# shared/global AXI install. The demo drives the REAL bin/fm-axi-suite.sh,
# bin/fm-bootstrap.sh and bin/fm-spawn.sh against a fake npm registry and a
# fake tmux pane, and prints what a captain would actually observe.
set -u

ROOT=${FM_DEMO_REPO:?set FM_DEMO_REPO to the firstmate checkout}
. "$ROOT/tests/lib.sh"
DEMO=$(mktemp -d "${TMPDIR:-/tmp}/fm-axi-demo.XXXXXX")
BASE_PATH=$PATH

hr() { printf '\n================================================================\n%s\n================================================================\n' "$1"; }
sub() { printf '\n--- %s\n' "$1"; }
show() { printf '$ %s\n' "$1"; eval "$2"; }

# ---------------------------------------------------------------- fake npm ---
# Honours `npm view <tool> version` and `npm install -g --prefix <p> <tool@v>`
# exactly like the real client would for this flow, and records every call.
mkdir -p "$DEMO/hostbin"
cat > "$DEMO/hostbin/npm" <<'SH'
#!/usr/bin/env bash
if [ "${1:-}" = view ]; then sed -n "s/^${2}=//p" "$FM_DEMO_REGISTRY"; exit 0; fi
if [ "${1:-}" = install ]; then
  printf '%s\n' "$*" >> "$FM_DEMO_NPM_LOG"
  shift; prefix=; spec=
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --prefix) prefix=${2:-}; shift 2 ;;
      -g) shift ;;
      *) spec=$1; shift ;;
    esac
  done
  [ -n "$prefix" ] && [ -n "$spec" ] || exit 1
  tool=${spec%@*}; version=${spec##*@}
  mkdir -p "$prefix/bin"
  cat > "$prefix/bin/$tool" <<TOOL
#!/usr/bin/env bash
if [ "\${1:-}" = --version ]; then printf '%s\n' '$version'; exit 0; fi
if [ "\${1:-}" = setup ] && [ "\${2:-}" = hooks ]; then
  printf 'hook setup ran via: %s (argv0=%s)\n' "\$(command -v $tool)" "\$0" >> "\${FM_DEMO_HOOK_LOG:-/dev/null}"
  exit 0
fi
TOOL
  chmod +x "$prefix/bin/$tool"
  exit 0
fi
exit 1
SH
chmod +x "$DEMO/hostbin/npm"

# ------------------------------------------- pre-existing shared/global install
mkdir -p "$DEMO/shared/npm-global/bin"
for t in demo-axi gh-axi; do
  printf '#!/usr/bin/env bash\n[ "${1:-}" = --version ] && { printf "1.0.0\\n"; exit 0; }\nexit 0\n' \
    > "$DEMO/shared/npm-global/bin/$t"
  chmod +x "$DEMO/shared/npm-global/bin/$t"
done
SHARED_BIN="$DEMO/shared/npm-global/bin"

ALPHA="$DEMO/vessel-alpha"; BRAVO="$DEMO/vessel-bravo"
mkdir -p "$ALPHA/state" "$BRAVO/state"
printf 'demo-axi=1.0.1\ngh-axi=1.0.0\n' > "$DEMO/registry-alpha"
printf 'demo-axi=1.0.1\ngh-axi=1.0.0\n' > "$DEMO/registry-bravo"

# The PATH a vessel process inherits: fake npm + the shared global install.
HOST_PATH="$DEMO/hostbin:$SHARED_BIN:$BASE_PATH"

hr "1. BEFORE CUTOVER - one shared install serves every vessel on this host"
show "command -v demo-axi" "PATH=$HOST_PATH command -v demo-axi"
show "demo-axi --version" "PATH=$HOST_PATH demo-axi --version"
show "ls \$FM_HOME/.local/axi  (alpha)" "ls '$ALPHA/.local/axi' 2>&1 || true"
show "ls \$FM_HOME/.local/axi  (bravo)" "ls '$BRAVO/.local/axi' 2>&1 || true"

hr "2. TWO VESSELS RUN THEIR INDEPENDENT UPDATE CLOCKS AT THE SAME MOMENT"
printf 'registry latest: demo-axi 1.0.1 (patch), gh-axi 1.0.0 (current)\n'
printf 'both vessels start bin/fm-axi-suite.sh --force concurrently ...\n\n'
for v in alpha bravo; do
  eval "home=\$$(printf '%s' "$v" | tr 'a-z' 'A-Z')"
  PATH="$HOST_PATH" FM_HOME="$home" FM_AXI_SUITE_DISABLE=0 \
    FM_AXI_SUITE_TOOLS="demo-axi gh-axi" FM_AXI_SUITE_CHECK_INTERVAL=86400 \
    FM_DEMO_REGISTRY="$DEMO/registry-$v" FM_DEMO_NPM_LOG="$DEMO/npm-$v.log" \
    FM_DEMO_HOOK_LOG="$DEMO/hooks-$v.log" \
    "$ROOT/bin/fm-axi-suite.sh" --force > "$DEMO/out-$v" 2> "$DEMO/progress-$v" &
done
wait

for v in alpha bravo; do
  sub "vessel-$v stdout (what bootstrap surfaces to the captain)"
  cat "$DEMO/out-$v"
  sub "vessel-$v stderr (seeding progress + closing summary)"
  cat "$DEMO/progress-$v"
  sub "vessel-$v npm calls"
  cat "$DEMO/npm-$v.log"
done

hr "3. NO CROSS-HOME WRITES, NO SHARED-INSTALL MUTATION"
sub "does alpha's npm log mention bravo's prefix (or vice versa)?"
if grep -q "$BRAVO" "$DEMO/npm-alpha.log" || grep -q "$ALPHA" "$DEMO/npm-bravo.log"; then
  printf 'CROSS-HOME WRITE DETECTED\n'
else
  printf 'no - every npm install targeted only the calling vessel prefix\n'
fi
sub "does any vessel npm call touch the shared global prefix?"
if grep -q "$SHARED_BIN" "$DEMO/npm-alpha.log" "$DEMO/npm-bravo.log"; then
  printf 'SHARED PREFIX WAS WRITTEN\n'
else
  printf "no - '$SHARED_BIN' never appears; no uninstall or modify of the shared copy\n"
fi
sub "resulting versions on disk"
show "alpha/.local/axi/bin/demo-axi --version" "'$ALPHA/.local/axi/bin/demo-axi' --version"
show "bravo/.local/axi/bin/demo-axi --version" "'$BRAVO/.local/axi/bin/demo-axi' --version"
show "shared demo-axi --version (untouched fallback)" "'$SHARED_BIN/demo-axi' --version"
show "alpha/.local/axi/bin listing" "ls -1 '$ALPHA/.local/axi/bin'"
show "bravo/.local/axi/bin listing" "ls -1 '$BRAVO/.local/axi/bin'"
sub "gh-axi hook setup (shared user-global surface, invoked by portable name)"
cat "$DEMO/hooks-alpha.log" "$DEMO/hooks-bravo.log" 2>/dev/null || true

hr "4. PATH ORDERING - who resolves which copy"
sub "primary launch exactly as README documents it"
show "PATH=\"\$FM_HOME/.local/axi/bin:\$PATH\" command -v demo-axi   (alpha)" \
  "PATH=\"$ALPHA/.local/axi/bin:$HOST_PATH\" command -v demo-axi"
show "PATH=\"\$FM_HOME/.local/axi/bin:\$PATH\" demo-axi --version    (alpha)" \
  "PATH=\"$ALPHA/.local/axi/bin:$HOST_PATH\" demo-axi --version"
show "PATH=\"\$FM_HOME/.local/axi/bin:\$PATH\" demo-axi --version    (bravo)" \
  "PATH=\"$BRAVO/.local/axi/bin:$HOST_PATH\" demo-axi --version"
sub "a firstmate entrypoint prepends its own vessel bin even when the shared bin came first"
show "PATH=<shared>:<vessel>:... ; . bin/fm-axi-path-lib.sh; fm_axi_prepend_path; command -v demo-axi" \
  "PATH=\"$SHARED_BIN:$ALPHA/.local/axi/bin:$BASE_PATH\" FM_HOME='$ALPHA' bash -c '. \"$ROOT/bin/fm-axi-path-lib.sh\"; fm_axi_prepend_path; command -v demo-axi; demo-axi --version'"

hr "5. CREW AND SECONDMATE LAUNCHES GET THEIR OWNING VESSEL'S BIN FIRST"
# Real bin/fm-spawn.sh against a fake tmux pane that records every typed line.
FAKE=$(fm_fakebin "$DEMO/spawn")
cat > "$FAKE/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "$*" in *"#{pane_current_path}"*) printf '%s\n' "${FM_FAKE_PANE_PATH:-}"; exit 0 ;; esac
case "${1:-}" in
  display-message) printf 'firstmate\n'; exit 0 ;;
  list-windows) exit 0 ;;
  has-session|new-session|new-window|kill-window) exit 0 ;;
  send-keys)
    prev=
    for a in "$@"; do
      case "$a" in 'export PATH='*) printf 'typed: %s\n' "$a" >> "$FM_FAKE_LAUNCH_LOG" ;; esac
      [ "$prev" != "-l" ] || printf 'typed: %s\n' "$a" >> "$FM_FAKE_LAUNCH_LOG"
      prev=$a
    done
    exit 0 ;;
esac
exit 0
SH
chmod +x "$FAKE/tmux"
fm_fake_exit0 "$FAKE" treehouse

mkdir -p "$ALPHA/data/crew-1" "$ALPHA/projects" "$ALPHA/config"
printf 'claude\n' > "$ALPHA/config/crew-harness"
printf 'brief\n' > "$ALPHA/data/crew-1/brief.md"
touch "$ALPHA/state/.last-watcher-beat"
fm_git_worktree "$DEMO/project" "$DEMO/wt" wt-demo >/dev/null 2>&1

: > "$DEMO/launch-crew.log"
FM_ROOT_OVERRIDE='' FM_HOME="$ALPHA" FM_STATE_OVERRIDE="$ALPHA/state" \
  FM_DATA_OVERRIDE="$ALPHA/data" FM_PROJECTS_OVERRIDE="$ALPHA/projects" \
  FM_CONFIG_OVERRIDE="$ALPHA/config" FM_SPAWN_NO_GUARD=1 FM_FAKE_PANE_PATH="$DEMO/wt" \
  TMUX="fake,1,0" FM_FAKE_LAUNCH_LOG="$DEMO/launch-crew.log" PATH="$FAKE:$HOST_PATH" \
  "$ROOT/bin/fm-spawn.sh" crew-1 "$DEMO/project" >/dev/null 2>&1
sub "crewmate spawned by vessel alpha - lines typed into the pane"
cat "$DEMO/launch-crew.log"

SM="$DEMO/secondmate-home"
mkdir -p "$SM/bin" "$SM/data" "$ALPHA/data/sm-1"
printf '# Firstmate\n' > "$SM/AGENTS.md"
printf 'sm-1\n' > "$SM/.fm-secondmate-home"
printf 'charter\n' > "$SM/data/charter.md"
printf 'brief\n' > "$ALPHA/data/sm-1/brief.md"
: > "$DEMO/launch-sm.log"
FM_ROOT_OVERRIDE='' FM_HOME="$ALPHA" FM_STATE_OVERRIDE="$ALPHA/state" \
  FM_DATA_OVERRIDE="$ALPHA/data" FM_PROJECTS_OVERRIDE="$ALPHA/projects" \
  FM_CONFIG_OVERRIDE="$ALPHA/config" FM_SPAWN_NO_GUARD=1 FM_FAKE_PANE_PATH="$DEMO/wt" \
  TMUX="fake,1,0" FM_FAKE_LAUNCH_LOG="$DEMO/launch-sm.log" PATH="$FAKE:$HOST_PATH" \
  "$ROOT/bin/fm-spawn.sh" --secondmate sm-1 "$SM" >/dev/null 2>&1
sub "secondmate spawned by vessel alpha - lines typed into the pane"
cat "$DEMO/launch-sm.log"
printf '\n(alpha home = %s)\n(secondmate home = %s)\n' "$ALPHA" "$SM"

hr "6. THE PER-HOME CURRENCY CLOCK STILL WORKS AFTER CUTOVER"
sub "state written by the first pass"
show "ls \$FM_HOME/state" "ls -1 '$ALPHA/state' | grep axi"
sub "a second run inside the 24h window (no --force): silent, no npm traffic"
before=$(wc -l < "$DEMO/npm-alpha.log")
printf 'demo-axi=1.0.2\ngh-axi=1.0.0\n' > "$DEMO/registry-alpha"
out=$(PATH="$HOST_PATH" FM_HOME="$ALPHA" FM_AXI_SUITE_DISABLE=0 \
  FM_AXI_SUITE_TOOLS="demo-axi gh-axi" FM_AXI_SUITE_CHECK_INTERVAL=86400 \
  FM_DEMO_REGISTRY="$DEMO/registry-alpha" FM_DEMO_NPM_LOG="$DEMO/npm-alpha.log" \
  FM_DEMO_HOOK_LOG="$DEMO/hooks-alpha.log" "$ROOT/bin/fm-axi-suite.sh" 2>&1)
after=$(wc -l < "$DEMO/npm-alpha.log")
printf 'output: %s\n' "${out:-<silent>}"
printf 'npm calls before=%s after=%s (registry meanwhile published 1.0.2)\n' "$before" "$after"
sub "the same check once the window is open (simulated by --force)"
show "bin/fm-axi-suite.sh --force" \
  "PATH=\"$HOST_PATH\" FM_HOME='$ALPHA' FM_AXI_SUITE_DISABLE=0 FM_AXI_SUITE_TOOLS='demo-axi gh-axi' FM_AXI_SUITE_CHECK_INTERVAL=86400 FM_DEMO_REGISTRY='$DEMO/registry-alpha' FM_DEMO_NPM_LOG='$DEMO/npm-alpha.log' FM_DEMO_HOOK_LOG='$DEMO/hooks-alpha.log' '$ROOT/bin/fm-axi-suite.sh' --force"
show "alpha vessel copy after the update" "'$ALPHA/.local/axi/bin/demo-axi' --version"
show "bravo vessel copy is unaffected by alpha's clock" "'$BRAVO/.local/axi/bin/demo-axi' --version"
show "shared copy still untouched" "'$SHARED_BIN/demo-axi' --version"

hr "7. UPDATE POLICY PRESERVED: MAJOR STILL WAITS FOR CAPTAIN REVIEW"
printf 'demo-axi=2.0.0\ngh-axi=1.0.0\n' > "$DEMO/registry-alpha"
show "bin/fm-axi-suite.sh --force (registry now offers demo-axi 2.0.0)" \
  "PATH=\"$HOST_PATH\" FM_HOME='$ALPHA' FM_AXI_SUITE_DISABLE=0 FM_AXI_SUITE_TOOLS='demo-axi gh-axi' FM_AXI_SUITE_CHECK_INTERVAL=86400 FM_DEMO_REGISTRY='$DEMO/registry-alpha' FM_DEMO_NPM_LOG='$DEMO/npm-alpha.log' FM_DEMO_HOOK_LOG='$DEMO/hooks-alpha.log' '$ROOT/bin/fm-axi-suite.sh' --force"
show "vessel copy stayed on the reviewed-safe version" "'$ALPHA/.local/axi/bin/demo-axi' --version"

hr "8. BOOTSTRAP DISCLOSES AND PERFORMS APPROVED INSTALLS IN THE VESSEL PREFIX"
BS="$DEMO/bootstrap-home"; mkdir -p "$BS"
# A minimal PATH so this host's own AXI installs do not mask the report.
show "bin/fm-bootstrap.sh on a fresh vessel (AXI suite absent)" \
  "PATH='$DEMO/hostbin:/usr/bin:/bin' FM_HOME='$BS' FM_ROOT_OVERRIDE='$BS' FM_AXI_SUITE_DISABLE=1 '$ROOT/bin/fm-bootstrap.sh' 2>&1 | grep -E 'MISSING: (tasks|quota|gh|lavish|chrome-devtools)-axi' || true"
: > "$DEMO/npm-bs.log"; : > "$DEMO/hooks-bs.log"
show "bin/fm-bootstrap.sh install gh-axi   (captain approves)" \
  "PATH='$DEMO/hostbin:/usr/bin:/bin' FM_HOME='$BS' FM_ROOT_OVERRIDE='$BS' FM_AXI_SUITE_DISABLE=1 FM_DEMO_REGISTRY='$DEMO/registry-alpha' FM_DEMO_NPM_LOG='$DEMO/npm-bs.log' FM_DEMO_HOOK_LOG='$DEMO/hooks-bs.log' '$ROOT/bin/fm-bootstrap.sh' install gh-axi 2>&1"
show "npm calls it made" "cat '$DEMO/npm-bs.log'"
show "where the binary landed" "ls -1 '$BS/.local/axi/bin'"
show "hook setup it ran" "cat '$DEMO/hooks-bs.log'"

printf '\n(demo workspace: %s)\n' "$DEMO"

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 7 issues found → auto-fixed (5) ✅
  • ⚠️ bin/fm-axi-suite.sh:303 - The new state/axi-suite-prefix-v1.ready gate is deleted whenever any suite tool that is on PATH lacks a vessel copy, and the cadence check at line 136 requires that marker. Any tool whose registry lookup or npm install --prefix cannot succeed (offline home, private/authenticated registry, or a suite entry such as gnhf that is not installable into this prefix) leaves the marker permanently absent, so the 24h cadence never engages again. Before this change the stamp alone gated the check, so a stuck home still only paid the sweep once a day. Consider recording a separate "cutover attempted" marker so a failed seed still honours the cadence.
  • ⚠️ bin/fm-axi-suite.sh:213 - A vessel-prefix install that writes the bin shim but is otherwise broken (interrupted install, node/ABI mismatch) permanently shadows the intact shared copy: the prepended $FM_HOME/.local/axi/bin always wins for the primary, crew, and secondmate, version_of returns empty, the loop emits installed version could not be read and continues without ever reinstalling, and the readiness loop at line 303 still sees an executable so it writes PREFIX_READY and caches for 24h. The intent's "shared copies untouched as fallback" is unreachable once a bad vessel copy exists. Consider removing/reinstalling the vessel copy when its version cannot be read.
  • ⚠️ bin/fm-axi-suite.sh:188 - setup_home_hooks now runs $FM_HOME/.local/axi/bin/&lt;tool&gt; setup hooks from each vessel's own binary during seeding and after every update. The axi hook entries are not in the tracked .claude/settings.json, so setup hooks writes user-global harness config; if it records the invoking binary's absolute path, the last home to cut over rewrites hook wiring for every other home, and removing that home breaks the others. Previously all vessels invoked the same shared binary so the result was identical regardless of which one ran it. Worth confirming what setup hooks writes before this lands on multi-home installs.
  • ℹ️ bin/fm-axi-suite.sh:252 - When the installed copy is ahead of the registry latest tag, seeding runs npm install -g --prefix ... &lt;tool&gt;@&lt;installed&gt;. That version may not be published (npm-linked dev build, local build, unpublished prerelease), so the install fails, an AXI_SUITE_STUCK: line is emitted every session, and combined with the readiness gate the home never regains its cadence. The old code left locally-ahead tools entirely alone. Falling back to the registry latest (or skipping the seed) when the exact version is unavailable would avoid the permanent stuck state.
  • ℹ️ bin/fm-axi-path-lib.sh:81 - fm_axi_bin_dir interpolates fm_axi_prefix via command substitution and returns printf's status, so when the prefix cannot be resolved (empty home) it prints /bin and exits 0 instead of failing. fm_axi_prepend_path's || return 1 therefore never triggers and /bin gets prepended to PATH. All current call sites pass a non-empty home, but the guard in fm_axi_prefix is defeated. Capture the prefix into a local and propagate its exit status.
  • ℹ️ bin/fm-axi-suite.sh:230 - The home_installed -eq 0 seed block (check-only report vs. seed_home_tool ... || true) is repeated verbatim three times: in the already-current branch (230-241), the locally-ahead branch (253-260), and the pending-major branch (266-273). Extracting a single ensure_home_copy &lt;tool&gt; &lt;version&gt; helper would remove the duplication and keep the three call sites in sync.
  • ℹ️ .agents/skills/bootstrap-diagnostics/SKILL.md:52 - seed_home_tool emits AXI_SUITE_UPDATED: &lt;tool&gt; &lt;version&gt; installed in vessel prefix, but the bootstrap-diagnostics skill still documents the token as only AXI_SUITE_UPDATED: &lt;tool&gt; &lt;old&gt; -&gt; &lt;new&gt; (a gated patch/minor self-update). docs/configuration.md was updated for the new seeding behavior; this skill, which is what an agent loads to interpret the digest line, was not.

🔧 Fix: harden vessel AXI prefix seeding, repair, and cadence
3 issues (1 warning, 2 infos) still open:

  • ⚠️ bin/fm-axi-suite.sh:187 - home_tool_is_readable executes the vessel binary via version_of, which runs outside net_call/bounded and inherits the script's stdin. The new repair pass at line 259 runs this probe for every tool that has a vessel copy, and discard_broken_home_tool runs it again at line 198 — on a binary already suspected of being broken. A half-installed copy that stalls rather than exiting (blocks reading inherited stdin, or a shim whose runtime hangs) wedges fm-axi-suite.sh forever, and with it the locked bootstrap at bin/fm-bootstrap.sh:938 and session start. FM_AXI_SUITE_NETWORK_TIMEOUT explicitly bounds registry, install, and hook work but does not cover this. The pre-existing version_of &#34;$tool&#34; at line 278 shares the exposure, so this widens an existing class rather than creating it. Adding &lt;/dev/null to version_of is a safe non-functional fix; bounding the probe as well would close it fully, though a timeout would then also classify a very slow but working copy as unreadable and remove it.
  • ℹ️ bin/fm-axi-suite.sh:259 - In the steady state (vessel copy present and readable) each suite tool's --version is now spawned twice per check — once by the repair probe at line 259 and once by version_of &#34;$tool&#34; at line 278, which resolves to the same vessel binary because $AXI_BIN is first on PATH — and three times when the probe reports unreadable, since discard_broken_home_tool re-evaluates home_tool_is_readable at line 198 for a verdict the caller just computed. For the six-tool default suite that is 12 node CLI launches instead of 6. Reading the vessel copy's version once and reusing it for the readable/unreadable decision, the discard decision, and installed would remove both duplications.
  • ℹ️ bin/fm-axi-suite.sh:246 - ensure_home_copy returns 1 both when seeding succeeded and when it failed, so it cannot signal "the vessel now owns a copy". At line 294 that leaves home_present=0 after a successful first seed, and the pending-hook-retry branch at line 298 is therefore skipped for a tool that does have a working copy. There is no behavioral gap today only because seed_home_tool runs setup_home_hooks itself on that path, so the pending stuck line still clears — but the two mechanisms are coupled implicitly and an edit to either silently drops the hook retry. Returning 0/1 for "copy present after this call" (or setting an out-variable) would make the guard at line 298 mean what it reads as.

🔧 Fix: bound vessel version probes and dedupe version reads
1 error still open:

  • 🚨 bin/fm-axi-suite.sh:109 - PROBE_DEADLINE=$((STARTED + PROBE_TIMEOUT)) makes the probe budget an absolute wall-clock deadline from script start, so time spent in npm view and npm install (which draw on the separate 30s network budget) silently consumes it. remaining_budget then returns 0 and probe_call (:137) returns 124 without executing anything, which read_version classifies as timeout. Walking the default 6-tool first cutover with conservative timings (npm view ~2s, npm install ~6s per tool): tool 1 seeds and verifies, but tool 2's post-install verification probe at :245 lands at ~16.6s wall clock with zero budget, so VERSION_READ is empty, install_home_tool returns 1, and seed_home_tool writes AXI_SUITE_STUCK: &lt;tool&gt; vessel-prefix installation at &lt;prefix&gt; failed for an install that actually succeeded; tools 3-6 then fail at read_version &#34;$tool&#34; (:345) and emit installed version could not be read before continue, so they are never seeded. With an unreachable registry the first npm view burns the whole network budget and every later tool holding a vessel copy reports did not answer --version within 15s (:316) for a binary that was never run - directly contradicting the comment at :133 ("an unreachable registry cannot starve the probes") and the docs/configuration.md claim that this budget covers "cumulative local version probing". The tests miss it because the stub npm returns instantly and the hung-copy test uses a single tool with no preceding slow work. Track probe time actually spent (or cap each probe at PROBE_TIMEOUT with its own accumulator) instead of deriving it from wall clock since start, and report the granted budget in the :317 message rather than always interpolating $PROBE_TIMEOUT.

🔧 Fix: charge probe and network budgets separately
2 issues (1 warning, 1 info) still open:

  • ⚠️ bin/fm-axi-suite.sh:140 - Now that the two budgets are charged separately, FM_AXI_SUITE_NETWORK_TIMEOUT (default 30s, covering registry lookups, installs, and hook setup) becomes the binding constraint on the one-time cutover. Before this branch a normal run made six npm view calls (~9s); the first run in each home now additionally performs up to six npm install -g --prefix calls via seed_home_tool. At realistic npm timings (~1.5s per view, ~3-6s per install) the 30s budget is consumed around the fourth tool: that tool's install is killed mid-flight and reports AXI_SUITE_STUCK: &lt;tool&gt; vessel-prefix installation at &lt;prefix&gt; failed (:307), and the remaining tools get NET_GRANTED=0, so net_call npm view (:398) returns 124 immediately and each reports AXI_SUITE_STUCK: &lt;tool&gt; latest version lookup failed. Those lines are copied to state/axi-suite-update.stuck (:452) and re-emitted by emit_cached on every invocation for the rest of the cadence window. Nothing is corrupted and the home converges over one or two more windows, but the bootstrap-diagnostics skill instructs an agent to investigate AXI_SUITE_STUCK and relay it via a crewmate, so a healthy home's first cutover likely raises a false alarm. The tests miss it because the stub npm is instantaneous and the one slow-timing test raises FM_AXI_SUITE_NETWORK_TIMEOUT to 60. Options: raise the default, give the one-time seeding installs their own accumulator the way probes just got one, or emit a softer non-STUCK token when seeding is cut short purely by budget exhaustion.
  • ℹ️ bin/fm-axi-suite.sh:118 - NET_GRANTED is declared at file scope but is only ever assigned and read inside net_call (:142-145); nothing outside consumes it. PROBE_GRANTED is genuinely global because read_version (:233) and the timeout message (:352) read it after the call returns. Making NET_GRANTED a local in net_call keeps the two apart and makes it obvious which grant is part of the contract with callers.

🔧 Fix: give first-cutover seeding its own bounded budget
2 warnings still open:

  • ⚠️ bin/fm-axi-suite.sh:506 - seed_home_tool gained a SEED_GRANTED -eq 0 branch (:369) that reports a spent seeding budget as vessel-prefix seeding ... was not attempted, but the patch/minor update call site at :505 has no equivalent check and always prints AXI_SUITE_STUCK: &lt;tool&gt; automatic update &lt;old&gt; -&gt; &lt;new&gt; failed (npm prefix ...). When the vessel copy is absent and the external copy is behind the registry, the loop skips the three ensure_home_copy branches (:478, :487, :492) and reaches :505, where install_home_tool correctly charges the seed budget because [ -x &#34;$path&#34; ] is false - so on a first cutover of a home whose shared installs are stale, every tool seeds through this path and seed_home_tool's disambiguation is never reached at all. If the 120s seeding budget then runs out mid-cutover, the remaining tools report a broken install rather than an unattempted seed, which is the exact misleading alarm this commit set out to remove, and the bootstrap-diagnostics skill will send an agent to investigate the local install path. test_stalled_first_cutover_alarms_and_stays_bounded uses tools where installed == latest, so it exercises only the seed_home_tool branch. Apply the same SEED_GRANTED -eq 0 check at :505 (or hoist it into install_home_tool's callers) so both entry points describe the same condition identically.
  • ⚠️ bin/fm-axi-suite.sh:307 - announce_seeding writes to stderr on the stated premise that "stdout is collected and emitted once at the end" so a fresh vessel shows live progress "inside a locked bootstrap where the operator sees only a pause"; docs/configuration.md repeats that rationale. But bin/fm-session-start.sh:278 invokes bootstrap as BOOT_OUT=$(&#34;$SCRIPT_DIR/fm-bootstrap.sh&#34; 2&gt;&amp;1), so stderr is captured into the same command substitution as stdout and nothing is printed until bootstrap exits. In the automated session-start path the progress lines are buffered exactly like the diagnostics and appear all at once in the digest's BOOTSTRAP section afterwards, so they cannot distinguish normal installation work from a hang while it is happening - live streaming only works when a captain runs bin/fm-bootstrap.sh by hand in a terminal. Deciding how to surface it (write to the controlling terminal, have session-start stream bootstrap rather than capture it, or accept post-hoc reporting and correct the two rationale comments) is a behavior choice worth confirming.

🔧 Fix: report spent seeding budget identically from both paths
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bin/fm-test-run.sh tests/fm-axi-suite.test.sh — 21 behaviors incl. concurrent two-home updates, PATH precedence, cutover currency clock, unreadable/hung vessel copy handling, seeding-budget stall reporting
  • bin/fm-test-run.sh tests/fm-bootstrap.test.sh tests/fm-spawn-dispatch-profile.test.sh tests/fm-runtime-ignore.test.sh tests/fm-gotmp.test.sh tests/fm-dispatch-select.test.sh
  • Manual end-to-end demo driving the real bin/fm-axi-suite.sh, bin/fm-bootstrap.sh and bin/fm-spawn.sh for two vessels against a fake npm registry and fake tmux pane: FM_DEMO_REPO=$PWD bash demo-vessel-axi-isolation.sh
  • Concurrent cutover of vessel-alpha and vessel-bravo, then inspection of each vessel's npm call log, resulting binaries, and the untouched shared npm-global copy
  • PATH resolution checks: README primary launch line, fm_axi_prepend_path with the shared bin listed first, plus the literal export PATH=... line fm-spawn types for a crewmate and for a secondmate
  • Currency-clock check after cutover: silent cached run inside the window with zero npm traffic, then --force picking up 1.0.1 -> 1.0.2 for alpha only
  • Policy check: registry offering 2.0.0 yields AXI_SUITE_REVIEW: with the vessel-prefix install hint and leaves the vessel copy at 1.0.2
  • bin/fm-bootstrap.sh MISSING disclosure and bin/fm-bootstrap.sh install gh-axi on a fresh home, verifying the npm call, the installed binary location, and the hook-setup invocation
  • git show ce8309f:bin/fm-axi-suite.sh | grep &#39;^SUITE=&#39; vs current, and grep &#39;uninstall|rm -f|rm -rf&#39; bin/fm-axi-suite.sh to confirm the tool set and that no shared install is removed
⚠️ **Document** - 3 issues (1 warning, 2 infos)
  • ⚠️ docs/admiralty-fleet-repo.md:63 - docs/admiralty-fleet-repo.md:63 states that the external fleet repo's fleet/bin/fmf-vendor-bump.sh treats data/, state/, config/, projects/, .no-mistakes/, graphify-out/, and .env as structurally out of reach. That is still an accurate report of that script, but an admiralty-deployed vessel now also creates the home-private .local/axi/ prefix, which the protected set does not cover. The fix belongs in the admiralty repo's script, not here, so I did not edit the sentence describing it.
  • ℹ️ AGENTS.md:53 - AGENTS.md:53 and CONTRIBUTING.md:41 both enumerate captain-private gitignored paths (.env, data/, state/, config/, projects/, .no-mistakes/, graphify-out/) and now omit .local/axi/. I deliberately left both alone: they already omitted .lavish/ before this change, so they read as illustrative rather than exhaustive, and the tracked root .gitignore (updated by this change) is the enforcing owner. Adding the entry would mean maintaining a third and fourth prose copy of that list.
  • ℹ️ docs/configuration.md:556 - The ## Environment variables block in docs/configuration.md lists no FM_AXI_SUITE_* variable at all, so the two new ones (FM_AXI_SUITE_PROBE_TIMEOUT, FM_AXI_SUITE_SEED_TIMEOUT) join three pre-existing absentees rather than creating fresh drift. The block covers roughly 122 of ~400 FM_* variables, and the prose owner (docs/configuration.md "AXI-suite self-update") does document all five, so I left it. A follow-up could decide whether that block is meant to be an inventory and reconcile it in one pass.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@Freudator86
Freudator86 merged commit 8502757 into main Jul 28, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant