feat(skills): skill lifecycle + ms-* skill set completion (#40, #41, #42)

- scripts/skill.sh: install (bundled or path) / activate / deactivate /
  uninstall (refuses while enabled) / list
- skills-enabled + skills-available dirs under the data root; a skill not
  in skills-enabled is not enabled or available for use
- pi adapter: MOSAIC_SKILLS -> --skill per dir; --no-skills when none
- agent.sh: seat definitions declare skills[]; resolution against
  skills-enabled refuses the launch loudly when missing
- ms-* skills completed (owner-authored canon, hands-off): ms-tools
  adapted to the runtime, ms-file-read/write/agent/conductor bodies
  written in the owner's style; ms-agent-watch + ms-unslop untouched
- tasks/USER.md onboarding fixtures; suite hardening (nested def path,
  user seed, mock-adapter dispatch evidence)

Suites: config 24, task 74, release 14, conductor 17, verify PASS.
RELEASE 0.0.12 packaged; health-gated activation on merge.

Closes #40, closes #41, closes #42
This commit is contained in:
2026-09-03 16:23:57 -05:00
parent e14ad9ab52
commit 2f5a8d2cec
10 changed files with 1347 additions and 5 deletions
+200
View File
@@ -0,0 +1,200 @@
#!/usr/bin/env bash
# Live tmux semantics on private sockets only. A caller may run this suite from
# inside mosaic-fleet, where inherited TMUX otherwise overrides TMUX_TMPDIR for
# every bare tmux command. Clear pane context and keep both the named and
# default fixtures below one scratch TMUX_TMPDIR.
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "$0")" && pwd)
SEND_MESSAGE="$SCRIPT_DIR/send-message.sh"
AGENT_SEND="$SCRIPT_DIR/agent-send.sh"
SOCKET="mosaic-test-$RANDOM-$$"
TARGET="target-$RANDOM"
DEFAULT_TARGET="default-target-$RANDOM"
TMPDIR=$(mktemp -d)
TEST_TMUX_TMPDIR="$TMPDIR/tmux"
mkdir -p "$TEST_TMUX_TMPDIR"
chmod 700 "$TEST_TMUX_TMPDIR"
unset TMUX TMUX_PANE
export TMUX_TMPDIR="$TEST_TMUX_TMPDIR"
ART_OUT=$(mktemp)
AMB_OUT=$(mktemp)
AMB_ERR=$(mktemp)
A2_OUT=$(mktemp)
A2_ERR=$(mktemp)
UNIQ_OUT=$(mktemp)
UNIQ_ERR=$(mktemp)
TWIN="twin-$RANDOM-$$"
cleanup() {
local test_rc=$? residue=0
trap - EXIT
env -u TMUX -u TMUX_PANE TMUX_TMPDIR="$TEST_TMUX_TMPDIR" \
tmux -L "$SOCKET" kill-server >/dev/null 2>&1 || true
env -u TMUX -u TMUX_PANE TMUX_TMPDIR="$TEST_TMUX_TMPDIR" \
tmux -L default kill-server >/dev/null 2>&1 || true
sleep 0.2
if env -u TMUX -u TMUX_PANE TMUX_TMPDIR="$TEST_TMUX_TMPDIR" \
tmux -L "$SOCKET" list-sessions >/dev/null 2>&1; then
echo "FAIL: named scratch server still answering during cleanup" >&2
residue=1
fi
if env -u TMUX -u TMUX_PANE TMUX_TMPDIR="$TEST_TMUX_TMPDIR" \
tmux -L default list-sessions >/dev/null 2>&1; then
echo "FAIL: default scratch server still answering during cleanup" >&2
residue=1
fi
rm -rf "$TMPDIR" "$ART_OUT" "$AMB_OUT" "$AMB_ERR" "$A2_OUT" "$A2_ERR" "$UNIQ_OUT" "$UNIQ_ERR"
if [ "$test_rc" -ne 0 ]; then
exit "$test_rc"
fi
exit "$residue"
}
trap cleanup EXIT
fail() {
echo "FAIL: $*" >&2
exit 1
}
require_tmux() {
command -v tmux >/dev/null 2>&1 || fail "tmux is required"
}
capture_named() {
tmux -L "$SOCKET" capture-pane -t "=$TARGET:0.0" -p
}
capture_default() {
tmux capture-pane -t "=$DEFAULT_TARGET:0.0" -p
}
require_tmux
tmux -L "$SOCKET" new-session -d -s "$TARGET" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
tmux new-session -d -s "$DEFAULT_TARGET" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
"$SEND_MESSAGE" -L "$SOCKET" -t "=$TARGET" -m "named socket hello" >/tmp/send-message-named.out
sleep 0.2
named_pane="$(capture_named)" || fail "could not capture named socket pane"
grep -qF "named socket hello" <<<"$named_pane" || fail "send-message.sh did not deliver to named socket"
default_pane="$(capture_default)" || fail "could not capture default socket pane"
if grep -qF "named socket hello" <<<"$default_pane"; then
fail "send-message.sh leaked named-socket message to default tmux server"
fi
"$AGENT_SEND" -L "$SOCKET" -S "tester:source" -s "=$TARGET" -m "agent socket hello" >/tmp/agent-send-named.out
sleep 0.2
named_pane="$(capture_named)" || fail "could not capture named socket pane"
grep -qF "[tester:source ->" <<<"$named_pane" || fail "agent-send.sh did not include preamble"
grep -qF "agent socket hello" <<<"$named_pane" || fail "agent-send.sh did not deliver to named socket"
default_pane="$(capture_default)" || fail "could not capture default socket pane"
if grep -qF "agent socket hello" <<<"$default_pane"; then
fail "agent-send.sh leaked named-socket message to default tmux server"
fi
# Concurrency: parallel senders on one server must not cross-deliver or drop.
# Locks the unique-per-invocation paste buffer (a fixed buffer name raced:
# load overwrote load, -d deleted underneath — messages swapped between panes).
CONC_N=5
for i in $(seq 1 "$CONC_N"); do
tmux -L "$SOCKET" new-session -d -s "conc-$i" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
done
pids=()
for i in $(seq 1 "$CONC_N"); do
"$SEND_MESSAGE" -L "$SOCKET" -t "=conc-$i" -m "CONCPAYLOAD-${i}-END" >/dev/null &
pids+=($!)
done
for pid in "${pids[@]}"; do
wait "$pid" || fail "concurrent send-message.sh invocation exited non-zero"
done
sleep 0.2
for i in $(seq 1 "$CONC_N"); do
pane=$(tmux -L "$SOCKET" capture-pane -t "=conc-$i:0.0" -p)
grep -qF "CONCPAYLOAD-${i}-END" <<<"$pane" \
|| fail "concurrent send dropped payload for pane conc-$i"
for j in $(seq 1 "$CONC_N"); do
[ "$j" = "$i" ] && continue
if grep -qF "CONCPAYLOAD-${j}-END" <<<"$pane"; then
fail "concurrent send cross-delivered payload $j to pane conc-$i"
fi
done
done
# B1 (2026-08-29): socket default resolution in agent-send.sh. Measured
# defect: tasking sends without -L landed in a stale default-socket twin of
# the target seat; rc 0 reported honest delivery to the wrong pane.
# Arm A: session on MULTIPLE sockets, no -L -> refuse with rc 4 naming both.
tmux -L "$SOCKET" new-session -d -s "$TWIN" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
tmux new-session -d -s "$TWIN" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
amb_rc=0
env -u MOSAIC_TMUX_SOCKET "$AGENT_SEND" -s "$TWIN" -m "must refuse" >$AMB_OUT 2>$AMB_ERR || amb_rc=$?
[ "$amb_rc" -eq 4 ] || fail "ambiguity refusal: rc=$amb_rc want 4 (stderr: $(cat $AMB_ERR))"
grep -q "multiple sockets" $AMB_ERR || fail "ambiguity refusal message missing socket list"
grep -qF "$SOCKET" $AMB_ERR || fail "ambiguity refusal message does not name the test socket"
tmux kill-session -t "$TWIN" >/dev/null 2>&1 || true
tmux -L "$SOCKET" kill-session -t "$TWIN" >/dev/null 2>&1 || true
# Arm A2: with MOSAIC_TMUX_SOCKET exported, a twin session is NOT ambiguous:
# the env var disambiguates by precedence (codex PR #1466 blocker).
tmux -L "$SOCKET" new-session -d -s "$TWIN" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
tmux new-session -d -s "$TWIN" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
a2_rc=0
MOSAIC_TMUX_SOCKET="$SOCKET" "$AGENT_SEND" -s "$TWIN" -m "env disambiguated" >$A2_OUT 2>$A2_ERR || a2_rc=$?
[ "$a2_rc" -eq 0 ] || fail "env disambiguation: rc=$a2_rc (stderr: $(cat $A2_ERR))"
sleep 0.2
a2_pane="$(tmux -L "$SOCKET" capture-pane -t "=$TWIN:0.0" -p)" || fail "cannot capture twin (arm A2)"
grep -qF "env disambiguated" <<<"$a2_pane" || fail "env disambiguation did not deliver on the named socket"
a2_default="$(tmux capture-pane -t "=$TWIN:0.0" -p)" || true
if grep -qF "env disambiguated" <<<"$a2_default"; then
fail "env disambiguation cross-delivered to the default-socket twin"
fi
tmux kill-session -t "$TWIN" >/dev/null 2>&1 || true
tmux -L "$SOCKET" kill-session -t "$TWIN" >/dev/null 2>&1 || true
# Arm B: session unique to ONE socket, no -L -> auto-resolve to that socket
# and deliver there.
# Arm A3: prefix matching must not produce false socket hits (codex PR
# #1466): a session named TWIN-old must not count as a hit for target
# TWIN (tmux target syntax prefix-matches without '=').
PSEUDO="${TWIN}-old"
tmux new-session -d -s "$PSEUDO" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
A3_ERR=$(mktemp)
a3_rc=0
env -u MOSAIC_TMUX_SOCKET "$AGENT_SEND" -s "$TWIN" -m "prefix trap" >/dev/null 2>"$A3_ERR" || a3_rc=$?
# TWIN exists nowhere (both twins killed after arm A2); with '=' the
# PSEUDO session is not a hit, so the sender must fail target-not-found
# (rc 1) instead of delivering into the prefix-named session.
[ "$a3_rc" -eq 1 ] || fail "prefix false-hit: rc=$a3_rc want 1 (stderr: $(cat "$A3_ERR"))"
if tmux capture-pane -t "=$PSEUDO:0.0" -p 2>/dev/null | grep -qF "prefix trap"; then
fail "delivery landed in the prefix-named session (false socket hit)"
fi
tmux kill-session -t "$PSEUDO" >/dev/null 2>&1 || true
rm -f "$A3_ERR"
# Arm A4: compound targets pin the SESSION component exact (codex PR
# #1466): 'TWIN:0.0' must not resolve into the prefix-named session.
PSEUDO2="${TWIN}-old"
tmux new-session -d -s "$PSEUDO2" -c "$TMPDIR" 'PS1=" " exec bash --noprofile --norc -i'
A4_ERR=$(mktemp)
a4_rc=0
env -u MOSAIC_TMUX_SOCKET "$AGENT_SEND" -s "$TWIN:0.0" -m "compound trap" >/dev/null 2>"$A4_ERR" || a4_rc=$?
[ "$a4_rc" -eq 1 ] || fail "compound prefix false-hit: rc=$a4_rc want 1 (stderr: $(cat "$A4_ERR"))"
if tmux capture-pane -t "=$PSEUDO2:0.0" -p 2>/dev/null | grep -qF "compound trap"; then
fail "compound delivery landed in the prefix-named session"
fi
tmux kill-session -t "$PSEUDO2" >/dev/null 2>&1 || true
rm -f "$A4_ERR"
uniq_rc=0
env -u MOSAIC_TMUX_SOCKET "$AGENT_SEND" -s "$TARGET" -m "autoresolved hello" >$UNIQ_OUT 2>$UNIQ_ERR || uniq_rc=$?
[ "$uniq_rc" -eq 0 ] || fail "unique auto-resolution: rc=$uniq_rc (stderr: $(cat $UNIQ_ERR))"
sleep 0.2
auto_pane="$(capture_named)" || fail "could not capture named socket pane (arm B)"
grep -qF "autoresolved hello" <<<"$auto_pane" || fail "auto-resolution did not deliver to the named-socket pane"
default_pane2="$(capture_default)" || fail "could not capture default socket pane (arm B)"
if grep -qF "autoresolved hello" <<<"$default_pane2"; then
fail "auto-resolution cross-delivered to the default socket pane"
fi
echo "ok - named tmux socket send tools"