ops(q81): mosaic on PATH runs the pinned mosaic-stack CLI (sage-0042)
agents/sage/work/q81/mosaic-cli.sh does install, update, undo and status.
~/.npm-global/bin/mosaic is now a wrapper that execs scripts/mosaic in a
detached worktree at ~/.local/share/mosaic-cli/stack, pinned to pushed
ee129aa6. The estate CLI symlink was renamed in place to mosaic-old.
Only commits on origin/refactor can be pinned. `mosaic queue` refuses
from the worktree (the queue runs only in the canonical checkout), so
queue work stays on scripts/mosaic. status flags a wrapper replaced by
`npm i -g --force`.
lease-gate-notes.md: no single environment location for
MOSAIC_LEASE_VERSION_PROBE_COMMAND exists outside fleet state, so the
old-path lease gate fails closed, as Mos ruled for that case.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# Q81 (2): lease gate env, read-only findings, 2026-10-11
|
||||
|
||||
- activation_version_gate.py:94-104 reads MOSAIC_LEASE_VERSION_PROBE_COMMAND from the
|
||||
environment it is handed (shlex.split), else `shutil.which("mosaic", PATH)`.
|
||||
- launch-runtime.py:129 uses os.environ unchanged; it inherits the old CLI's process env,
|
||||
which inherits from whatever runs the old CLI (shell, provisioning, a unit). It reads no
|
||||
env file of its own.
|
||||
- So there is no single env location to set. Candidates and their problems:
|
||||
- ~/.config/environment.d/*.conf: reaches systemd user services only, not shells.
|
||||
- shell rc files: Jason's dotfiles, out of scope.
|
||||
- fleet unit drop-ins or ~/.mosaic / ~/.config/mosaic files: fleet state, read-only for me.
|
||||
- Alternative inside files Q81 owns: the new wrapper forwards only `__lease-capability`
|
||||
to ~/.npm-global/bin/mosaic-old. Same effect as the env var (the real old CLI answers),
|
||||
no env file, removed by undo. Needs Mos's ok, since it differs from the ruling's mechanism.
|
||||
Executable
+138
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env bash
|
||||
# Q81 (sage-0042): put the mosaic-stack CLI on PATH as `mosaic`.
|
||||
#
|
||||
# mosaic-cli.sh install [REF] rename the npm-global `mosaic` to `mosaic-old`,
|
||||
# add a pinned worktree and the wrapper
|
||||
# mosaic-cli.sh update [REF] move the pinned worktree to REF
|
||||
# mosaic-cli.sh undo remove the wrapper and the worktree, rename
|
||||
# `mosaic-old` back to `mosaic`
|
||||
# mosaic-cli.sh status what is installed, and whether it drifted
|
||||
#
|
||||
# REF defaults to origin/refactor after a fetch, and must already be on
|
||||
# origin/refactor: the wrapper runs pushed code only, never the working
|
||||
# checkout. The worktree is a detached linked worktree of the canonical
|
||||
# checkout, so `mosaic queue` refuses there (the queue runs only in the
|
||||
# canonical checkout); queue work stays on scripts/mosaic.
|
||||
#
|
||||
# Writes only: ~/.npm-global/bin/{mosaic,mosaic-old}, the worktree and
|
||||
# state directory below, and the canonical checkout's worktree list.
|
||||
set -euo pipefail
|
||||
|
||||
CANON=/mnt/storage/src/mosaic-stack
|
||||
BIN="$HOME/.npm-global/bin"
|
||||
STATE_DIR="$HOME/.local/share/mosaic-cli"
|
||||
WT="$STATE_DIR/stack"
|
||||
LOG="$STATE_DIR/install.log"
|
||||
OLD_TARGET=../lib/node_modules/@mosaicstack/mosaic/dist/cli.js
|
||||
MARKER="# mosaic-cli wrapper (Q81, agents/sage/work/q81/mosaic-cli.sh)"
|
||||
|
||||
die() { echo "mosaic-cli: $*" >&2; exit 1; }
|
||||
now() { date -u +%FT%TZ; }
|
||||
log() { mkdir -p "$STATE_DIR"; echo "$(now) $*" >>"$LOG"; }
|
||||
|
||||
is_wrapper() { [ -f "$BIN/mosaic" ] && [ ! -L "$BIN/mosaic" ] && sed -n 2p "$BIN/mosaic" | grep -qxF "$MARKER"; }
|
||||
is_old_link() { [ -L "$1" ] && [ "$(readlink "$1")" = "$OLD_TARGET" ]; }
|
||||
|
||||
# Fetch, then resolve REF to a full sha that origin/refactor contains.
|
||||
pinned_sha() {
|
||||
local ref="${1:-origin/refactor}" sha
|
||||
git -C "$CANON" fetch -q origin refactor || die "git fetch origin refactor failed"
|
||||
sha="$(git -C "$CANON" rev-parse --verify -q "$ref^{commit}")" || die "$ref is not a commit"
|
||||
git -C "$CANON" merge-base --is-ancestor "$sha" origin/refactor || die "$ref ($sha) is not on origin/refactor; only pushed code is pinned"
|
||||
echo "$sha"
|
||||
}
|
||||
|
||||
# npm ci only when the lock differs from what node_modules was built from.
|
||||
deps() {
|
||||
if [ -f "$WT/node_modules/.package-lock.json" ] && [ -f "$STATE_DIR/lock.sha256" ] \
|
||||
&& (cd "$WT" && sha256sum -c --status "$STATE_DIR/lock.sha256"); then
|
||||
echo "node_modules: lock unchanged, kept"
|
||||
return
|
||||
fi
|
||||
[ -f "$WT/package.json" ] || die "$WT/package.json missing; refusing npm ci (it would walk up)"
|
||||
(cd "$WT" && npm ci --prefer-offline --no-audit --no-fund >/dev/null) || die "npm ci in $WT failed"
|
||||
(cd "$WT" && sha256sum package-lock.json) >"$STATE_DIR/lock.sha256"
|
||||
echo "node_modules: npm ci done"
|
||||
}
|
||||
|
||||
write_wrapper() {
|
||||
local tmp
|
||||
tmp="$(mktemp "$BIN/.mosaic.XXXXXX")"
|
||||
cat >"$tmp" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
$MARKER
|
||||
# Runs the mosaic-stack CLI pinned in $WT.
|
||||
# The estate CLI it replaced is $BIN/mosaic-old.
|
||||
exec "$WT/scripts/mosaic" "\$@"
|
||||
EOF
|
||||
chmod 0755 "$tmp"
|
||||
mv -f "$tmp" "$BIN/mosaic"
|
||||
}
|
||||
|
||||
cmd_install() {
|
||||
is_wrapper && die "already installed; use update or undo"
|
||||
is_old_link "$BIN/mosaic" || die "$BIN/mosaic is not the expected symlink to $OLD_TARGET; refusing"
|
||||
[ -e "$BIN/mosaic-old" ] || [ -L "$BIN/mosaic-old" ] && die "$BIN/mosaic-old already exists; refusing"
|
||||
[ -e "$WT" ] && die "$WT already exists; refusing"
|
||||
local sha; sha="$(pinned_sha "${1:-}")"
|
||||
mkdir -p "$STATE_DIR"
|
||||
git -C "$CANON" worktree add -q --detach "$WT" "$sha" || die "worktree add failed"
|
||||
deps
|
||||
mv -n "$BIN/mosaic" "$BIN/mosaic-old"
|
||||
is_old_link "$BIN/mosaic-old" || die "rename to mosaic-old did not happen"
|
||||
write_wrapper
|
||||
log "install $sha"
|
||||
echo "installed: $BIN/mosaic -> $WT/scripts/mosaic at $sha"
|
||||
}
|
||||
|
||||
cmd_update() {
|
||||
is_wrapper || die "not installed (no wrapper at $BIN/mosaic)"
|
||||
local sha; sha="$(pinned_sha "${1:-}")"
|
||||
[ -z "$(git -C "$WT" status --porcelain --untracked-files=no)" ] || die "$WT has tracked changes; refusing"
|
||||
git -C "$WT" checkout -q --detach "$sha"
|
||||
deps
|
||||
log "update $sha"
|
||||
echo "updated: $WT at $sha"
|
||||
}
|
||||
|
||||
cmd_undo() {
|
||||
is_wrapper || die "$BIN/mosaic is not this wrapper; refusing (status says why)"
|
||||
is_old_link "$BIN/mosaic-old" || die "$BIN/mosaic-old is not the expected symlink; refusing"
|
||||
rm -f "$BIN/mosaic"
|
||||
mv -n "$BIN/mosaic-old" "$BIN/mosaic"
|
||||
is_old_link "$BIN/mosaic" || die "rename back to mosaic did not happen"
|
||||
if [ -e "$WT" ]; then git -C "$CANON" worktree remove --force "$WT"; fi
|
||||
rm -f "$STATE_DIR/lock.sha256"
|
||||
log "undo"
|
||||
echo "undone: $BIN/mosaic -> $OLD_TARGET again; worktree removed"
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
local bad=0 sha origin
|
||||
if is_wrapper; then echo "wrapper: $BIN/mosaic (marker ok)"
|
||||
elif is_old_link "$BIN/mosaic"; then echo "wrapper: none; $BIN/mosaic is the estate symlink"
|
||||
[ -e "$WT" ] && { echo "DRIFT: worktree exists but the wrapper is gone (an npm i -g --force?)"; bad=1; }
|
||||
else echo "DRIFT: $BIN/mosaic is neither the wrapper nor the estate symlink"; bad=1; fi
|
||||
if is_old_link "$BIN/mosaic-old"; then echo "mosaic-old: $BIN/mosaic-old -> $OLD_TARGET"
|
||||
elif [ -e "$BIN/mosaic-old" ] || [ -L "$BIN/mosaic-old" ]; then echo "DRIFT: $BIN/mosaic-old is not the expected symlink"; bad=1
|
||||
else echo "mosaic-old: none"; fi
|
||||
if [ -d "$WT" ]; then
|
||||
sha="$(git -C "$WT" rev-parse HEAD)"
|
||||
origin="$(git -C "$CANON" rev-parse origin/refactor)"
|
||||
echo "worktree: $WT at $sha"
|
||||
if [ "$sha" = "$origin" ]; then echo "pin: origin/refactor (as last fetched)"
|
||||
else echo "pin: behind origin/refactor $origin by $(git -C "$CANON" rev-list --count "$sha..$origin") commits"; fi
|
||||
[ -z "$(git -C "$WT" status --porcelain --untracked-files=no)" ] || { echo "DRIFT: worktree has tracked changes"; bad=1; }
|
||||
else echo "worktree: none"; fi
|
||||
echo "on PATH: $(command -v mosaic || echo none)"
|
||||
[ -f "$LOG" ] && echo "last: $(tail -n 1 "$LOG")"
|
||||
return "$bad"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
install) shift; cmd_install "$@" ;;
|
||||
update) shift; cmd_update "$@" ;;
|
||||
undo) cmd_undo ;;
|
||||
status) cmd_status ;;
|
||||
*) echo "usage: mosaic-cli.sh install [REF] | update [REF] | undo | status" >&2; exit 4 ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user