#!/usr/bin/env bash # ============================================================================== # install.sh — Multi-Agent Mux (MAM) Orchestration Installer (Rev.2) # ============================================================================== set -euo pipefail # --- Configuration & Defaults --- TARGET_DIR="" REFRESH="${MAM_REFRESH:-1}" OVERWRITE_CUSTOM="${MAM_OVERWRITE_CUSTOM:-0}" VENV_NAME=".venv" MIN_PYTHON_VERSION="3.9" while [[ $# -gt 0 ]]; do case "$1" in -f|--force|--refresh-skills) REFRESH=1 shift ;; --no-refresh|--offline) REFRESH=0 shift ;; --overwrite-custom) OVERWRITE_CUSTOM=1 shift ;; -h|--help) cat <&2 exit 1 fi shift ;; esac done if [ -z "$TARGET_DIR" ]; then TARGET_DIR="$(pwd)" fi echo "====================================================================" echo "⚡ Starting Multi-Agent Mux (MAM) Installation" echo "📂 Target Workspace: $TARGET_DIR" echo "====================================================================" # --- 1. System Requirements Validation --- echo "🔍 Checking system dependencies..." check_cmd() { local cmd="$1" if ! command -v "$cmd" &>/dev/null; then echo "❌ Error: '$cmd' is not installed. Please install it first." >&2 exit 1 fi } check_cmd herdr check_cmd python3 PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') PYTHON_MAJOR="${MIN_PYTHON_VERSION%%.*}" PYTHON_MINOR="${MIN_PYTHON_VERSION##*.}" if python3 -c "import sys; exit(0 if sys.version_info >= ($PYTHON_MAJOR, $PYTHON_MINOR) else 1)"; then echo "✅ Python $PYTHON_VERSION detected." else echo "❌ Error: Python version must be $MIN_PYTHON_VERSION or higher. Detected: $PYTHON_VERSION" >&2 exit 1 fi if ! python3 -c "import yaml" &>/dev/null; then echo "❌ Error: 'PyYAML' is not installed in system python3." >&2 exit 1 fi echo "✅ PyYAML (system dependency) detected." # --- 2. Workspace Setup --- mkdir -p "$TARGET_DIR" cd "$TARGET_DIR" REPO_URL="${MAM_REPO_URL:-https://git.godopu.com/tmpl/multi-agent-mux.git}" ARCHIVE_URL="${MAM_ARCHIVE_URL:-https://git.godopu.com/tmpl/multi-agent-mux/archive/main.tar.gz}" check_assets_present() { local dir="${1:-.}" local core_files=( ".agents/skills/lib.sh" ".agents/skills/multi-agent-mux-create/scripts/create_session.sh" ".agents/skills/multi-agent-mux-delegate-job/scripts/registry.py" ".agents/skills/multi-agent-mux-status/scripts/status.sh" ".agents/skills/multi-agent-mux-loop/scripts/run_loop.sh" ".agents/hooks.json" ".agents/hooks/loop_delegation_guard.sh" ) for f in "${core_files[@]}"; do if [ ! -f "$dir/$f" ]; then return 1 fi done return 0 } # Fetch orchestration assets if REFRESH=1 or if core assets are missing. if [ "$REFRESH" -eq 1 ] || [ "${MAM_SKIP_VENV:-0}" -eq 1 ] || ! check_assets_present "."; then echo "đŸ“Ĩ Staging orchestration assets from Gitea repository..." STAGE_DIR="$(mktemp -d)" trap 'rm -rf "$STAGE_DIR"' EXIT OWNERSHIP_LIB="$STAGE_DIR/deploy/lib_ownership.sh" FETCH_METHOD="archive" if [ -d "$REPO_URL" ]; then echo "🌐 Copying local working tree into a staging area..." cp -R "$REPO_URL/." "$STAGE_DIR/" FETCH_METHOD="local" elif command -v git &>/dev/null; then echo "🌐 Cloning repository (shallow) into a staging area..." git clone --depth 1 "$REPO_URL" "$STAGE_DIR" FETCH_METHOD="git" elif command -v curl &>/dev/null; then echo "🌐 Downloading and extracting archive into a staging area..." curl -fsSL "$ARCHIVE_URL" | tar -xz --strip-components=1 -C "$STAGE_DIR" \ --exclude='*/.agents/reports/*' --exclude='*/.agents/references/*' 2>/dev/null || true FETCH_METHOD="archive" else echo "❌ Error: neither 'git' nor 'curl' is available to fetch the skills." >&2 exit 1 fi if [ ! -f "$OWNERSHIP_LIB" ]; then echo "❌ Error: missing $OWNERSHIP_LIB; refusing to install with unknown ownership rules." >&2 exit 1 fi # shellcheck source=deploy/lib_ownership.sh . "$OWNERSHIP_LIB" if ! check_assets_present "$STAGE_DIR"; then echo "❌ Error: fetched source is missing core runtime assets. Aborting." >&2 exit 1 fi mkdir -p .mam MANIFEST_FILE=".mam/install_manifest.txt" touch "$MANIFEST_FILE" # Migrate legacy root layout for remove.sh and update.sh into .mam_deploy/ if owned mkdir -p .mam_deploy for legacy_script in remove.sh update.sh; do if [ -f "$legacy_script" ] && grep -Fqx "$legacy_script" "$MANIFEST_FILE" 2>/dev/null; then mv -f "$legacy_script" ".mam_deploy/$legacy_script" python3 -c ' import sys path = sys.argv[1] old_s = sys.argv[2] new_s = sys.argv[3] with open(path, "r") as f: lines = f.readlines() with open(path, "w") as f: for line in lines: if line.strip() == old_s: f.write(new_s + "\n") else: f.write(line) ' "$MANIFEST_FILE" "$legacy_script" ".mam_deploy/$legacy_script" 2>/dev/null || true fi done # Safe refresh & fingerprint checking logic TS=$(date -u +%Y%m%dT%H%M%SZ) FRAMEWORK_LEDGER=".mam/.framework_ledger.tmp" : > "$FRAMEWORK_LEDGER" mkdir -p .agents ( cd "$STAGE_DIR/.agents" && find . -type f -print ) | while IFS= read -r rel; do case "$rel" in ./reports/*|./references/*) continue ;; *.tmp|*.log|*.pyc|*/__pycache__/*) continue ;; esac dest=".agents/${rel#./}" mkdir -p "$(dirname "$dest")" if is_framework_owned "$dest"; then STAGING_FILE="$STAGE_DIR/.agents/$rel" printf '%s\t%s\n' "$STAGING_FILE" "$dest" >> "$FRAMEWORK_LEDGER" IS_REGISTRY=0 if is_registry_file "$dest"; then IS_REGISTRY=1; fi ACTION=$(python3 - "$dest" "$STAGING_FILE" ".mam/asset_hashes.txt" "$OVERWRITE_CUSTOM" "$IS_REGISTRY" <<'PY' import sys, hashlib, os target_path, staging_path, hash_db_path, force_overwrite, is_registry = ( sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] == "1", sys.argv[5] == "1" ) def file_sha(path): if not os.path.exists(path): return None h = hashlib.sha256() with open(path, "rb") as f: while chunk := f.read(65536): h.update(chunk) return h.hexdigest() target_sha = file_sha(target_path) staging_sha = file_sha(staging_path) if target_sha is None: print("COPY_NEW") sys.exit(0) if target_sha == staging_sha: print("NO_OP") sys.exit(0) db_sha = None if os.path.exists(hash_db_path): with open(hash_db_path, "r") as f: for line in f: parts = line.strip().split(None, 1) if len(parts) == 2 and parts[1] == target_path: db_sha = parts[0] break if db_sha is None: print("BOOTSTRAP_OVERWRITE") elif target_sha == db_sha: print("UPDATE_UNMODIFIED") else: if force_overwrite: print("FORCE_OVERWRITE_CUSTOM") elif is_registry: print("MERGE_REGISTRY") else: print("PRESERVE_CUSTOM") PY ) case "$ACTION" in COPY_NEW|UPDATE_UNMODIFIED|BOOTSTRAP_OVERWRITE|FORCE_OVERWRITE_CUSTOM) if [ "$ACTION" = "BOOTSTRAP_OVERWRITE" ] || [ "$ACTION" = "FORCE_OVERWRITE_CUSTOM" ]; then BACKUP_DIR=".mam/skill-backups/$TS/$(dirname "$dest")" mkdir -p "$BACKUP_DIR" cp "$dest" "$BACKUP_DIR/" fi cp -f "$STAGING_FILE" "$dest" if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then echo "$dest" >> "$MANIFEST_FILE" fi ;; MERGE_REGISTRY) BACKUP_DIR=".mam/skill-backups/$TS/$(dirname "$dest")" mkdir -p "$BACKUP_DIR" cp "$dest" "$BACKUP_DIR/" MERGE_MSG=$(python3 - "$dest" "$STAGING_FILE" ".mam/base/$dest" <<'MERGEREG' import json, os, sys target_path, upstream_path, base_path = sys.argv[1], sys.argv[2], sys.argv[3] def load(path): if not os.path.exists(path): return None try: with open(path, encoding="utf-8") as f: d = json.load(f) return d if isinstance(d, dict) else None except Exception: return None target, upstream, base = load(target_path), load(upstream_path), load(base_path) if target is None or upstream is None: print("unmergeable (not a JSON object) -- kept your file untouched") sys.exit(0) merged = dict(target) added, updated, conflicts, retired = [], [], [], [] if base is None: for k, v in upstream.items(): if k not in merged: merged[k] = v added.append(k) else: for k, v in upstream.items(): if k not in base: if k not in merged: merged[k] = v added.append(k) elif merged[k] != v: conflicts.append(k) elif k not in merged: added.append(k) merged[k] = v elif merged[k] == base[k]: if merged[k] != v: merged[k] = v updated.append(k) elif merged[k] != v: conflicts.append(k) for k in base: if k not in upstream and k in merged and merged[k] == base[k]: del merged[k] retired.append(k) if merged != target: tmp = target_path + ".merge.tmp" with open(tmp, "w", encoding="utf-8") as f: json.dump(merged, f, indent=2) f.write("\n") os.replace(tmp, target_path) bits = [] for label, keys in (("added", added), ("updated", updated), ("retired", retired), ("kept yours", conflicts)): if keys: bits.append("%s %s" % (label, ", ".join(sorted(keys)))) print("; ".join(bits) if bits else "already in sync") MERGEREG ) || MERGE_MSG="merge failed -- kept your file untouched" echo "MERGED:$dest:$MERGE_MSG" if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then echo "$dest" >> "$MANIFEST_FILE" fi ;; PRESERVE_CUSTOM) BACKUP_DIR=".mam/skill-backups/$TS/$(dirname "$dest")" mkdir -p "$BACKUP_DIR" cp "$dest" "$BACKUP_DIR/" echo "PRESERVED:$dest" ;; NO_OP) if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then echo "$dest" >> "$MANIFEST_FILE" fi ;; esac elif [ ! -e "$dest" ]; then cp "$STAGE_DIR/.agents/$rel" "$dest" if ! grep -Fqx "$dest" "$MANIFEST_FILE" 2>/dev/null; then echo "$dest" >> "$MANIFEST_FILE" fi fi done | while IFS= read -r line; do if [[ "$line" == PRESERVED:* ]]; then echo "â„šī¸ Local modification detected: ${line#PRESERVED:}" >&2 elif [[ "$line" == MERGED:* ]]; then _mam_rest="${line#MERGED:}" echo "🔀 Registry merged: ${_mam_rest%%:*} (${_mam_rest#*:})" >&2 fi done # Copy root docs (R-2 essential set) ROOT_DOCS="AGENTS.md" if [ "${MAM_INSTALL_DOCS:-minimal}" = "full" ]; then ROOT_DOCS="AGENTS.md MESSAGING.md BOOTSTRAP.md BOOTSTRAP.ko.md" fi for doc in $ROOT_DOCS; do if [ -f "$STAGE_DIR/$doc" ] && [ ! -e "$doc" ]; then cp "$STAGE_DIR/$doc" . echo "$doc" >> "$MANIFEST_FILE" fi done # Install remove.sh and update.sh into .mam_deploy/ mkdir -p .mam_deploy if [ -f "$STAGE_DIR/deploy/remove.sh" ]; then cp "$STAGE_DIR/deploy/remove.sh" .mam_deploy/remove.sh chmod 0755 .mam_deploy/remove.sh if ! grep -Fqx ".mam_deploy/remove.sh" "$MANIFEST_FILE" 2>/dev/null; then echo ".mam_deploy/remove.sh" >> "$MANIFEST_FILE" fi fi if [ -f "$STAGE_DIR/deploy/update.sh" ]; then cp "$STAGE_DIR/deploy/update.sh" .mam_deploy/update.sh chmod 0755 .mam_deploy/update.sh if ! grep -Fqx ".mam_deploy/update.sh" "$MANIFEST_FILE" 2>/dev/null; then echo ".mam_deploy/update.sh" >> "$MANIFEST_FILE" fi fi if [ -f "$STAGE_DIR/.mam.env.example" ] && [ ! -e ".mam.env.example" ]; then cp "$STAGE_DIR/.mam.env.example" . echo ".mam.env.example" >> "$MANIFEST_FILE" fi if [ -f "$STAGE_DIR/deploy/INSTALL.md" ]; then mkdir -p .agents cp -f "$STAGE_DIR/deploy/INSTALL.md" .agents/INSTALL.md printf '%s\t%s\n' "$STAGE_DIR/deploy/INSTALL.md" ".agents/INSTALL.md" \ >> "$FRAMEWORK_LEDGER" if ! grep -Fqx ".agents/INSTALL.md" "$MANIFEST_FILE" 2>/dev/null; then echo ".agents/INSTALL.md" >> "$MANIFEST_FILE" fi fi # Re-build asset_hashes.txt from the UPSTREAM copy of every framework-owned file python3 - .mam/asset_hashes.txt "$FRAMEWORK_LEDGER" <<'HASHDB' import os, hashlib, sys hash_db_path, ledger_path = 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() hashes = {} with open(ledger_path) as f: for line in f: line = line.rstrip("\n") if "\t" not in line: continue upstream, dest = line.split("\t", 1) if os.path.exists(upstream): hashes[dest] = sha(upstream) with open(hash_db_path, "w") as f: f.writelines(sorted("%s %s\n" % (h, d) for d, h in hashes.items())) HASHDB # Snapshot registry base from upstream while IFS="$(printf '\t')" read -r _up _dest; do [ -n "${_dest:-}" ] || continue is_registry_file "$_dest" || continue [ -f "$_up" ] || continue mkdir -p ".mam/base/$(dirname "$_dest")" cp -f "$_up" ".mam/base/$_dest" done < "$FRAMEWORK_LEDGER" rm -f "$FRAMEWORK_LEDGER" # Record version stamp STAGE_COMMIT="unknown" if [ -d "$STAGE_DIR/.git" ]; then STAGE_COMMIT=$(git -C "$STAGE_DIR" rev-parse HEAD 2>/dev/null || echo "unknown") fi cat < .mam/version.txt source=$REPO_URL commit=$STAGE_COMMIT fetched_at=$TS method=$FETCH_METHOD EOF rm -rf "$STAGE_DIR" trap - EXIT echo "✅ Skills staged into workspace (user documents and custom configs preserved)." fi if ! check_assets_present "."; then echo "❌ Error: Core runtime assets missing after setup." >&2 exit 1 fi echo "✅ Orchestration skills present." # --- 3. Python Virtual Environment (.venv) Setup --- if [ "${MAM_SKIP_VENV:-0}" -eq 1 ]; then echo "⏊ Skipping virtualenv setup (MAM_SKIP_VENV=1)." else echo "🐍 Bootstrapping Python virtual environment ($VENV_NAME)..." if [ ! -d "$VENV_NAME" ]; then python3 -m venv "$VENV_NAME" echo "✅ Created virtualenv at $VENV_NAME." else echo "â„šī¸ Virtualenv ($VENV_NAME) already exists. Reusing." fi echo "đŸ“Ļ Installing/upgrading core dependencies inside $VENV_NAME..." # shellcheck disable=SC1091 source "$VENV_NAME/bin/activate" python -m pip install --upgrade pip -q REQ_FILE=".agents/skills/multi-agent-mux-delegate-job/requirements.txt" if [ -f "$REQ_FILE" ]; then pip install -r "$REQ_FILE" -q echo "✅ Installed dependencies from $REQ_FILE." else pip install "paho-mqtt>=2.0.0" pyyaml -q echo "✅ Installed default fallback dependencies (paho-mqtt, pyyaml)." fi deactivate fi # --- 4. Environment Configuration (.mam.env) --- echo "âš™ī¸ Configuring environment variables..." MAM_ENV=".mam.env" LEGACY_ENV=".env" if [ -f "$LEGACY_ENV" ] && [ ! -f "$MAM_ENV" ]; then echo "đŸ“Ļ Migrating existing $LEGACY_ENV to $MAM_ENV..." cp "$LEGACY_ENV" "$MAM_ENV" fi if [ ! -f "$MAM_ENV" ]; then echo "📝 Initializing $MAM_ENV with default orchestration configuration..." MAM_CLIENT_PREFIX="mam-agent" MAM_PORT="1883" cat < "$MAM_ENV" # ============================================================================== # Multi-Agent Mux (MAM) Environment Configuration # ============================================================================== # Generated by install.sh at $(date -u +'%Y-%m-%dT%H:%M:%SZ') MQTT_BROKER=localhost MQTT_PORT=$MAM_PORT MQTT_CLIENT_ID_PREFIX=$MAM_CLIENT_PREFIX MQTT_RETRY_INTERVAL=2 MQTT_MAX_RETRIES=5 HERDR_SERVER_NAME=multi-agent-mux HERDR_SOCKET_PATH=$HOME/.config/herdr/herdr.sock MAM_LOG_LEVEL=INFO MAM_EVENT_RETENTION_DAYS=7 EOF echo "$MAM_ENV" >> "$MANIFEST_FILE" echo "✅ Initialized $MAM_ENV." else echo "â„šī¸ $MAM_ENV already exists. Preserving existing settings." fi # Ensure gitignore handles MAM isolation files (R-4: managed block) GITIGNORE=".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 if [ "${MAM_SKIP_GITIGNORE:-0}" -eq 0 ]; then python3 -c ' import os if os.path.exists(".mam/install_state"): with open(".mam/install_state", "a") as f: f.write("gitignore_created=1\n") else: os.makedirs(".mam", exist_ok=True) with open(".mam/install_state", "w") as f: f.write("gitignore_created=1\n") ' fi echo "====================================================================" echo "🎉 Multi-Agent Mux (MAM) Installation Completed Successfully!" echo "====================================================================" cat <