622 lines
19 KiB
Bash
622 lines
19 KiB
Bash
#!/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 <<EOF
|
||
Usage: $0 [options] [target_dir]
|
||
|
||
Options:
|
||
-f, --force, --refresh-skills Fetch and refresh skills (now default)
|
||
--no-refresh, --offline Skip fetching latest assets if assets exist
|
||
--overwrite-custom Force overwrite user-modified skill files (backup still created)
|
||
-h, --help Show this help message
|
||
EOF
|
||
exit 0
|
||
;;
|
||
*)
|
||
if [ -z "$TARGET_DIR" ]; then
|
||
TARGET_DIR="$1"
|
||
else
|
||
echo "❌ Error: Unknown argument: $1" >&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 <<EOF > .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="hermes"
|
||
MAM_PORT="1883"
|
||
|
||
cat <<EOF > "$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 <<EOF
|
||
|
||
--------------------------------------------------------------------------------
|
||
💡 Next Steps:
|
||
1. Initialize a new isolated session:
|
||
$ bash .agents/skills/multi-agent-mux-create/scripts/create_session.sh \\
|
||
--workspace "$TARGET_DIR" --agent claude --role developer --isolate
|
||
|
||
2. Monitor live agent states:
|
||
$ bash .agents/skills/multi-agent-mux-status/scripts/status.sh
|
||
|
||
3. Update installed skills in the future:
|
||
$ bash .mam_deploy/update.sh
|
||
--------------------------------------------------------------------------------
|
||
EOF
|