Compare commits
5 Commits
feat/fleet
...
feat/p4-up
| Author | SHA1 | Date | |
|---|---|---|---|
| d2d0279e92 | |||
| c2c0b5fe8d | |||
| c9cfe36204 | |||
| fc90c89913 | |||
| af2eede7a9 |
@@ -115,6 +115,11 @@ Every artifact, starting Phase 2, MUST:
|
|||||||
- Observation: **read-only default, opt-in takeover**.
|
- Observation: **read-only default, opt-in takeover**.
|
||||||
- Multi-host: **designed-for from day one**; control plane **rides federation (W1)**.
|
- Multi-host: **designed-for from day one**; control plane **rides federation (W1)**.
|
||||||
- Delivery: **CLI-first now**, dogfood against the live stub fleet; webUI deferred to Phase 5.
|
- Delivery: **CLI-first now**, dogfood against the live stub fleet; webUI deferred to Phase 5.
|
||||||
|
- Runtimes: fleet agents default to **Codex / pi-on-Codex**; **Claude is reserved for Claude
|
||||||
|
Code only** (avoid alternate-harness API pricing). Validated durable recipe:
|
||||||
|
`mosaic yolo pi --model openai-codex/gpt-5.5:high`. Durable detached launch requires the
|
||||||
|
runtime-bin on PATH (baked into the pane command) + boot-survival (`enable` + linger),
|
||||||
|
which `fleet init` should automate.
|
||||||
|
|
||||||
## Assumptions (veto-able)
|
## Assumptions (veto-able)
|
||||||
|
|
||||||
|
|||||||
@@ -73,3 +73,28 @@ with a second agent on `dragon-lin`.
|
|||||||
tmux session-name fallback; the systemd/tmux env handoff needs a real fix.
|
tmux session-name fallback; the systemd/tmux env handoff needs a real fix.
|
||||||
- Next: rebase on merged main, open Phase-2 PR, dual-engine review, merge, close
|
- Next: rebase on merged main, open Phase-2 PR, dual-engine review, merge, close
|
||||||
`fleet-observability-1`. Defer launch-path + env-propagation fixes to Phase 3.
|
`fleet-observability-1`. Defer launch-path + env-propagation fixes to Phase 3.
|
||||||
|
- 2026-06-21 (session 3): Phase-2 PR #579 merged (3 dual-engine rounds hardened
|
||||||
|
verify+watch). Then closed the launch-path question with Jason's input — CORRECTING
|
||||||
|
earlier findings:
|
||||||
|
- The ad-hoc launch deaths were NOT a fundamental TTY blocker: (a) codex was a stale
|
||||||
|
version (Jason updated it); (b) pi was misconfigured to Claude auth (Jason removed it;
|
||||||
|
default is now Codex). The REAL durable-launch bug is **PATH**: the detached tmux
|
||||||
|
launch shell is login+non-interactive, so it misses `~/.npm-global/bin` (added only in
|
||||||
|
`~/.bashrc`) -> `mosaic: command not found` (127) -> pane dies. tmux panes inherit the
|
||||||
|
tmux _server_ env, so PATH must be baked into the pane command.
|
||||||
|
- **Durable real-agent recipe (validated live on gpt-5.5, Claude-free):**
|
||||||
|
`mosaic yolo pi --model openai-codex/gpt-5.5:high` — pi tolerates detached tmux; a raw
|
||||||
|
interactive TUI (codex CLI) exits without an attached client. Status line confirmed
|
||||||
|
`(openai-codex) gpt-5.5 • high`.
|
||||||
|
- PATH fix landed in `start-agent-session.sh` (commit 32efc13, branch
|
||||||
|
feat/fleet-launch-path): derive runtime-bin prefix (MOSAIC_RUNTIME_BIN | npm prefix |
|
||||||
|
~/.npm-global/bin | ~/.local/bin), bake `export PATH=...; exec <cmd>` into the pane;
|
||||||
|
`exec` also fixes the drift false-positive. Live-tested under stripped PATH -> durable.
|
||||||
|
- Boot-survival: Jason ran `systemctl --user enable` (+ linger). TODO: auto-enable in
|
||||||
|
**fleet init** so operators never have to remember it (agentic-enhancement cycle).
|
||||||
|
- Future custom Pi harness build: pi cannot self-report its model (track
|
||||||
|
runtime/model/effort as fleet metadata); drift detection should recognize `node` as
|
||||||
|
pi's pane command (a node-wrapped pane can currently read as drift).
|
||||||
|
- Findings recorded in AI Guide playbooks/tmux-fleet.md (aiguide PR #7, merged).
|
||||||
|
- Policy: avoid Claude outside Claude Code (API pricing for alt-harness use) — fleet
|
||||||
|
runtimes default to Codex / pi-on-Codex; Claude stays in Claude Code only.
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ Skills, hooks, MCP, and plugins are force multipliers you MUST use when applicab
|
|||||||
## Missing core file
|
## Missing core file
|
||||||
|
|
||||||
If `CONSTITUTION.md`, `AGENTS.md`, `SOUL.md`, or the runtime contract is missing, stop and report it.
|
If `CONSTITUTION.md`, `AGENTS.md`, `SOUL.md`, or the runtime contract is missing, stop and report it.
|
||||||
|
This agent-facing strictness is intentional and stricter than the launcher: the launcher injects
|
||||||
|
`CONSTITUTION.md` tolerantly (skipping it if absent so pre-upgrade hosts keep working), but once a host
|
||||||
|
is re-seeded a genuinely missing core file is a stop-and-report condition — not something to proceed past.
|
||||||
|
|
||||||
## Session Closure
|
## Session Closure
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
The irreducible, non-negotiable law for every Mosaic agent on every harness.
|
The irreducible, non-negotiable law for every Mosaic agent on every harness.
|
||||||
|
|
||||||
**Framework-owned.** This file is overwritten verbatim on every upgrade — do not edit it. To change
|
**Framework-owned.** This file is overwritten verbatim on every upgrade — do not edit it. There is
|
||||||
behavior, add a `.local.md` overlay or a `policy/` file (tighten-only; see `constitution/LAYER-MODEL.md`).
|
**no `CONSTITUTION.local.md`**: hard gates are not locally overridable. A lower layer may only make
|
||||||
|
behavior _stricter_, never relax or override a gate (see Precedence). Operator customization lives in
|
||||||
|
other layers — `SOUL.md` / `USER.md` and the tighten-only overlays `STANDARDS.local.md` /
|
||||||
|
`SOUL.local.md` / `USER.local.md` / `policy/*.md` (see `constitution/LAYER-MODEL.md`).
|
||||||
Authored in **capability verbs**: where a gate names a capability ("structured reasoning", "queue
|
Authored in **capability verbs**: where a gate names a capability ("structured reasoning", "queue
|
||||||
guard"), the runtime adapter binds it to a concrete tool and states whether absence is a hard stop.
|
guard"), the runtime adapter binds it to a concrete tool and states whether absence is a hard stop.
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,19 @@ INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}"
|
|||||||
|
|
||||||
# Files/dirs preserved across upgrades (never overwritten).
|
# Files/dirs preserved across upgrades (never overwritten).
|
||||||
# User-created content in these paths survives rsync --delete.
|
# User-created content in these paths survives rsync --delete.
|
||||||
PRESERVE_PATHS=("AGENTS.md" "SOUL.md" "USER.md" "TOOLS.md" "STANDARDS.md" "memory" "sources" "credentials")
|
PRESERVE_PATHS=("CONSTITUTION.md" "AGENTS.md" "SOUL.md" "USER.md" "TOOLS.md" "STANDARDS.md" "memory" "sources" "credentials")
|
||||||
|
|
||||||
|
# Framework-owned contract files: re-copied from defaults/ on every upgrade (the
|
||||||
|
# user must not edit them; a divergent copy is backed up once before overwrite).
|
||||||
|
# USER_SEEDED files are written once on first install, then owned by the user.
|
||||||
|
# Both lists are APPEND-FRIENDLY — add a new shipped framework file here and to the
|
||||||
|
# matching list in packages/mosaic/src/config/file-adapter.ts.
|
||||||
|
FRAMEWORK_OWNED=("CONSTITUTION.md" "AGENTS.md" "STANDARDS.md")
|
||||||
|
USER_SEEDED=("TOOLS.md")
|
||||||
|
|
||||||
# Current framework schema version — bump this when the layout changes.
|
# Current framework schema version — bump this when the layout changes.
|
||||||
# The migration system uses this to run upgrade steps.
|
# The migration system uses this to run upgrade steps.
|
||||||
FRAMEWORK_VERSION=2
|
FRAMEWORK_VERSION=3
|
||||||
|
|
||||||
# ─── colours ──────────────────────────────────────────────────────────────────
|
# ─── colours ──────────────────────────────────────────────────────────────────
|
||||||
if [[ -t 1 ]]; then
|
if [[ -t 1 ]]; then
|
||||||
@@ -40,6 +48,45 @@ warn() { echo -e " ${YELLOW}⚠${RESET} $1" >&2; }
|
|||||||
fail() { echo -e " ${RED}✗${RESET} $1" >&2; }
|
fail() { echo -e " ${RED}✗${RESET} $1" >&2; }
|
||||||
step() { echo -e "\n${BOLD}$1${RESET}"; }
|
step() { echo -e "\n${BOLD}$1${RESET}"; }
|
||||||
|
|
||||||
|
# ─── snapshot / restore (crash safety for upgrades) ──────────────────────────
|
||||||
|
SNAPSHOT_DIR=""
|
||||||
|
make_snapshot() {
|
||||||
|
is_existing_install || return 0
|
||||||
|
SNAPSHOT_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-snapshot-XXXXXX")"
|
||||||
|
cp -a "$TARGET_DIR/." "$SNAPSHOT_DIR/" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
restore_snapshot() {
|
||||||
|
[[ -n "$SNAPSHOT_DIR" && -d "$SNAPSHOT_DIR" ]] || return 0
|
||||||
|
fail "Install interrupted/failed — restoring previous state from snapshot"
|
||||||
|
rm -rf "$TARGET_DIR"; mkdir -p "$TARGET_DIR"
|
||||||
|
cp -a "$SNAPSHOT_DIR/." "$TARGET_DIR/" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
cleanup_snapshot() { [[ -n "$SNAPSHOT_DIR" && -d "$SNAPSHOT_DIR" ]] && rm -rf "$SNAPSHOT_DIR"; SNAPSHOT_DIR=""; }
|
||||||
|
|
||||||
|
# Reconcile contract files after sync: framework-owned overwrite (backup-once),
|
||||||
|
# user-seeded seed-if-absent.
|
||||||
|
reconcile_framework_files() {
|
||||||
|
local defaults="$TARGET_DIR/defaults" f
|
||||||
|
[[ -d "$defaults" ]] || return 0
|
||||||
|
for f in "${FRAMEWORK_OWNED[@]}"; do
|
||||||
|
[[ -f "$defaults/$f" ]] || continue
|
||||||
|
if [[ -f "$TARGET_DIR/$f" ]] && ! cmp -s "$TARGET_DIR/$f" "$defaults/$f"; then
|
||||||
|
if [[ ! -f "$TARGET_DIR/${f}.pre-constitution.bak" ]]; then
|
||||||
|
cp "$TARGET_DIR/$f" "$TARGET_DIR/${f}.pre-constitution.bak"
|
||||||
|
warn "$f is now framework-owned and was updated; your previous copy is saved as ${f}.pre-constitution.bak — re-apply intended changes as a .local overlay or policy/ file (see CONSTITUTION.md / constitution/LAYER-MODEL.md)."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
cp "$defaults/$f" "$TARGET_DIR/$f"
|
||||||
|
done
|
||||||
|
for f in "${USER_SEEDED[@]}"; do
|
||||||
|
[[ -f "$defaults/$f" ]] || continue
|
||||||
|
if [[ ! -f "$TARGET_DIR/$f" ]]; then
|
||||||
|
cp "$defaults/$f" "$TARGET_DIR/$f"
|
||||||
|
ok "Seeded $f from defaults"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# ─── helpers ──────────────────────────────────────────────────────────────────
|
# ─── helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
is_existing_install() {
|
is_existing_install() {
|
||||||
@@ -113,11 +160,14 @@ sync_framework() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if command -v rsync >/dev/null 2>&1; then
|
if command -v rsync >/dev/null 2>&1; then
|
||||||
local rsync_args=(-a --delete --exclude ".git" --exclude ".framework-version")
|
local rsync_args=(-a --delete --exclude ".git" --exclude ".framework-version" --exclude "*.pre-constitution.bak")
|
||||||
|
|
||||||
if [[ "$INSTALL_MODE" == "keep" ]]; then
|
if [[ "$INSTALL_MODE" == "keep" ]]; then
|
||||||
|
# Anchor to the transfer root (leading /) so we preserve the TOP-LEVEL
|
||||||
|
# ~/.config/mosaic/<file> without also excluding defaults/<file> from sync
|
||||||
|
# (reconcile_framework_files needs the freshly-synced defaults/ copies).
|
||||||
for path in "${PRESERVE_PATHS[@]}"; do
|
for path in "${PRESERVE_PATHS[@]}"; do
|
||||||
rsync_args+=(--exclude "$path")
|
rsync_args+=(--exclude "/$path")
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -137,7 +187,7 @@ sync_framework() {
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
find "$TARGET_DIR" -mindepth 1 -maxdepth 1 ! -name ".git" ! -name ".framework-version" -exec rm -rf {} +
|
find "$TARGET_DIR" -mindepth 1 -maxdepth 1 ! -name ".git" ! -name ".framework-version" ! -name "*.pre-constitution.bak" -exec rm -rf {} +
|
||||||
cp -R "$SOURCE_DIR"/. "$TARGET_DIR"/
|
cp -R "$SOURCE_DIR"/. "$TARGET_DIR"/
|
||||||
rm -rf "$TARGET_DIR/.git"
|
rm -rf "$TARGET_DIR/.git"
|
||||||
|
|
||||||
@@ -195,10 +245,15 @@ run_migrations() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Future migrations go here ──────────────────────────────────────────────
|
# ── Migration: v2 → v3 (Constitution split) ───────────────────────────────
|
||||||
# if [[ "$from_version" -lt 3 ]]; then
|
# CONSTITUTION.md / AGENTS.md / STANDARDS.md become framework-owned (overwritten
|
||||||
# ...
|
# on upgrade). reconcile_framework_files() has already run before this point: it
|
||||||
# fi
|
# backed up any user-edited copy to <file>.pre-constitution.bak and installed the
|
||||||
|
# new framework version. Nothing further to do here — the advisory was emitted at
|
||||||
|
# reconcile time. (STANDARDS.local.md composition lands with the overlay composer.)
|
||||||
|
if [[ "$from_version" -lt 3 ]]; then
|
||||||
|
ok "Migrated to the Constitution layout (framework-owned CONSTITUTION/AGENTS/STANDARDS)"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -216,6 +271,10 @@ else
|
|||||||
ok "Install mode: overwrite"
|
ok "Install mode: overwrite"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Snapshot before any destructive file operation; restore on interrupt/failure.
|
||||||
|
make_snapshot
|
||||||
|
trap 'restore_snapshot' ERR INT TERM
|
||||||
|
|
||||||
sync_framework
|
sync_framework
|
||||||
|
|
||||||
# Ensure persistent directories exist
|
# Ensure persistent directories exist
|
||||||
@@ -230,15 +289,7 @@ mkdir -p "$TARGET_DIR/credentials"
|
|||||||
# packages/mosaic/src/config/file-adapter.ts (FileConfigAdapter.syncFramework).
|
# packages/mosaic/src/config/file-adapter.ts (FileConfigAdapter.syncFramework).
|
||||||
# SOUL.md and USER.md are intentionally NOT seeded here — they are generated
|
# SOUL.md and USER.md are intentionally NOT seeded here — they are generated
|
||||||
# by `mosaic init` from templates with user-supplied values.
|
# by `mosaic init` from templates with user-supplied values.
|
||||||
DEFAULTS_DIR="$TARGET_DIR/defaults"
|
reconcile_framework_files
|
||||||
if [[ -d "$DEFAULTS_DIR" ]]; then
|
|
||||||
for default_file in CONSTITUTION.md AGENTS.md STANDARDS.md TOOLS.md; do
|
|
||||||
if [[ -f "$DEFAULTS_DIR/$default_file" ]] && [[ ! -f "$TARGET_DIR/$default_file" ]]; then
|
|
||||||
cp "$DEFAULTS_DIR/$default_file" "$TARGET_DIR/$default_file"
|
|
||||||
ok "Seeded $default_file from defaults"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure tool scripts are executable
|
# Ensure tool scripts are executable
|
||||||
find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
|
find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
|
||||||
@@ -249,6 +300,18 @@ ok "Framework synced to $TARGET_DIR"
|
|||||||
# Run migrations before post-install (migrations may remove old bin/ etc.)
|
# Run migrations before post-install (migrations may remove old bin/ etc.)
|
||||||
run_migrations
|
run_migrations
|
||||||
|
|
||||||
|
# File-system phase complete and consistent — clear the restore trap.
|
||||||
|
trap - ERR INT TERM
|
||||||
|
cleanup_snapshot
|
||||||
|
|
||||||
|
# Testability / minimal-install hook: stop after the file-system phase, before any
|
||||||
|
# environment-touching post-install steps (runtime linking, MCP setup, skills, doctor).
|
||||||
|
if [[ "${MOSAIC_SYNC_ONLY:-0}" == "1" ]]; then
|
||||||
|
write_framework_version
|
||||||
|
ok "Sync-only mode: file phase complete"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
step "Post-install tasks"
|
step "Post-install tasks"
|
||||||
|
|
||||||
SCRIPTS="$TARGET_DIR/tools/_scripts"
|
SCRIPTS="$TARGET_DIR/tools/_scripts"
|
||||||
|
|||||||
@@ -26,5 +26,75 @@ if [ -z "$MOSAIC_AGENT_COMMAND" ]; then
|
|||||||
MOSAIC_AGENT_COMMAND="mosaic yolo $MOSAIC_AGENT_RUNTIME"
|
MOSAIC_AGENT_COMMAND="mosaic yolo $MOSAIC_AGENT_RUNTIME"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Derive a runtime-bin PATH prefix ─────────────────────────────────────────
|
||||||
|
# Precedence:
|
||||||
|
# 1. $MOSAIC_RUNTIME_BIN (explicit override)
|
||||||
|
# 2. $(npm config get prefix)/bin (if npm is on PATH)
|
||||||
|
# 3. Fallbacks: $HOME/.npm-global/bin and $HOME/.local/bin
|
||||||
|
#
|
||||||
|
# Only directories that already exist are included. The prefix is baked into
|
||||||
|
# the pane command regardless of what the LAUNCHER process's $PATH contains,
|
||||||
|
# because the tmux pane inherits the tmux SERVER environment (not this script's
|
||||||
|
# environment). A dir on the launcher's PATH may be absent from the server PATH,
|
||||||
|
# so every existing candidate must always be included. Dedup within the
|
||||||
|
# constructed prefix avoids listing the same dir twice.
|
||||||
|
_build_runtime_bin_prefix() {
|
||||||
|
local candidates=()
|
||||||
|
|
||||||
|
if [ -n "${MOSAIC_RUNTIME_BIN:-}" ]; then
|
||||||
|
candidates+=("$MOSAIC_RUNTIME_BIN")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v npm >/dev/null 2>&1; then
|
||||||
|
local npm_prefix
|
||||||
|
npm_prefix=$(npm config get prefix 2>/dev/null) || true
|
||||||
|
if [ -n "$npm_prefix" ]; then
|
||||||
|
candidates+=("${npm_prefix}/bin")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
candidates+=("$HOME/.npm-global/bin")
|
||||||
|
candidates+=("$HOME/.local/bin")
|
||||||
|
|
||||||
|
local prefix=""
|
||||||
|
for dir in "${candidates[@]}"; do
|
||||||
|
[ -d "$dir" ] || continue
|
||||||
|
if [ -z "$prefix" ]; then
|
||||||
|
prefix="$dir"
|
||||||
|
else
|
||||||
|
case ":${prefix}:" in
|
||||||
|
*":${dir}:"*) ;; # already in our prefix — skip
|
||||||
|
*) prefix="${prefix}:${dir}" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '%s' "$prefix"
|
||||||
|
}
|
||||||
|
|
||||||
|
MOSAIC_RUNTIME_BIN_PREFIX=$(_build_runtime_bin_prefix)
|
||||||
|
|
||||||
|
# ── Build the pane command ────────────────────────────────────────────────────
|
||||||
|
# The pane command must:
|
||||||
|
# - Export the augmented PATH so the runtime binary is found.
|
||||||
|
# - exec the agent command so the runtime is the pane's foreground process
|
||||||
|
# (makes `fleet ps` pane_current_command check reliable; no DRIFT false-positive).
|
||||||
|
#
|
||||||
|
# Quoting strategy: single-quote the inner shell snippet so that variable
|
||||||
|
# references in MOSAIC_AGENT_COMMAND are NOT expanded here — they expand inside
|
||||||
|
# the pane shell. However, MOSAIC_RUNTIME_BIN_PREFIX and PATH must be expanded
|
||||||
|
# NOW (in this script) because the pane shell inherits the tmux server
|
||||||
|
# environment, not this script's env.
|
||||||
|
#
|
||||||
|
# We build the snippet as a double-quoted here-string embedded in a printf call
|
||||||
|
# to avoid nested quoting problems.
|
||||||
|
|
||||||
|
if [ -n "$MOSAIC_RUNTIME_BIN_PREFIX" ]; then
|
||||||
|
PANE_SHELL_SNIPPET="export PATH=\"${MOSAIC_RUNTIME_BIN_PREFIX}:\${PATH}\"; exec ${MOSAIC_AGENT_COMMAND}"
|
||||||
|
else
|
||||||
|
PANE_SHELL_SNIPPET="exec ${MOSAIC_AGENT_COMMAND}"
|
||||||
|
fi
|
||||||
|
|
||||||
mkdir -p "$MOSAIC_AGENT_WORKDIR"
|
mkdir -p "$MOSAIC_AGENT_WORKDIR"
|
||||||
exec tmux -L "$MOSAIC_TMUX_SOCKET" new-session -d -s "$AGENT_NAME" -c "$MOSAIC_AGENT_WORKDIR" "$MOSAIC_AGENT_COMMAND"
|
exec tmux -L "$MOSAIC_TMUX_SOCKET" new-session -d -s "$AGENT_NAME" -c "$MOSAIC_AGENT_WORKDIR" \
|
||||||
|
bash -c "$PANE_SHELL_SNIPPET"
|
||||||
|
|||||||
@@ -6,13 +6,26 @@ START="$SCRIPT_DIR/start-agent-session.sh"
|
|||||||
SOCKET="mosaic-agent-test-$RANDOM-$$"
|
SOCKET="mosaic-agent-test-$RANDOM-$$"
|
||||||
AGENT="agent-$RANDOM"
|
AGENT="agent-$RANDOM"
|
||||||
WORKDIR=$(mktemp -d)
|
WORKDIR=$(mktemp -d)
|
||||||
trap 'tmux -L "$SOCKET" kill-server >/dev/null 2>&1 || true; rm -rf "$WORKDIR"' EXIT
|
|
||||||
|
# Keep a single cleanup trap that accumulates resources.
|
||||||
|
CLEANUP_DIRS=("$WORKDIR")
|
||||||
|
CLEANUP_SOCKETS=("$SOCKET")
|
||||||
|
trap '_cleanup' EXIT
|
||||||
|
_cleanup() {
|
||||||
|
for s in "${CLEANUP_SOCKETS[@]:-}"; do
|
||||||
|
tmux -L "$s" kill-server >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
for d in "${CLEANUP_DIRS[@]:-}"; do
|
||||||
|
rm -rf "$d"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
echo "FAIL: $*" >&2
|
echo "FAIL: $*" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Test 1: basic session creation with workdir check ─────────────────────────
|
||||||
MOSAIC_TMUX_SOCKET="$SOCKET" \
|
MOSAIC_TMUX_SOCKET="$SOCKET" \
|
||||||
MOSAIC_AGENT_WORKDIR="$WORKDIR" \
|
MOSAIC_AGENT_WORKDIR="$WORKDIR" \
|
||||||
MOSAIC_AGENT_COMMAND='bash --noprofile --norc -i' \
|
MOSAIC_AGENT_COMMAND='bash --noprofile --norc -i' \
|
||||||
@@ -22,6 +35,7 @@ tmux -L "$SOCKET" has-session -t "=$AGENT:0.0" || fail "agent session was not cr
|
|||||||
actual_dir=$(tmux -L "$SOCKET" display-message -p -t "=$AGENT:0.0" '#{pane_current_path}')
|
actual_dir=$(tmux -L "$SOCKET" display-message -p -t "=$AGENT:0.0" '#{pane_current_path}')
|
||||||
[ "$actual_dir" = "$WORKDIR" ] || fail "agent workdir mismatch: $actual_dir"
|
[ "$actual_dir" = "$WORKDIR" ] || fail "agent workdir mismatch: $actual_dir"
|
||||||
|
|
||||||
|
# ── Test 2: idempotency (duplicate start prints 'already running') ─────────────
|
||||||
MOSAIC_TMUX_SOCKET="$SOCKET" \
|
MOSAIC_TMUX_SOCKET="$SOCKET" \
|
||||||
MOSAIC_AGENT_WORKDIR="$WORKDIR" \
|
MOSAIC_AGENT_WORKDIR="$WORKDIR" \
|
||||||
MOSAIC_AGENT_COMMAND='bash --noprofile --norc -i' \
|
MOSAIC_AGENT_COMMAND='bash --noprofile --norc -i' \
|
||||||
@@ -29,4 +43,166 @@ MOSAIC_AGENT_COMMAND='bash --noprofile --norc -i' \
|
|||||||
|
|
||||||
grep -qF 'already running' /tmp/mosaic-start-agent-idempotent.out || fail "duplicate start was not idempotent"
|
grep -qF 'already running' /tmp/mosaic-start-agent-idempotent.out || fail "duplicate start was not idempotent"
|
||||||
|
|
||||||
|
# ── Test 3: runtime-bin PATH prefix is baked into the pane command ────────────
|
||||||
|
#
|
||||||
|
# We capture the command the script would hand to tmux by injecting a fake
|
||||||
|
# 'tmux' shim into PATH. The shim:
|
||||||
|
# - Intercepts 'new-session' calls and records its arguments to a file.
|
||||||
|
# - For 'has-session' calls, exits 1 (session does not exist) so the script
|
||||||
|
# proceeds to launch instead of printing "already running".
|
||||||
|
# - For all other subcommands, exits 0.
|
||||||
|
#
|
||||||
|
# Assertions:
|
||||||
|
# a) 'export PATH=' with the synthetic MOSAIC_RUNTIME_BIN prefix appears.
|
||||||
|
# b) 'exec' appears so the runtime replaces the wrapper shell.
|
||||||
|
# c) MOSAIC_AGENT_COMMAND with flags is forwarded intact.
|
||||||
|
|
||||||
|
FAKE_BIN=$(mktemp -d)
|
||||||
|
FAKE_RUNTIME_BIN=$(mktemp -d)
|
||||||
|
TMUX_ARGS_FILE=$(mktemp)
|
||||||
|
CLEANUP_DIRS+=("$FAKE_BIN" "$FAKE_RUNTIME_BIN")
|
||||||
|
|
||||||
|
# Write the fake tmux shim (uses only positional args, no sourced vars).
|
||||||
|
cat > "$FAKE_BIN/tmux" <<SHIM
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# Fake tmux: record new-session args; report has-session as missing.
|
||||||
|
subcmd="\$3" # argv: tmux -L <socket> <subcmd> ...
|
||||||
|
if [ "\$subcmd" = "has-session" ]; then
|
||||||
|
exit 1 # session not found → script will attempt new-session
|
||||||
|
fi
|
||||||
|
if [ "\$subcmd" = "new-session" ]; then
|
||||||
|
printf '%s\n' "\$@" > "$TMUX_ARGS_FILE"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
SHIM
|
||||||
|
chmod +x "$FAKE_BIN/tmux"
|
||||||
|
|
||||||
|
SOCKET3="mosaic-agent-test3-$RANDOM-$$"
|
||||||
|
AGENT3="agent3-$RANDOM"
|
||||||
|
WORKDIR3=$(mktemp -d)
|
||||||
|
CLEANUP_DIRS+=("$WORKDIR3")
|
||||||
|
|
||||||
|
PATH="$FAKE_BIN:$PATH" \
|
||||||
|
MOSAIC_TMUX_SOCKET="$SOCKET3" \
|
||||||
|
MOSAIC_AGENT_WORKDIR="$WORKDIR3" \
|
||||||
|
MOSAIC_AGENT_RUNTIME="pi" \
|
||||||
|
MOSAIC_RUNTIME_BIN="$FAKE_RUNTIME_BIN" \
|
||||||
|
MOSAIC_AGENT_COMMAND="mosaic yolo pi --model openai-codex/gpt-5.5:high" \
|
||||||
|
"$START" "$AGENT3"
|
||||||
|
|
||||||
|
all_args=$(cat "$TMUX_ARGS_FILE" 2>/dev/null || true)
|
||||||
|
rm -f "$TMUX_ARGS_FILE"
|
||||||
|
|
||||||
|
echo "--- captured tmux new-session args ---"
|
||||||
|
echo "$all_args"
|
||||||
|
echo "--- end args ---"
|
||||||
|
|
||||||
|
# a) PATH prefix containing FAKE_RUNTIME_BIN must appear.
|
||||||
|
echo "$all_args" | grep -qF "export PATH=" || fail "pane command does not export PATH"
|
||||||
|
echo "$all_args" | grep -qF "$FAKE_RUNTIME_BIN" || fail "pane command does not include MOSAIC_RUNTIME_BIN in PATH prefix"
|
||||||
|
|
||||||
|
# b) exec must appear so the runtime replaces the wrapper shell.
|
||||||
|
echo "$all_args" | grep -qF "exec " || fail "pane command does not use exec"
|
||||||
|
|
||||||
|
# c) Full MOSAIC_AGENT_COMMAND (with flags) must be forwarded.
|
||||||
|
echo "$all_args" | grep -qF "mosaic yolo pi --model openai-codex/gpt-5.5:high" || \
|
||||||
|
fail "pane command does not forward MOSAIC_AGENT_COMMAND with flags intact"
|
||||||
|
|
||||||
|
# ── Test 4: when no extra runtime-bin dirs exist, exec still appears ───────────
|
||||||
|
TMUX_ARGS_FILE2=$(mktemp)
|
||||||
|
FAKE_BIN2=$(mktemp -d)
|
||||||
|
CLEANUP_DIRS+=("$FAKE_BIN2")
|
||||||
|
|
||||||
|
cat > "$FAKE_BIN2/tmux" <<SHIM2
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
subcmd="\$3"
|
||||||
|
if [ "\$subcmd" = "has-session" ]; then exit 1; fi
|
||||||
|
if [ "\$subcmd" = "new-session" ]; then
|
||||||
|
printf '%s\n' "\$@" > "$TMUX_ARGS_FILE2"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
SHIM2
|
||||||
|
chmod +x "$FAKE_BIN2/tmux"
|
||||||
|
|
||||||
|
SOCKET4="mosaic-agent-test4-$RANDOM-$$"
|
||||||
|
AGENT4="agent4-$RANDOM"
|
||||||
|
WORKDIR4=$(mktemp -d)
|
||||||
|
CLEANUP_DIRS+=("$WORKDIR4")
|
||||||
|
|
||||||
|
# MOSAIC_RUNTIME_BIN points to a non-existent dir so prefix will be empty;
|
||||||
|
# .npm-global/bin and .local/bin may or may not exist but we just want exec.
|
||||||
|
PATH="$FAKE_BIN2:$PATH" \
|
||||||
|
MOSAIC_TMUX_SOCKET="$SOCKET4" \
|
||||||
|
MOSAIC_AGENT_WORKDIR="$WORKDIR4" \
|
||||||
|
MOSAIC_AGENT_RUNTIME="pi" \
|
||||||
|
MOSAIC_RUNTIME_BIN="/nonexistent-dir-$$" \
|
||||||
|
MOSAIC_AGENT_COMMAND="mosaic yolo pi" \
|
||||||
|
"$START" "$AGENT4"
|
||||||
|
|
||||||
|
all_args4=$(cat "$TMUX_ARGS_FILE2" 2>/dev/null || true)
|
||||||
|
rm -f "$TMUX_ARGS_FILE2"
|
||||||
|
rm -rf "$WORKDIR4"
|
||||||
|
|
||||||
|
echo "$all_args4" | grep -qF "exec " || fail "pane command (no prefix dirs) does not use exec"
|
||||||
|
echo "$all_args4" | grep -qF "mosaic yolo pi" || fail "pane command does not include agent command when no prefix"
|
||||||
|
|
||||||
|
# ── Test 5: candidate dir already in LAUNCHER $PATH is still baked into pane ──
|
||||||
|
#
|
||||||
|
# Regression guard for the bug where _build_runtime_bin_prefix() used to skip
|
||||||
|
# a candidate because it was already present in the launcher process's $PATH.
|
||||||
|
# That check was wrong: the pane inherits the tmux SERVER environment, not the
|
||||||
|
# launcher's env. Even if a dir is on the launcher's PATH it must always be
|
||||||
|
# baked into the pane's PATH export.
|
||||||
|
#
|
||||||
|
# We prove this by setting PATH to include FAKE_RUNTIME_BIN5 (the candidate),
|
||||||
|
# then asserting the generated new-session command still exports it.
|
||||||
|
TMUX_ARGS_FILE5=$(mktemp)
|
||||||
|
FAKE_BIN5=$(mktemp -d)
|
||||||
|
FAKE_RUNTIME_BIN5=$(mktemp -d) # this dir IS on the launcher's PATH below
|
||||||
|
CLEANUP_DIRS+=("$FAKE_BIN5" "$FAKE_RUNTIME_BIN5")
|
||||||
|
|
||||||
|
cat > "$FAKE_BIN5/tmux" <<SHIM5
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
subcmd="\$3"
|
||||||
|
if [ "\$subcmd" = "has-session" ]; then exit 1; fi
|
||||||
|
if [ "\$subcmd" = "new-session" ]; then
|
||||||
|
printf '%s\n' "\$@" > "$TMUX_ARGS_FILE5"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit 0
|
||||||
|
SHIM5
|
||||||
|
chmod +x "$FAKE_BIN5/tmux"
|
||||||
|
|
||||||
|
SOCKET5="mosaic-agent-test5-$RANDOM-$$"
|
||||||
|
AGENT5="agent5-$RANDOM"
|
||||||
|
WORKDIR5=$(mktemp -d)
|
||||||
|
CLEANUP_DIRS+=("$WORKDIR5")
|
||||||
|
CLEANUP_SOCKETS+=("$SOCKET5")
|
||||||
|
|
||||||
|
# FAKE_RUNTIME_BIN5 is deliberately placed on the LAUNCHER PATH so that the
|
||||||
|
# old (buggy) code would have skipped it. The correct code must still include
|
||||||
|
# it in the pane PATH export.
|
||||||
|
PATH="$FAKE_BIN5:$FAKE_RUNTIME_BIN5:$PATH" \
|
||||||
|
MOSAIC_TMUX_SOCKET="$SOCKET5" \
|
||||||
|
MOSAIC_AGENT_WORKDIR="$WORKDIR5" \
|
||||||
|
MOSAIC_AGENT_RUNTIME="pi" \
|
||||||
|
MOSAIC_RUNTIME_BIN="$FAKE_RUNTIME_BIN5" \
|
||||||
|
MOSAIC_AGENT_COMMAND="mosaic yolo pi" \
|
||||||
|
"$START" "$AGENT5"
|
||||||
|
|
||||||
|
all_args5=$(cat "$TMUX_ARGS_FILE5" 2>/dev/null || true)
|
||||||
|
rm -f "$TMUX_ARGS_FILE5"
|
||||||
|
rm -rf "$WORKDIR5"
|
||||||
|
|
||||||
|
echo "--- test 5: launcher-PATH candidate must still appear in pane export ---"
|
||||||
|
echo "$all_args5"
|
||||||
|
echo "--- end test 5 args ---"
|
||||||
|
|
||||||
|
echo "$all_args5" | grep -qF "export PATH=" || \
|
||||||
|
fail "test5: pane command does not export PATH when candidate is on launcher PATH"
|
||||||
|
echo "$all_args5" | grep -qF "$FAKE_RUNTIME_BIN5" || \
|
||||||
|
fail "test5: candidate dir (already on launcher PATH) was NOT baked into pane PATH — regression"
|
||||||
|
|
||||||
echo "ok - start-agent-session"
|
echo "ok - start-agent-session"
|
||||||
|
|||||||
67
packages/mosaic/framework/tools/quality/scripts/test-install-migration.sh
Executable file
67
packages/mosaic/framework/tools/quality/scripts/test-install-migration.sh
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# test-install-migration.sh — fixture matrix for the v2→v3 (Constitution) upgrade
|
||||||
|
# migration in install.sh. Runs the installer against throwaway MOSAIC_HOME dirs
|
||||||
|
# with MOSAIC_SYNC_ONLY=1 (file phase only — no environment-touching post-install)
|
||||||
|
# and asserts the framework-owned-overwrite + user-preserve + backup semantics.
|
||||||
|
#
|
||||||
|
# Mirrors the TS fixture suite in packages/mosaic/src/config/file-adapter.test.ts;
|
||||||
|
# both installers MUST behave identically.
|
||||||
|
#
|
||||||
|
# Usage: bash test-install-migration.sh
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
FW="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" # packages/mosaic/framework
|
||||||
|
INSTALL="$FW/install.sh"
|
||||||
|
DEFA="$FW/defaults"
|
||||||
|
|
||||||
|
pass=0; fail=0
|
||||||
|
chk() { if eval "$2"; then echo " ✓ $1"; pass=$((pass + 1)); else echo " ✗ $1"; fail=$((fail + 1)); fi; }
|
||||||
|
run() { MOSAIC_HOME="$1" MOSAIC_INSTALL_MODE="$2" MOSAIC_SYNC_ONLY=1 bash "$INSTALL" >/dev/null 2>&1; }
|
||||||
|
|
||||||
|
echo "install.sh v2→v3 migration fixture matrix:"
|
||||||
|
|
||||||
|
# F1 — fresh install
|
||||||
|
T1=$(mktemp -d); run "$T1" overwrite
|
||||||
|
chk "F1 fresh: CONSTITUTION/AGENTS/STANDARDS/TOOLS seeded" \
|
||||||
|
"[ -f '$T1/CONSTITUTION.md' ] && [ -f '$T1/AGENTS.md' ] && [ -f '$T1/STANDARDS.md' ] && [ -f '$T1/TOOLS.md' ]"
|
||||||
|
chk "F1 fresh: AGENTS == shipped default" "cmp -s '$T1/AGENTS.md' '$DEFA/AGENTS.md'"
|
||||||
|
chk "F1 fresh: framework-version stamped 3" "[ \"\$(cat '$T1/.framework-version' 2>/dev/null)\" = 3 ]"
|
||||||
|
|
||||||
|
# F2 — legacy install with a user-edited AGENTS.md (the sanctioned pre-constitution customization)
|
||||||
|
T2=$(mktemp -d); mkdir -p "$T2/credentials"
|
||||||
|
printf '# user-edited AGENTS pre-constitution\n' > "$T2/AGENTS.md"
|
||||||
|
printf '# my persona\n' > "$T2/SOUL.md"
|
||||||
|
printf 'token\n' > "$T2/credentials/c.json"
|
||||||
|
echo 2 > "$T2/.framework-version"
|
||||||
|
run "$T2" keep
|
||||||
|
chk "F2 legacy-edited: AGENTS overwritten to framework version" "cmp -s '$T2/AGENTS.md' '$DEFA/AGENTS.md'"
|
||||||
|
chk "F2 legacy-edited: prior AGENTS saved to .pre-constitution.bak" \
|
||||||
|
"grep -q 'user-edited AGENTS pre-constitution' '$T2/AGENTS.md.pre-constitution.bak'"
|
||||||
|
chk "F2 legacy-edited: SOUL.md preserved" "grep -q 'my persona' '$T2/SOUL.md'"
|
||||||
|
chk "F2 legacy-edited: credentials preserved" "grep -q token '$T2/credentials/c.json'"
|
||||||
|
chk "F2 legacy-edited: CONSTITUTION.md installed" "[ -f '$T2/CONSTITUTION.md' ]"
|
||||||
|
run "$T2" keep
|
||||||
|
chk "F2 idempotent: .pre-constitution.bak preserved across a 2nd upgrade" \
|
||||||
|
"grep -q 'user-edited AGENTS pre-constitution' '$T2/AGENTS.md.pre-constitution.bak'"
|
||||||
|
|
||||||
|
# F3 — user-tuned STANDARDS.md
|
||||||
|
T3=$(mktemp -d); printf '# tuned standards\n' > "$T3/STANDARDS.md"; printf '# persona\n' > "$T3/SOUL.md"; echo 2 > "$T3/.framework-version"
|
||||||
|
run "$T3" keep
|
||||||
|
chk "F3 tuned-standard: STANDARDS overwritten" "cmp -s '$T3/STANDARDS.md' '$DEFA/STANDARDS.md'"
|
||||||
|
chk "F3 tuned-standard: tuned copy backed up" "grep -q 'tuned standards' '$T3/STANDARDS.md.pre-constitution.bak'"
|
||||||
|
|
||||||
|
# F4 — unattended / no TTY (stdin closed): must complete without hanging, default to keep
|
||||||
|
T4=$(mktemp -d); printf '# persona\n' > "$T4/SOUL.md"; printf '# old\n' > "$T4/AGENTS.md"; echo 2 > "$T4/.framework-version"
|
||||||
|
MOSAIC_HOME="$T4" MOSAIC_SYNC_ONLY=1 bash "$INSTALL" </dev/null >/dev/null 2>&1
|
||||||
|
chk "F4 no-TTY: completed, AGENTS updated" "cmp -s '$T4/AGENTS.md' '$DEFA/AGENTS.md'"
|
||||||
|
|
||||||
|
# F5 — failure path must not corrupt existing data (invalid mode rejected before any file op)
|
||||||
|
T5=$(mktemp -d); mkdir -p "$T5/credentials"; printf '# orig\n' > "$T5/SOUL.md"; printf 'keepme\n' > "$T5/credentials/c.json"; echo 2 > "$T5/.framework-version"
|
||||||
|
MOSAIC_HOME="$T5" MOSAIC_INSTALL_MODE=bogus MOSAIC_SYNC_ONLY=1 bash "$INSTALL" >/dev/null 2>&1; rc=$?
|
||||||
|
chk "F5 failure: invalid mode rejected (nonzero exit)" "[ $rc -ne 0 ]"
|
||||||
|
chk "F5 failure: SOUL + credentials intact" "grep -q orig '$T5/SOUL.md' && grep -q keepme '$T5/credentials/c.json'"
|
||||||
|
|
||||||
|
rm -rf "$T1" "$T2" "$T3" "$T4" "$T5"
|
||||||
|
echo
|
||||||
|
echo "RESULT: $pass passed, $fail failed"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
# 2. STRUCTURAL (private $HOME default in *.sh) — scanned everywhere EXCEPT examples/,
|
# 2. STRUCTURAL (private $HOME default in *.sh) — scanned everywhere EXCEPT examples/,
|
||||||
# because worked example overlays/personas legitimately show placeholder paths.
|
# because worked example overlays/personas legitimately show placeholder paths.
|
||||||
#
|
#
|
||||||
# File types: *.md, *.sh, *.ps1, *.json, and the extensionless CLI scripts under
|
# File types: *.md, *.sh, *.ps1, *.json, *.yml/*.yaml, *.toml, *.env, *.service, and the CLI scripts under
|
||||||
# tools/_scripts/. Excludes node_modules/ and this gate file.
|
# tools/_scripts/. Excludes node_modules/ and this gate file.
|
||||||
#
|
#
|
||||||
# NOTE: '\bPDA\b' intentionally matches "PDA-friendly" (the contamination removed in P2);
|
# NOTE: '\bPDA\b' intentionally matches "PDA-friendly" (the contamination removed in P2);
|
||||||
@@ -39,7 +39,7 @@ cd "$FRAMEWORK_ROOT" || { echo "FRAMEWORK_ROOT not found: $FRAMEWORK_ROOT" >&2;
|
|||||||
# Identity scope = ALL shipped text files (examples/ INCLUDED).
|
# Identity scope = ALL shipped text files (examples/ INCLUDED).
|
||||||
_files_identity() {
|
_files_identity() {
|
||||||
find . -type f \
|
find . -type f \
|
||||||
\( -name '*.md' -o -name '*.sh' -o -name '*.ps1' -o -name '*.json' -o -path '*/tools/_scripts/*' \) \
|
\( -name '*.md' -o -name '*.sh' -o -name '*.ps1' -o -name '*.json' -o -name '*.yml' -o -name '*.yaml' -o -name '*.toml' -o -name '*.env' -o -name '*.service' -o -path '*/tools/_scripts/*' \) \
|
||||||
-not -path '*/node_modules/*' -not -path "./$SELF_REL" -print0
|
-not -path '*/node_modules/*' -not -path "./$SELF_REL" -print0
|
||||||
}
|
}
|
||||||
# Structural scope = shipped scripts, examples/ EXCLUDED.
|
# Structural scope = shipped scripts, examples/ EXCLUDED.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@mosaicstack/mosaic",
|
"name": "@mosaicstack/mosaic",
|
||||||
"version": "0.0.34",
|
"version": "0.0.35",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
"url": "https://git.mosaicstack.dev/mosaicstack/stack.git",
|
||||||
|
|||||||
Reference in New Issue
Block a user