feat(deploy): implement lib_ownership, registry merge, and deployment test suite
This commit is contained in:
+8
-5
@@ -6,12 +6,15 @@ This directory contains packaging templates and installation scripts to deploy t
|
||||
|
||||
## 📁 Deployment Directory Structure
|
||||
|
||||
* **`install.sh`**: A self-contained, idempotent remote shell installer (via curl) that checks system requirements (`herdr`, `python3`), detects NFS/network filesystem mounts, sets up a local python virtual environment (`.venv`), and initializes environment configuration (`.mam.env`).
|
||||
* **`install_mam.sh`**: A local-clone installer that copies rules/skills (`.agents/`), `AGENTS.md`, and sets up environment bootstrap on target projects.
|
||||
* **`install.sh`**: A self-contained, idempotent remote shell installer (via curl) that checks system requirements (`herdr`, `python3`), sets up a local python virtual environment (`.venv`), and performs 3-way refresh with key-level registry merge (Rev.2).
|
||||
* **`install_mam.sh`**: A local-clone installer that copies rules/skills (`.agents/`), `AGENTS.md`, manifests, and asset hashes into target projects.
|
||||
* **`lib_ownership.sh`**: Single source of truth for framework-owned files and key-level registry files.
|
||||
* **`update.sh`**: In-place updater script installed into target `.mam_deploy/update.sh`.
|
||||
* **`remove.sh`**: Clean uninstaller script installed into target `.mam_deploy/remove.sh`.
|
||||
* **`generate-env.sh`**: Environment configuration bootstrap helper.
|
||||
* **`INSTALL.md`**: Detailed installation and quick-start user manual.
|
||||
* **`plugin.json`**: Metadata declaration file to register MAM as an installable plugin for AI Agent coding platforms (such as Claude Code, Antigravity, or other TUI clients).
|
||||
* **`gitea-ci.yml`**: CI/CD pipeline definition template for Gitea Actions (running ShellCheck linting on bash scripts, validation on python scripts, and compilation tests).
|
||||
* **`plugin.json`**: Metadata declaration file to register MAM as an installable plugin for AI Agent coding platforms.
|
||||
* **`gitea-ci.yml`**: CI/CD pipeline definition template for Gitea Actions (running ShellCheck linting, Python syntax checks, and pytest suite).
|
||||
|
||||
---
|
||||
|
||||
@@ -37,7 +40,7 @@ bash deploy/install_mam.sh --target /path/to/your/project
|
||||
Refer to **`INSTALL.md`** inside this directory for the full instructions and workflows.
|
||||
|
||||
> [!NOTE]
|
||||
> The local-clone installer does not ship `update.sh`/`remove.sh` to targets. To enable in-place updates, re-run the remote installer (`curl ... | bash`) or copy `deploy/update.sh` + `deploy/remove.sh` manually.
|
||||
> `install_mam.sh` automatically deploys `.mam_deploy/update.sh` and `.mam_deploy/remove.sh` into target workspaces, recording install manifests and asset hashes to enable clean updates and uninstalls.
|
||||
|
||||
### 3. Custom Fork / Private Mirror Installations
|
||||
If you run a private mirror or fork, you can override the source URLs during installation using environment variables:
|
||||
|
||||
@@ -33,6 +33,8 @@ jobs:
|
||||
shellcheck .agents/skills/multi-agent-mux-stop/scripts/stop_session.sh
|
||||
shellcheck .agents/skills/multi-agent-mux-monitor/scripts/reconcile.sh
|
||||
shellcheck .agents/skills/multi-agent-mux-loop/scripts/run_loop.sh
|
||||
shellcheck .agents/hooks/loop_delegation_guard.sh
|
||||
shellcheck deploy/lib_ownership.sh
|
||||
shellcheck deploy/install.sh
|
||||
shellcheck deploy/install_mam.sh
|
||||
shellcheck deploy/generate-env.sh
|
||||
@@ -73,3 +75,34 @@ jobs:
|
||||
echo "🔍 Verifying Python file compilation..."
|
||||
python -m py_compile .agents/skills/multi-agent-mux-delegate-job/scripts/*.py
|
||||
echo "✅ All Python files compiled successfully."
|
||||
|
||||
test:
|
||||
name: Run Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install Test Dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [ -f requirements-dev.txt ]; then
|
||||
pip install -r requirements-dev.txt
|
||||
else
|
||||
pip install pytest
|
||||
fi
|
||||
if [ -f .agents/skills/multi-agent-mux-delegate-job/requirements.txt ]; then
|
||||
pip install -r .agents/skills/multi-agent-mux-delegate-job/requirements.txt
|
||||
fi
|
||||
|
||||
- name: Run Pytest
|
||||
run: |
|
||||
echo "🧪 Running full test suite..."
|
||||
pytest tests/ -q
|
||||
|
||||
|
||||
+237
-159
@@ -103,6 +103,8 @@ check_assets_present() {
|
||||
".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
|
||||
@@ -112,19 +114,13 @@ check_assets_present() {
|
||||
return 0
|
||||
}
|
||||
|
||||
is_framework_owned() {
|
||||
case "$1" in
|
||||
.agents/skills/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# 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..."
|
||||
@@ -137,14 +133,20 @@ if [ "$REFRESH" -eq 1 ] || [ "${MAM_SKIP_VENV:-0}" -eq 1 ] || ! check_assets_pre
|
||||
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/*' \
|
||||
--exclude='*/MESSAGING.md' --exclude='*/BOOTSTRAP.md' --exclude='*/BOOTSTRAP.ko.md' 2>/dev/null || true
|
||||
--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
|
||||
@@ -178,8 +180,8 @@ with open(path, "w") as f:
|
||||
|
||||
# Safe refresh & fingerprint checking logic
|
||||
TS=$(date -u +%Y%m%dT%H%M%SZ)
|
||||
PRESERVED_COUNT=0
|
||||
MODIFIED_FILES=()
|
||||
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
|
||||
@@ -192,12 +194,16 @@ with open(path, "w") as f:
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
|
||||
if is_framework_owned "$dest"; then
|
||||
# 3-way check using python3 inline
|
||||
STAGING_FILE="$STAGE_DIR/.agents/$rel"
|
||||
ACTION=$(python3 - "$dest" "$STAGING_FILE" ".mam/asset_hashes.txt" "$OVERWRITE_CUSTOM" <<'PY'
|
||||
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 = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] == "1"
|
||||
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):
|
||||
@@ -235,6 +241,8 @@ elif target_sha == db_sha:
|
||||
else:
|
||||
if force_overwrite:
|
||||
print("FORCE_OVERWRITE_CUSTOM")
|
||||
elif is_registry:
|
||||
print("MERGE_REGISTRY")
|
||||
else:
|
||||
print("PRESERVE_CUSTOM")
|
||||
PY
|
||||
@@ -251,6 +259,81 @@ PY
|
||||
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"
|
||||
@@ -272,29 +355,12 @@ PY
|
||||
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
|
||||
|
||||
# Re-build asset_hashes.txt for all framework owned files
|
||||
python3 - .mam/asset_hashes.txt <<'PY'
|
||||
import os, hashlib, sys
|
||||
|
||||
hash_db_path = sys.argv[1]
|
||||
hashes = []
|
||||
|
||||
for root, _, files in os.walk(".agents/skills"):
|
||||
for file in files:
|
||||
path = os.path.join(root, file)
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
while chunk := f.read(65536):
|
||||
h.update(chunk)
|
||||
hashes.append(f"{h.hexdigest()} {path}\n")
|
||||
|
||||
with open(hash_db_path, "w") as f:
|
||||
f.writelines(sorted(hashes))
|
||||
PY
|
||||
|
||||
# Copy root docs (R-2 essential set)
|
||||
ROOT_DOCS="AGENTS.md"
|
||||
if [ "${MAM_INSTALL_DOCS:-minimal}" = "full" ]; then
|
||||
@@ -330,12 +396,53 @@ PY
|
||||
echo ".mam.env.example" >> "$MANIFEST_FILE"
|
||||
fi
|
||||
|
||||
if [ -f "$STAGE_DIR/deploy/INSTALL.md" ] && [ ! -e ".agents/INSTALL.md" ]; then
|
||||
if [ -f "$STAGE_DIR/deploy/INSTALL.md" ]; then
|
||||
mkdir -p .agents
|
||||
cp "$STAGE_DIR/deploy/INSTALL.md" .agents/INSTALL.md
|
||||
echo ".agents/INSTALL.md" >> "$MANIFEST_FILE"
|
||||
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
|
||||
@@ -359,26 +466,79 @@ if ! check_assets_present "."; then
|
||||
fi
|
||||
echo "✅ Orchestration skills present."
|
||||
|
||||
echo "📂 Ensuring metadata directory structure (.mam/)..."
|
||||
mkdir -p .mam/jobs .mam/delegate_job_logs
|
||||
# --- 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
|
||||
|
||||
if [ -O .mam ]; then
|
||||
chmod 0700 .mam
|
||||
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
|
||||
|
||||
# R-4: Manage .gitignore block (never put .gitignore in manifest)
|
||||
# --- 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 <<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 <<<"
|
||||
|
||||
if [ "${MAM_SKIP_GITIGNORE:-0}" != "1" ]; then
|
||||
GI_CREATED=0
|
||||
[ -e .gitignore ] || { touch .gitignore; GI_CREATED=1; }
|
||||
|
||||
if ! grep -q '^gitignore_created=' .mam/install_state 2>/dev/null; then
|
||||
echo "gitignore_created=$GI_CREATED" >> .mam/install_state
|
||||
fi
|
||||
|
||||
python3 - .gitignore "$MAM_GI_START" "$MAM_GI_END" <<'PY'
|
||||
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]
|
||||
@@ -427,117 +587,35 @@ if not block_inserted:
|
||||
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
|
||||
|
||||
# --- 3. Check Network File System (NFS) Warnings ---
|
||||
echo "💾 Detecting file system mount type..."
|
||||
if command -v df &>/dev/null && command -v mount &>/dev/null; then
|
||||
MOUNTPOINT="$(df --output=target . 2>/dev/null | tail -1 || echo "")"
|
||||
if [ -n "$MOUNTPOINT" ]; then
|
||||
if mount | grep -q "$MOUNTPOINT.*nfs\|$MOUNTPOINT.*cifs\|$MOUNTPOINT.*fuse.sshfs"; then
|
||||
echo "⚠️ WARNING: Target directory is on a network filesystem."
|
||||
else
|
||||
echo "✅ File system supports WAL (Local storage detected)."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
echo "===================================================================="
|
||||
echo "🎉 Multi-Agent Mux (MAM) Installation Completed Successfully!"
|
||||
echo "===================================================================="
|
||||
cat <<EOF
|
||||
|
||||
# --- 4. Python Virtual Environment Setup ---
|
||||
if [ "${MAM_SKIP_VENV:-0}" != "1" ]; then
|
||||
echo "🐍 Bootstrapping Python virtual environment (.venv)..."
|
||||
if [ ! -d "$VENV_NAME" ]; then
|
||||
python3 -m venv "$VENV_NAME"
|
||||
echo "✅ Virtual environment created."
|
||||
else
|
||||
echo "ℹ️ Virtual environment (.venv) already exists. Skipping creation."
|
||||
fi
|
||||
--------------------------------------------------------------------------------
|
||||
💡 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
|
||||
|
||||
source "$VENV_NAME"/bin/activate
|
||||
pip install --upgrade pip
|
||||
2. Monitor live agent states:
|
||||
$ bash .agents/skills/multi-agent-mux-status/scripts/status.sh
|
||||
|
||||
REQ_FILE=".agents/skills/multi-agent-mux-delegate-job/requirements.txt"
|
||||
if [ -f "$REQ_FILE" ]; then
|
||||
echo "📦 Installing backplane dependencies from $REQ_FILE..."
|
||||
pip install -r "$REQ_FILE"
|
||||
echo "✅ Dependencies installed successfully."
|
||||
else
|
||||
pip install "paho-mqtt>=2.0.0" pyyaml
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 5. Generate Environment Template ---
|
||||
ENV_FILE=".mam.env"
|
||||
ENV_EXAMPLE=".mam.env.example"
|
||||
|
||||
migrate_legacy_env() {
|
||||
local manifest=".mam/install_manifest.txt"
|
||||
[ -f ".mam.env" ] && return 0
|
||||
[ -f ".env" ] || return 0
|
||||
if [ "${MAM_LEGACY_ENV_OWNED:-0}" = "1" ] || { [ -f "$manifest" ] && grep -Fqx ".env" "$manifest" 2>/dev/null; }; then
|
||||
mv -f ".env" ".mam.env"
|
||||
chmod 0600 ".mam.env" 2>/dev/null || true
|
||||
if [ -f "$manifest" ]; then
|
||||
if grep -Fqx ".env" "$manifest" 2>/dev/null; then
|
||||
python3 -c '
|
||||
import sys
|
||||
path = sys.argv[1]
|
||||
with open(path, "r") as f:
|
||||
lines = f.readlines()
|
||||
with open(path, "w") as f:
|
||||
for line in lines:
|
||||
if line.strip() == ".env":
|
||||
f.write(".mam.env\n")
|
||||
else:
|
||||
f.write(line)
|
||||
' "$manifest" 2>/dev/null || true
|
||||
else
|
||||
echo ".mam.env" >> "$manifest"
|
||||
fi
|
||||
fi
|
||||
echo "ℹ️ Legacy MAM config migrated: .env -> .mam.env"
|
||||
else
|
||||
echo "ℹ️ Existing .env left untouched (ownership unproven)."
|
||||
echo " MAM will read it via the deprecated fallback."
|
||||
echo " To migrate explicitly: deploy/generate-env.sh --migrate-legacy"
|
||||
fi
|
||||
}
|
||||
|
||||
migrate_legacy_env
|
||||
|
||||
if [ ! -f "$ENV_FILE" ] && [ ! -f ".env" ] && [ ! -f ".env.update-tmp" ]; then
|
||||
if [ -f "$ENV_EXAMPLE" ]; then
|
||||
echo "📝 Creating configuration from $ENV_EXAMPLE..."
|
||||
cp "$ENV_EXAMPLE" "$ENV_FILE"
|
||||
else
|
||||
echo "📝 Creating default $ENV_FILE..."
|
||||
touch "$ENV_FILE"
|
||||
fi
|
||||
|
||||
cat <<EOF >> "$ENV_FILE"
|
||||
|
||||
# === Installer-applied active defaults ===
|
||||
MQTT_BROKER=broker.hivemq.com
|
||||
MQTT_PORT=1883
|
||||
MQTT_TLS=0
|
||||
MQTT_CLIENT_ID_PREFIX=mam-agent
|
||||
HERDR_SERVER_NAME=default
|
||||
3. Update installed skills in the future:
|
||||
$ bash .mam_deploy/update.sh
|
||||
--------------------------------------------------------------------------------
|
||||
EOF
|
||||
chmod 0600 "$ENV_FILE"
|
||||
echo "✅ Config file .mam.env initialized with chmod 0600."
|
||||
|
||||
mkdir -p .mam
|
||||
touch .mam/install_manifest.txt
|
||||
echo "$ENV_FILE" >> .mam/install_manifest.txt
|
||||
else
|
||||
if [ -f "$ENV_FILE" ]; then
|
||||
echo "ℹ️ $ENV_FILE already exists. Skipping config override."
|
||||
else
|
||||
echo "ℹ️ Legacy environment detected. Preserved without shadowing."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "===================================================================="
|
||||
echo "🎉 Installation complete!"
|
||||
echo "✨ You can now run the status or monitor skills."
|
||||
echo "💡 Hint: Try executing: .venv/bin/python .agents/skills/multi-agent-mux-delegate-job/scripts/registry.py list"
|
||||
echo "===================================================================="
|
||||
|
||||
@@ -81,6 +81,14 @@ if [ "$SRC_DIR" = "$TARGET_DIR" ]; then
|
||||
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)
|
||||
@@ -147,6 +155,59 @@ if [ -f "$SRC_DIR/deploy/INSTALL.md" ]; then
|
||||
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"
|
||||
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
# Single source of truth for "which shipped files does the framework own".
|
||||
# Sourced by deploy/install.sh and deploy/install_mam.sh. Paths are workspace
|
||||
# relative (e.g. ".agents/hooks.json"); no leading "./".
|
||||
|
||||
# Framework-owned == shipped by this repo and safe to refresh in place.
|
||||
# Anything outside this set is the user's file and is only ever created
|
||||
# when absent.
|
||||
is_framework_owned() {
|
||||
case "$1" in
|
||||
.agents/skills/*) return 0 ;;
|
||||
.agents/hooks.json) return 0 ;;
|
||||
.agents/hooks/*) return 0 ;;
|
||||
.agents/MULTI_AGENT_RULES*.md) return 0 ;;
|
||||
.agents/INSTALL.md) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Registry files are key-value documents that BOTH sides legitimately write:
|
||||
# we ship framework hooks, the user registers their own. Preserving such a
|
||||
# file wholesale pins the workspace to its old hook set, so these get a
|
||||
# key-level 3-way merge instead.
|
||||
is_registry_file() {
|
||||
case "$1" in
|
||||
.agents/hooks.json) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
+13
-8
@@ -84,10 +84,16 @@ else
|
||||
".agents/skills/lib.sh"
|
||||
".agents/skills/multi-agent-mux-create"
|
||||
".agents/skills/multi-agent-mux-delegate-job"
|
||||
".agents/skills/multi-agent-mux-loop"
|
||||
".agents/skills/multi-agent-mux-monitor"
|
||||
".agents/skills/multi-agent-mux-resume"
|
||||
".agents/skills/multi-agent-mux-status"
|
||||
".agents/skills/multi-agent-mux-stop"
|
||||
".agents/hooks"
|
||||
".agents/hooks.json"
|
||||
".agents/MULTI_AGENT_RULES.md"
|
||||
".agents/MULTI_AGENT_RULES.ko.md"
|
||||
".agents/INSTALL.md"
|
||||
".venv"
|
||||
".mam"
|
||||
".mam_deploy"
|
||||
@@ -172,14 +178,13 @@ if [ ${#manifest_files[@]} -gt 0 ]; then
|
||||
delete_asset "$f"
|
||||
done
|
||||
else
|
||||
echo "⚠️ No manifest found. Deleting standard MAM skills..."
|
||||
delete_asset ".agents/skills/lib.sh"
|
||||
delete_asset ".agents/skills/multi-agent-mux-create"
|
||||
delete_asset ".agents/skills/multi-agent-mux-delegate-job"
|
||||
delete_asset ".agents/skills/multi-agent-mux-monitor"
|
||||
delete_asset ".agents/skills/multi-agent-mux-resume"
|
||||
delete_asset ".agents/skills/multi-agent-mux-status"
|
||||
delete_asset ".agents/skills/multi-agent-mux-stop"
|
||||
echo "⚠️ No manifest found. Deleting standard MAM assets..."
|
||||
for asset in ${fallback_assets[@]+"${fallback_assets[@]}"}; do
|
||||
case "$asset" in
|
||||
.venv|.mam|.mam_deploy) continue ;; # handled explicitly further down
|
||||
esac
|
||||
delete_asset "$asset"
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -d ".agents" ]; then
|
||||
|
||||
Reference in New Issue
Block a user