Files
multi-agent-mux/deploy/install.sh
T
GodopuandClaude Opus 5.5 47fe24e07c feat: multi-agent-mux-update skill and in-place install (Issue #72)
The update is a skill (decision 4); deploy/update.sh and the installed
.mam_deploy/update.sh are gone.

- Phase 1 (installed code, update.sh): publish the update marker under the
  exclusive admission lock; refuse before any change while a running row, a
  live herdr agent in the workspace on any running server, an open job
  (unreadable JSON included, dry_run not), a live loop marker or a
  monitor.lock held by a monitor the skills did not start (U3) is there - the
  calling agent (server + pane, its row and one running job) excepted
  (lib_py/update.py precheck, decisions 1 and 7). Stop the monitor (TERM, then
  KILL and restart it if it does not end; decision 2), handle a leftover
  backup by its state (U4), back up .agents, .mam_deploy, root files, .venv,
  the installer's .mam files and the registry (SQLite backup API under the
  registry lock) into .mam/update-backup/, then exec a copy of
  install_phase.sh outside the tree (decision 5).
- The copy (no lib.sh/lib_py, bash 3.2) runs the installer in place; on
  failure it restores the backup and restarts the monitor from the restored
  tree; on success it runs phase 2, post_update.sh of the new tree, which
  converts the registry with lib_py.registry_format and restarts the monitor
  if it ran. The backup and the marker are removed at the end.
- install.sh updates in place (decisions 3, 4, 9): install_state keys are set,
  one line each; no updater is installed and an old one (.mam_deploy/update.sh,
  or a root update.sh the manifest lists) is removed; framework files the new
  version no longer ships are deleted when unmodified, kept and listed when
  modified, and dropped from base/, the manifest and asset_hashes.txt; a local
  source's .mam is not staged. remove.sh knows the new skill; CI shellchecks
  its scripts; docs point at the skill.
- test_issue8_update_state and test_deploy_layout td6/td27 run the skill.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 04:06:57 +00:00

672 lines
21 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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/lib_py/delegation/__init__.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..."
# Everything but .mam/: runtime state is never staged, and it may hold the
# update skill's rollback copy (.mam/update-backup/, Issue #72).
find "$REPO_URL/." -mindepth 1 -maxdepth 1 ! -name .mam -exec cp -R {} "$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"
# Drop every line naming this path from the manifest
manifest_drop() {
grep -Fvx -- "$1" "$MANIFEST_FILE" > "$MANIFEST_FILE.tmp" || true
mv -f "$MANIFEST_FILE.tmp" "$MANIFEST_FILE"
}
# Migrate legacy root layout for remove.sh into .mam_deploy/ if owned
mkdir -p .mam_deploy
for legacy_script in remove.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
# update.sh is no longer shipped (Issue #72; the multi-agent-mux-update skill
# updates). A legacy root update.sh goes only if the manifest says we put it there.
if [ -f update.sh ] && grep -Fqx update.sh "$MANIFEST_FILE" 2>/dev/null; then
rm -f update.sh
manifest_drop update.sh
fi
# 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 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
# An older install's updater is not shipped any more (Issue #72)
rm -f .mam_deploy/update.sh
manifest_drop .mam_deploy/update.sh
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
# Framework-owned files the previous install recorded (manifest and
# asset_hashes.txt) that this version no longer ships (Issue #72). Unmodified
# ones are deleted. Modified ones are kept and become the user's file: they
# leave the manifest, so remove.sh leaves them alone, and asset_hashes.txt
# (rebuilt below from the shipped files only).
if [ -f .mam/asset_hashes.txt ]; then
SHIPPED_LIST=".mam/.shipped_paths.tmp"
cut -f2 "$FRAMEWORK_LEDGER" > "$SHIPPED_LIST"
while read -r _old_sha _old_path; do
[ -n "${_old_path:-}" ] || continue
is_framework_owned "$_old_path" || continue
grep -Fqx -- "$_old_path" "$MANIFEST_FILE" || continue
if grep -Fqx -- "$_old_path" "$SHIPPED_LIST"; then continue; fi
if [ -f "$_old_path" ] && [ "$(python3 -c 'import hashlib, sys; print(hashlib.sha256(open(sys.argv[1], "rb").read()).hexdigest())' "$_old_path")" != "$_old_sha" ]; then
echo "ℹ️ Local modification detected (no longer shipped, kept as yours): $_old_path" >&2
else
rm -f "$_old_path"
_old_dir="$(dirname "$_old_path")"
while [ "$_old_dir" != ".agents" ] && rmdir "$_old_dir" 2>/dev/null; do
_old_dir="$(dirname "$_old_dir")"
done
fi
rm -f ".mam/base/$_old_path"
manifest_drop "$_old_path"
done < .mam/asset_hashes.txt
rm -f "$SHIPPED_LIST"
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
# Set the key: overwrite it in place, one line per key, other keys kept
python3 -c '
import os
os.makedirs(".mam", exist_ok=True)
lines = []
if os.path.exists(".mam/install_state"):
with open(".mam/install_state") as f:
lines = f.read().splitlines()
out, done = [], False
for line in lines:
if line.startswith("gitignore_created="):
if done:
continue
line, done = "gitignore_created=1", True
out.append(line)
if not done:
out.append("gitignore_created=1")
with open(".mam/install_state", "w") as f:
f.write("\n".join(out) + "\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 .agents/skills/multi-agent-mux-update/scripts/update.sh
--------------------------------------------------------------------------------
EOF