#!/usr/bin/env bash # ============================================================================== # Multi-Agent Mux (MAM) Skill Installer # Efficiently installs MAM orchestration rules & skills to another project. # ============================================================================== set -euo pipefail # ANSI color codes RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Helper functions for logs log_info() { echo -e "${BLUE}[INFO]${NC} $*"; } log_ok() { echo -e "${GREEN}[OK]${NC} $*"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } show_help() { cat < Target directory/project where MAM should be installed (defaults to current directory) -f, --force Force copy AGENTS.md even if it already exists (backups are still made) -h, --help Show this help message EOF } TARGET_DIR="." FORCE=0 # Parse CLI arguments while [[ $# -gt 0 ]]; do case "$1" in -t|--target) TARGET_DIR="$2" shift 2 ;; -f|--force) FORCE=1 shift ;; -h|--help) show_help exit 0 ;; *) log_error "Unknown option: $1" show_help exit 1 ;; esac done # Resolve absolute path for source and target, tracking symlinks gracefully SOURCE="${BASH_SOURCE[0]}" while [ -h "$SOURCE" ]; do DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" SOURCE="$(readlink "$SOURCE")" [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" done DIR="$( cd -P "$( dirname "$SOURCE" )" >/dev/null 2>&1 && pwd )" SRC_DIR="$(cd "$DIR/.." && pwd)" # Ensure target directory exists if [ ! -d "$TARGET_DIR" ]; then log_info "Creating target directory: $TARGET_DIR" mkdir -p "$TARGET_DIR" fi TARGET_DIR="$(cd "$TARGET_DIR" && pwd)" log_info "Installing MAM skills to target project: $TARGET_DIR" log_info "Source directory resolved: $SRC_DIR" if [ "$SRC_DIR" = "$TARGET_DIR" ]; then log_error "Source and Target directories are the same! Cannot install to oneself." exit 1 fi OWNERSHIP_LIB="$SRC_DIR/deploy/lib_ownership.sh" if [ ! -f "$OWNERSHIP_LIB" ]; then log_error "Missing $OWNERSHIP_LIB; refusing to install with unknown ownership rules." exit 1 fi # shellcheck source=deploy/lib_ownership.sh . "$OWNERSHIP_LIB" # 1. Dependency Checks log_info "Verifying host dependencies..." DEPS=(herdr python3 rsync uuidgen) MISSING_DEPS=() for dep in "${DEPS[@]}"; do if ! command -v "$dep" &>/dev/null; then MISSING_DEPS+=("$dep") fi done if [ ${#MISSING_DEPS[@]} -ne 0 ]; then log_error "Missing required dependencies: ${MISSING_DEPS[*]}" if [[ " ${MISSING_DEPS[*]} " == *" herdr "* ]]; then log_error "To install herdr, please refer to the official installation guide:" log_error " curl -fsSL https://herdr.dev/install.sh | sh" fi log_error "Please install them before using MAM." exit 1 fi # Check Python PyYAML and sqlite3 library (hard dependencies for registry parsing) if ! python3 -c "import yaml, sqlite3" &>/dev/null; then log_error "Python 'pyyaml' or built-in 'sqlite3' modules are missing (hard dependencies for session registry)." log_error "Please run: pip install pyyaml" exit 1 fi log_ok "Dependency checks completed." # 2. Copy .agents/ folder and config tools log_info "Deploying orchestration rules & skills (.agents/)..." mkdir -p "$TARGET_DIR/.agents" # Sync rules and skills, avoiding copying temporary or system files # Exclude git histories, reports, references, logs or internal runtime cache rsync -a --exclude='.git/' --exclude='/reports/' --exclude='/references/' --exclude='*.log' --exclude='*.tmp' --exclude='__pycache__/' --exclude='*.pyc' "$SRC_DIR/.agents/" "$TARGET_DIR/.agents/" log_ok "Deployed Rules and Skills under target's .agents/" # Copy config templates and generate scripts (M-2) if [ -f "$SRC_DIR/.mam.env.example" ]; then cp "$SRC_DIR/.mam.env.example" "$TARGET_DIR/.mam.env.example" log_ok "Copied .mam.env.example configuration template" fi # Deploy remove.sh and update.sh into .mam_deploy/ (R-3) mkdir -p "$TARGET_DIR/.mam_deploy" if [ -f "$SRC_DIR/deploy/remove.sh" ]; then cp "$SRC_DIR/deploy/remove.sh" "$TARGET_DIR/.mam_deploy/remove.sh" chmod 0755 "$TARGET_DIR/.mam_deploy/remove.sh" fi if [ -f "$SRC_DIR/deploy/update.sh" ]; then cp "$SRC_DIR/deploy/update.sh" "$TARGET_DIR/.mam_deploy/update.sh" chmod 0755 "$TARGET_DIR/.mam_deploy/update.sh" fi if [ -f "$SRC_DIR/deploy/generate-env.sh" ]; then mkdir -p "$TARGET_DIR/scripts" cp "$SRC_DIR/deploy/generate-env.sh" "$TARGET_DIR/scripts/generate-env.sh" chmod +x "$TARGET_DIR/scripts/generate-env.sh" log_ok "Copied deploy/generate-env.sh helper tool" fi # Copy the user manual into the target's .agents/ (previously came via rsync of .agents/) if [ -f "$SRC_DIR/deploy/INSTALL.md" ]; then cp "$SRC_DIR/deploy/INSTALL.md" "$TARGET_DIR/.agents/INSTALL.md" log_ok "Copied INSTALL.md user manual into target .agents/" fi # Record an install manifest (.mam/install_manifest.txt) and state hashes (R-5, F14) log_info "Recording install manifest (.mam/install_manifest.txt)..." mkdir -p "$TARGET_DIR/.mam" MANIFEST_FILE="$TARGET_DIR/.mam/install_manifest.txt" : > "$MANIFEST_FILE" ( cd "$TARGET_DIR" && find .agents -type f -print ) >> "$MANIFEST_FILE" for extra in ".mam.env.example" ".mam_deploy/remove.sh" ".mam_deploy/update.sh" \ "scripts/generate-env.sh"; do if [ -e "$TARGET_DIR/$extra" ]; then echo "$extra" >> "$MANIFEST_FILE" fi done log_ok "Recorded $(wc -l < "$MANIFEST_FILE" | tr -d ' ') manifest entries." OWNED_LIST="$TARGET_DIR/.mam/.owned_paths.tmp" : > "$OWNED_LIST" while IFS= read -r rel; do rel="${rel#./}" if is_framework_owned "$rel"; then echo "$rel" >> "$OWNED_LIST" if is_registry_file "$rel"; then mkdir -p "$TARGET_DIR/.mam/base/$(dirname "$rel")" cp -f "$TARGET_DIR/$rel" "$TARGET_DIR/.mam/base/$rel" fi fi done < <( cd "$TARGET_DIR" && find .agents -type f -print ) python3 - "$TARGET_DIR" "$OWNED_LIST" <<'MAMSTATE' import hashlib, os, sys target, listing = sys.argv[1], sys.argv[2] def sha(path): h = hashlib.sha256() with open(path, "rb") as f: while chunk := f.read(65536): h.update(chunk) return h.hexdigest() rows = [] with open(listing) as f: for rel in f: rel = rel.strip() full = os.path.join(target, rel) if rel and os.path.isfile(full): rows.append("%s %s\n" % (sha(full), rel)) with open(os.path.join(target, ".mam", "asset_hashes.txt"), "w") as f: f.writelines(sorted(rows)) MAMSTATE rm -f "$OWNED_LIST" log_ok "Recorded asset hashes and registry base under .mam/." # 3. Copy AGENTS.md to root or inject guidelines pointer log_info "Configuring developer guidelines (AGENTS.md)..." AGENTS_FILE="$TARGET_DIR/AGENTS.md" MARKER_START="" MARKER_END="" if [ -f "$AGENTS_FILE" ]; then if [ "$FORCE" -eq 1 ]; then log_warn "AGENTS.md already exists in target project. Backing up and overwriting (--force)..." cp "$AGENTS_FILE" "$AGENTS_FILE.bak.$(date +%s)" cp "$SRC_DIR/AGENTS.md" "$AGENTS_FILE" log_ok "Guidelines overwritten successfully." else if grep -Fq "$MARKER_START" "$AGENTS_FILE"; then log_ok "MAM orchestration guidelines pointer already exists in AGENTS.md." else echo -e "\n$MARKER_START\n# 🤖 Multi-Agent Orchestration Guidelines\nPlease refer to [.agents/MULTI_AGENT_RULES.md](file://./.agents/MULTI_AGENT_RULES.md) for detailed collaborative rules and state flow constraints.\n$MARKER_END" >> "$AGENTS_FILE" log_ok "Injected MAM guidelines pointer to existing AGENTS.md." fi fi else cp "$SRC_DIR/AGENTS.md" "$AGENTS_FILE" log_ok "Guidelines AGENTS.md copied to project root." fi # 4. Gitignore adjustments (R-4: managed block) log_info "Registering runtime isolation blocks in .gitignore..." GITIGNORE="$TARGET_DIR/.gitignore" MAM_GI_START="# >>> MAM managed block (managed by install.sh — do not edit) >>>" MAM_GI_END="# <<< MAM managed block <<<" python3 - "$GITIGNORE" "$MAM_GI_START" "$MAM_GI_END" <<'PY' import sys, os gi_path, start_marker, end_marker = sys.argv[1], sys.argv[2], sys.argv[3] block_lines = [ start_marker + "\n", "/.venv/\n", "/.mam/\n", "/.mam_deploy/\n", "/.mam.env\n", "/.mam.env.*\n", "!/.mam.env.example\n", "/.cache/multi-agent-mux-monitor/\n", "/.mam-skill-backup.*/\n", "CURRENT_JOB.md\n", end_marker + "\n" ] lines = [] if os.path.exists(gi_path): with open(gi_path, "r") as f: lines = f.readlines() new_lines = [] in_block = False block_inserted = False for line in lines: if line.strip() == start_marker: in_block = True if not block_inserted: new_lines.extend(block_lines) block_inserted = True continue if line.strip() == end_marker: in_block = False continue if not in_block: new_lines.append(line) if not block_inserted: if new_lines and not new_lines[-1].endswith("\n"): new_lines[-1] += "\n" new_lines.extend(block_lines) with open(gi_path, "w") as f: f.writelines(new_lines) PY log_ok "Registered MAM managed block in .gitignore." # 5. Python Virtual Environment Setup (F-1) log_info "Bootstrapping Python virtual environment (.venv) in target..." VENV_NAME="$TARGET_DIR/.venv" if [ ! -d "$VENV_NAME" ]; then python3 -m venv "$VENV_NAME" log_ok "Virtual environment created." else log_info "Virtual environment (.venv) already exists. Skipping creation." fi # Upgrade pip and install dependencies inside target venv # shellcheck disable=SC1091 source "$VENV_NAME"/bin/activate pip install --upgrade pip REQ_FILE="$TARGET_DIR/.agents/skills/multi-agent-mux-delegate-job/requirements.txt" if [ -f "$REQ_FILE" ]; then log_info "Installing dependencies from $REQ_FILE..." pip install -r "$REQ_FILE" log_ok "Dependencies installed successfully." else log_warn "Could not find requirements file: $REQ_FILE. Installing defaults." pip install "paho-mqtt>=2.0.0" pyyaml fi deactivate # Done log_ok "MAM Installation completed successfully!" cat < 3. Gracefully stop the session: $ bash .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh \\ --session --agent claude -------------------------------------------------------------------------------- EOF