Compare commits
3 Commits
fix/1536-h
...
fix/880
| Author | SHA1 | Date | |
|---|---|---|---|
| 07debcd612 | |||
| 3a1d5aa333 | |||
|
|
578bcd93ef |
3
app.js
3
app.js
@@ -734,9 +734,6 @@ async function init() {
|
||||
const response = await fetch('./portals.json');
|
||||
const portalData = await response.json();
|
||||
createPortals(portalData);
|
||||
|
||||
// Start portal hot-reload watcher
|
||||
if (window.PortalHotReload) PortalHotReload.start(5000);
|
||||
} catch (e) {
|
||||
console.error('Failed to load portals.json:', e);
|
||||
addChatMessage('error', 'Portal registry offline. Check logs.');
|
||||
|
||||
@@ -397,7 +397,6 @@
|
||||
<script src="./boot.js"></script>
|
||||
<script src="./avatar-customization.js"></script>
|
||||
<script src="./lod-system.js"></script>
|
||||
<script src="./portal-hot-reload.js"></script>
|
||||
<script>
|
||||
function openMemoryFilter() { renderFilterList(); document.getElementById('memory-filter').style.display = 'flex'; }
|
||||
function closeMemoryFilter() { document.getElementById('memory-filter').style.display = 'none'; }
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
/**
|
||||
* Portal Hot-Reload for The Nexus
|
||||
*
|
||||
* Watches portals.json for changes and hot-reloads portal list
|
||||
* without server restart. Existing connections unaffected.
|
||||
*
|
||||
* Usage:
|
||||
* PortalHotReload.start(intervalMs);
|
||||
* PortalHotReload.stop();
|
||||
* PortalHotReload.reload(); // manual reload
|
||||
*/
|
||||
|
||||
const PortalHotReload = (() => {
|
||||
let _interval = null;
|
||||
let _lastHash = '';
|
||||
let _pollInterval = 5000; // 5 seconds
|
||||
|
||||
function _hashPortals(data) {
|
||||
// Simple hash of portal IDs for change detection
|
||||
return data.map(p => p.id || p.name).sort().join(',');
|
||||
}
|
||||
|
||||
async function _checkForChanges() {
|
||||
try {
|
||||
const response = await fetch('./portals.json?t=' + Date.now());
|
||||
if (!response.ok) return;
|
||||
|
||||
const data = await response.json();
|
||||
const hash = _hashPortals(data);
|
||||
|
||||
if (hash !== _lastHash) {
|
||||
console.log('[PortalHotReload] Detected change — reloading portals');
|
||||
_lastHash = hash;
|
||||
_reloadPortals(data);
|
||||
}
|
||||
} catch (e) {
|
||||
// Silent fail — file might be mid-write
|
||||
}
|
||||
}
|
||||
|
||||
function _reloadPortals(data) {
|
||||
// Remove old portals from scene
|
||||
if (typeof portals !== 'undefined' && Array.isArray(portals)) {
|
||||
portals.forEach(p => {
|
||||
if (p.group && typeof scene !== 'undefined' && scene) {
|
||||
scene.remove(p.group);
|
||||
}
|
||||
});
|
||||
portals.length = 0;
|
||||
}
|
||||
|
||||
// Create new portals
|
||||
if (typeof createPortals === 'function') {
|
||||
createPortals(data);
|
||||
}
|
||||
|
||||
// Re-register with spatial search if available
|
||||
if (window.SpatialSearch && typeof portals !== 'undefined') {
|
||||
portals.forEach(p => {
|
||||
if (p.config && p.config.name && p.group) {
|
||||
SpatialSearch.register('portal', p, p.config.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Notify
|
||||
if (typeof addChatMessage === 'function') {
|
||||
addChatMessage('system', `Portals reloaded: ${data.length} portals active`);
|
||||
}
|
||||
|
||||
console.log(`[PortalHotReload] Reloaded ${data.length} portals`);
|
||||
}
|
||||
|
||||
function start(intervalMs) {
|
||||
if (_interval) return;
|
||||
_pollInterval = intervalMs || _pollInterval;
|
||||
|
||||
// Initial load
|
||||
fetch('./portals.json').then(r => r.json()).then(data => {
|
||||
_lastHash = _hashPortals(data);
|
||||
}).catch(() => {});
|
||||
|
||||
_interval = setInterval(_checkForChanges, _pollInterval);
|
||||
console.log(`[PortalHotReload] Watching portals.json every ${_pollInterval}ms`);
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (_interval) {
|
||||
clearInterval(_interval);
|
||||
_interval = null;
|
||||
console.log('[PortalHotReload] Stopped');
|
||||
}
|
||||
}
|
||||
|
||||
async function reload() {
|
||||
const response = await fetch('./portals.json?t=' + Date.now());
|
||||
const data = await response.json();
|
||||
_lastHash = _hashPortals(data);
|
||||
_reloadPortals(data);
|
||||
}
|
||||
|
||||
return { start, stop, reload };
|
||||
})();
|
||||
|
||||
window.PortalHotReload = PortalHotReload;
|
||||
184
scripts/lazarus.py
Executable file
184
scripts/lazarus.py
Executable 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()
|
||||
Reference in New Issue
Block a user