feat(deploy): implement lib_ownership, registry merge, and deployment test suite

This commit is contained in:
2026-08-07 23:48:49 +09:00
parent cc11a02784
commit 399242dac5
11 changed files with 939 additions and 172 deletions
+8 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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 "===================================================================="
+61
View File
@@ -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"
+29
View File
@@ -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
View File
@@ -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