memory-os/icarus/scripts/smoke-handoff.sh

166 lines
5.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# smoke-handoff.sh -- prove the Icarus plugin handoff chain works end-to-end
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/icarus-smoke-XXXXXX")"
trap 'rm -rf "$TMP_ROOT"' EXIT
FABRIC_DIR="$TMP_ROOT/fabric"
ICARUS_HOME="$TMP_ROOT/.hermes-icarus"
DAEDALUS_HOME="$TMP_ROOT/.hermes-daedalus"
pass() { printf ' pass: %s\n' "$1"; }
fail() { printf ' FAIL: %s\n' "$1" >&2; exit 1; }
mkdir -p "$FABRIC_DIR" "$FABRIC_DIR/cold"
mkdir -p "$ICARUS_HOME/plugins/icarus" "$DAEDALUS_HOME/plugins/icarus"
# repo root IS the plugin — copy all plugin files + support scripts
for home in "$ICARUS_HOME" "$DAEDALUS_HOME"; do
cp "$REPO_DIR"/*.py "$home/plugins/icarus/"
cp "$REPO_DIR"/plugin.yaml "$home/plugins/icarus/"
[ -d "$REPO_DIR/scripts" ] && cp -R "$REPO_DIR/scripts" "$home/plugins/icarus/"
done
export FABRIC_DIR
export ICARUS_HOME
export DAEDALUS_HOME
export REPO_DIR
python3 - <<'PY'
import json
import os
import sys
from pathlib import Path
# load the plugin as a package (files use relative imports)
import importlib.util
import types
repo_dir = Path(os.environ["REPO_DIR"])
ns = types.ModuleType("hermes_plugins")
ns.__path__ = []
ns.__package__ = "hermes_plugins"
sys.modules["hermes_plugins"] = ns
spec = importlib.util.spec_from_file_location(
"hermes_plugins.icarus", str(repo_dir / "__init__.py"),
submodule_search_locations=[str(repo_dir)])
mod = importlib.util.module_from_spec(spec)
mod.__package__ = "hermes_plugins.icarus"
mod.__path__ = [str(repo_dir)]
sys.modules["hermes_plugins.icarus"] = mod
spec.loader.exec_module(mod)
hooks = mod.hooks
state = mod.state
tools = mod.tools
def parse_id(path: str) -> str:
for line in Path(path).read_text("utf-8").splitlines():
if line.startswith("id: "):
return line.split(": ", 1)[1].strip()
raise RuntimeError(f"missing id in {path}")
def run_as(agent_name: str, home: str):
os.environ["HERMES_AGENT_NAME"] = agent_name
os.environ["HERMES_HOME"] = home
state.AGENT_NAME = agent_name
state.HERMES_HOME = Path(home)
state.FABRIC_DIR = Path(os.environ["FABRIC_DIR"])
state._JOB_FILE = state.HERMES_HOME / ".icarus-training-job.txt"
state._STATE_FILE = state.HERMES_HOME / ".icarus-state.json"
state._REGISTRY_FILE = state.HERMES_HOME / ".icarus-models.json"
state.session_id = ""
state.exchanges = []
def expect(cond: bool, msg: str):
if not cond:
raise AssertionError(msg)
print(f" pass: {msg}")
icarus_home = os.environ["ICARUS_HOME"]
daedalus_home = os.environ["DAEDALUS_HOME"]
# 1. Builder writes an assigned handoff.
run_as("icarus", icarus_home)
raw = tools.fabric_write({
"type": "code-session",
"summary": "smoke handoff for daedalus",
"content": "Built the relay patch. Needs review. Token amber relay.",
"status": "open",
"assigned_to": "daedalus",
})
payload = json.loads(raw)
expect(payload.get("status") == "written", "builder handoff written")
task_path = payload["path"]
task_id = parse_id(task_path)
expect(task_id, "builder handoff has entry id")
# 2. Reviewer sees the handoff in session-start context.
run_as("daedalus", daedalus_home)
ctx = hooks.on_session_start(session_id="smoke-daedalus")
context = (ctx or {}).get("context", "")
expect("smoke handoff for daedalus" in context, "reviewer session-start sees handoff")
expect(f"id {task_id}" in context, "reviewer session-start includes source id")
# 3. Reviewer sees the handoff in fabric_pending.
pending = json.loads(tools.fabric_pending({}))
expect(pending.get("total", 0) >= 1, "fabric_pending returns assigned work")
open_tasks = pending.get("open_tasks", [])
expect(any(t.get("id") == task_id for t in open_tasks), "fabric_pending exposes exact task id")
# 4. Reviewer writes a linked review.
review_ref = f"icarus:{task_id}"
raw = tools.fabric_write({
"type": "review",
"summary": "reviewed smoke handoff from icarus",
"content": "Confirmed relay handoff pickup worked. One nit on naming.",
"review_of": review_ref,
"status": "completed",
"outcome": "pickup worked",
})
payload = json.loads(raw)
expect(payload.get("status") == "written", "reviewer linked review written")
review_path = payload["path"]
review_id = parse_id(review_path)
expect(review_id, "review has entry id")
# 5. Builder sees the linked review.
run_as("icarus", icarus_home)
ctx = hooks.on_session_start(session_id="smoke-icarus")
context = (ctx or {}).get("context", "")
expect("reviewed smoke handoff from icarus" in context, "builder session-start sees linked review")
expect(review_ref in context, "builder context shows review_of link")
# 6. Builder writes a linked fix.
raw = tools.fabric_write({
"type": "code-session",
"summary": "fixed smoke handoff after review",
"content": "Renamed relay variables and cleaned the patch after review.",
"revises": review_ref,
"status": "completed",
})
payload = json.loads(raw)
expect(payload.get("status") == "written", "builder linked fix written")
# 7. Recall retrieves the chain.
results = json.loads(tools.fabric_recall({"query": "amber relay handoff", "max_results": 5}))
entries = results.get("entries", [])
expect(any(e.get("id") == task_id for e in entries), "recall returns source handoff")
expect(any(e.get("review_of") == review_ref for e in entries), "recall returns linked review")
print("")
print("Smoke handoff OK")
print(f" fabric: {os.environ['FABRIC_DIR']}")
print(f" task id: {task_id}")
print(f" review_of:{review_ref}")
PY