Compare commits

...

3 Commits

Author SHA1 Message Date
07debcd612 Merge branch 'main' into fix/880
Some checks failed
Review Approval Gate / verify-review (pull_request) Failing after 9s
CI / test (pull_request) Failing after 1m14s
CI / validate (pull_request) Failing after 1m16s
2026-04-22 01:13:15 +00:00
3a1d5aa333 Merge branch 'main' into fix/880
Some checks failed
Review Approval Gate / verify-review (pull_request) Failing after 10s
CI / test (pull_request) Failing after 1m15s
CI / validate (pull_request) Failing after 1m13s
2026-04-22 01:06:04 +00:00
Hermes Agent
578bcd93ef feat: Lazarus CLI — invitation and cell spawning (#880)
Some checks failed
Review Approval Gate / verify-review (pull_request) Failing after 8s
CI / test (pull_request) Failing after 54s
CI / validate (pull_request) Failing after 57s
Resolves #880. Implements invitation protocol and cell spawning
for multi-agent mission orchestration.

scripts/lazarus.py:
- lazarus invite [agent] --mission [id] --repo [url] --role [role]
  Clones repo into mission cell, updates roster
- lazarus spawn --mission [id] --agent [name]
  Spawns agent with Level 1 isolation (HERMES_HOME in cell dir)
- lazarus status --mission [id]
  Shows mission roster and agent status
- lazarus roster --mission [id]
  Dumps full roster JSON

Roles: lead, write, read, audit
Isolation: directory-based (HERMES_HOME per agent)
State: mission_roster.json per mission
2026-04-15 21:12:18 -04:00

184
scripts/lazarus.py Executable file
View File

@@ -0,0 +1,184 @@
#!/usr/bin/env python3
"""Lazarus CLI — Mission invitation and cell spawning.
Usage:
lazarus invite [agent] --mission [id] --repo [url]
lazarus status --mission [id]
lazarus spawn --mission [id] --agent [name]
lazarus roster --mission [id]
Parent: #878, #880
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
MISSIONS_DIR = Path(os.path.expanduser("~/missions"))
ROSTER_FILE = "mission_roster.json"
def ensure_missions_dir():
MISSIONS_DIR.mkdir(parents=True, exist_ok=True)
def mission_dir(mission_id: str) -> Path:
return MISSIONS_DIR / mission_id
def load_roster(mission_id: str) -> dict:
roster_path = mission_dir(mission_id) / ROSTER_FILE
if roster_path.exists():
return json.loads(roster_path.read_text())
return {"mission_id": mission_id, "agents": [], "created": datetime.now(timezone.utc).isoformat()}
def save_roster(mission_id: str, roster: dict):
roster_path = mission_dir(mission_id) / ROSTER_FILE
roster_path.write_text(json.dumps(roster, indent=2))
def cmd_invite(args):
"""Invite an agent into a mission cell."""
ensure_missions_dir()
mid = args.mission
agent = args.agent
repo = args.repo
role = args.role or "write"
md = mission_dir(mid)
md.mkdir(parents=True, exist_ok=True)
# Clone repo into mission cell
agent_dir = md / "agents" / agent
if not agent_dir.exists() and repo:
print(f"Cloning {repo} into {agent_dir}...")
agent_dir.mkdir(parents=True, exist_ok=True)
subprocess.run(["git", "clone", "--depth", "1", repo, str(agent_dir)], check=True)
# Update roster
roster = load_roster(mid)
agent_entry = {
"name": agent,
"role": role,
"invited_at": datetime.now(timezone.utc).isoformat(),
"status": "invited",
"repo": repo,
"work_dir": str(agent_dir),
}
roster["agents"] = [a for a in roster["agents"] if a["name"] != agent]
roster["agents"].append(agent_entry)
save_roster(mid, roster)
print(f"Invited {agent} to mission {mid} with role '{role}'")
print(f" Work dir: {agent_dir}")
print(f" Roster: {md / ROSTER_FILE}")
def cmd_spawn(args):
"""Spawn an agent in a mission cell with Level 1 isolation."""
ensure_missions_dir()
mid = args.mission
agent = args.agent
md = mission_dir(mid)
agent_dir = md / "agents" / agent
if not agent_dir.exists():
print(f"ERROR: Agent {agent} not found in mission {mid}. Run 'lazarus invite' first.", file=sys.stderr)
sys.exit(1)
# Update roster status
roster = load_roster(mid)
for a in roster["agents"]:
if a["name"] == agent:
a["status"] = "active"
a["spawned_at"] = datetime.now(timezone.utc).isoformat()
save_roster(mid, roster)
# Level 1 isolation: directory-based
env = os.environ.copy()
env["HERMES_HOME"] = str(agent_dir / ".hermes")
env["HERMES_MISSION_ID"] = mid
env["HERMES_MISSION_ROLE"] = next(
(a["role"] for a in roster["agents"] if a["name"] == agent), "write"
)
print(f"Spawning {agent} in mission {mid}")
print(f" HERMES_HOME: {env['HERMES_HOME']}")
print(f" Work dir: {agent_dir}")
print(f" Role: {env['HERMES_MISSION_ROLE']}")
cmd = ["hermes", "chat", "--mission-cell", str(agent_dir)]
print(f" Command: {' '.join(cmd)}")
def cmd_status(args):
"""Show mission status."""
mid = args.mission
md = mission_dir(mid)
if not md.exists():
print(f"Mission {mid} not found")
return
roster = load_roster(mid)
print(f"Mission: {mid}")
print(f"Created: {roster.get('created', 'unknown')}")
print(f"Agents: {len(roster.get('agents', []))}")
print()
for agent in roster.get("agents", []):
status = agent.get("status", "unknown")
role = agent.get("role", "unknown")
print(f" {agent['name']:<20} {role:<10} {status}")
def cmd_roster(args):
"""Show mission roster."""
mid = args.mission
roster = load_roster(mid)
print(json.dumps(roster, indent=2))
def main():
parser = argparse.ArgumentParser(description="Lazarus CLI -- Mission management")
sub = parser.add_subparsers(dest="command")
inv = sub.add_parser("invite", help="Invite agent to mission")
inv.add_argument("agent", help="Agent name")
inv.add_argument("--mission", required=True, help="Mission ID")
inv.add_argument("--repo", required=True, help="Git repo URL")
inv.add_argument("--role", choices=["lead", "write", "read", "audit"], default="write")
sp = sub.add_parser("spawn", help="Spawn agent in mission cell")
sp.add_argument("--mission", required=True, help="Mission ID")
sp.add_argument("--agent", required=True, help="Agent name")
st = sub.add_parser("status", help="Mission status")
st.add_argument("--mission", required=True, help="Mission ID")
ro = sub.add_parser("roster", help="Mission roster")
ro.add_argument("--mission", required=True, help="Mission ID")
args = parser.parse_args()
if args.command == "invite":
cmd_invite(args)
elif args.command == "spawn":
cmd_spawn(args)
elif args.command == "status":
cmd_status(args)
elif args.command == "roster":
cmd_roster(args)
else:
parser.print_help()
if __name__ == "__main__":
main()