Files
jason.woltje 9fd16b9739 feat(release): recursion guard for the health gate; run-task drift warning; M20 packages/* decision recorded (#39)
- release.sh health gate runs with MOSAIC_ENSURE_SKIP=1: the gated task run
  cannot re-enter release self-determination
- run-task.sh warns on release drift instead of silently using a stale image
- ROADMAP: M20 decision recorded (packages/* monorepo at usurpation,
  continuity-first); restructure sequenced as M20 phase 1

Closes #39
2026-09-03 15:58:46 -05:00

103 lines
3.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Skill lifecycle: install / activate / deactivate / uninstall / list.
#
# Usage:
# scripts/skill.sh install <source-dir | bundled-name>
# scripts/skill.sh activate <name>
# scripts/skill.sh deactivate <name>
# scripts/skill.sh uninstall <name>
# scripts/skill.sh list
#
# Layout (machine-scoped, under the data root):
# <dataRoot>/skills-available/<name> installed, not loadable
# <dataRoot>/skills-enabled/<name> loadable by agent launches
#
# A skill not in skills-enabled is not enabled or available for use.
# Lifecycle: install -> available; activate -> enabled; deactivate ->
# available; uninstall -> removed (only from available).
set -euo pipefail
cd "$(dirname "$0")/.."
# shellcheck source=common.sh
source scripts/common.sh
load_config
load_release
bootstrap_runtime_dir
ENABLED="$MOSAIC_DEV_DIR/skills-enabled"
AVAILABLE="$MOSAIC_DEV_DIR/skills-available"
mkdir -p "$ENABLED" "$AVAILABLE"
die() { local code="$1"; shift; echo "skill: $*" >&2; exit "$code"; }
is_skill_dir() { [ -f "$1/SKILL.md" ]; }
resolve_source() { # bundled name (skills/<name> in repo) or explicit path
if [ -d "skills/$1" ]; then printf 'skills/%s' "$1"; return; fi
if [ -d "$1" ]; then printf '%s' "$1"; return; fi
die 4 "source skill dir not found: $1"
}
frontmatter_name() { # extract name: from SKILL.md frontmatter
node -e '
const fs = require("fs");
const t = fs.readFileSync(process.argv[1], "utf8");
const m = t.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (!m) process.exit(1);
const name = (m[1].match(/^name:\s*(.+)$/m) || [])[1];
if (!name) process.exit(1);
process.stdout.write(name.trim());
' "$1/SKILL.md"
}
case "${1:-list}" in
install)
src="${2:?usage: skill.sh install <source-dir|bundled-name>}"
resolve_source "$src" >/dev/null || die 4 "source skill dir not found: $src"
SRC_DIR="$(resolve_source "$src")"
is_skill_dir "$SRC_DIR" || die 4 "not a skill (missing SKILL.md): $SRC_DIR"
NAME="$(basename "$SRC_DIR")"
[ -d "$AVAILABLE/$NAME" ] && die 1 "already installed: $NAME"
mkdir -p "$AVAILABLE"
cp -r "$SRC_DIR" "$AVAILABLE/$NAME"
echo "skill: installed $NAME -> skills-available"
echo "skill: activate with scripts/skill.sh activate $NAME"
;;
activate)
name="${2:?usage: skill.sh activate <name>}"
[ -d "$AVAILABLE/$name" ] || die 4 "not installed: $name"
[ -d "$ENABLED/$name" ] && die 1 "already enabled: $name"
mv "$AVAILABLE/$name" "$ENABLED/$name"
echo "skill: enabled $name"
;;
deactivate)
name="${2:?usage: skill.sh deactivate <name>}"
[ -d "$ENABLED/$name" ] || die 4 "not enabled: $name"
mv "$ENABLED/$name" "$AVAILABLE/$name"
echo "skill: deactivated $name -> skills-available"
;;
uninstall)
name="${2:?usage: skill.sh uninstall <name>}"
if [ -d "$ENABLED/$name" ]; then
die 1 "refusing: $name is enabled - deactivate first"
fi
[ -d "$AVAILABLE/$name" ] || die 4 "not installed: $name"
rm -rf "$AVAILABLE/$name"
echo "skill: uninstalled $name"
;;
list)
echo "enabled:"
for d in "$ENABLED"/*/; do
[ -d "$d" ] && echo " $(basename "$d")"
done
echo "available:"
for d in "$AVAILABLE"/*/; do
[ -d "$d" ] && echo " $(basename "$d")"
done
;;
*)
echo "usage: scripts/skill.sh install <src> | activate <name> | deactivate <name> | uninstall <name> | list" >&2
exit 4
;;
esac