|
|
|
|
@@ -259,6 +259,14 @@ async def apply_diff(mirror_id: str, agent_name: str, diff: dict) -> list[str]:
|
|
|
|
|
if "runtime_config" in diff:
|
|
|
|
|
patch_body["runtimeConfig"] = diff["runtime_config"]["to"]
|
|
|
|
|
|
|
|
|
|
# Stamp claude_md_mtime + last_synced into metadata
|
|
|
|
|
mtime = diff.get("_claude_md_mtime")
|
|
|
|
|
if mtime:
|
|
|
|
|
current_meta = dict(patch_body.get("metadata") or {})
|
|
|
|
|
current_meta["claude_md_mtime"] = mtime
|
|
|
|
|
current_meta["claude_md_last_synced"] = datetime.now(timezone.utc).isoformat()
|
|
|
|
|
patch_body["metadata"] = current_meta
|
|
|
|
|
|
|
|
|
|
if patch_body:
|
|
|
|
|
status, data = await call_patch(mirror_id, patch_body)
|
|
|
|
|
if status >= 400:
|
|
|
|
|
@@ -278,12 +286,73 @@ async def apply_diff(mirror_id: str, agent_name: str, diff: dict) -> list[str]:
|
|
|
|
|
return errors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_claude_md_mtime(adapter_config: dict) -> str | None:
|
|
|
|
|
"""Return Unix mtime of the agent's instructionsFilePath, or None if file missing."""
|
|
|
|
|
path = adapter_config.get("instructionsFilePath", "")
|
|
|
|
|
if not path or not os.path.exists(path):
|
|
|
|
|
return None
|
|
|
|
|
return str(int(os.path.getmtime(path)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def check_instructions(agents: list[dict]) -> bool:
|
|
|
|
|
"""Print a report of all agents' instruction files. Returns True if all OK."""
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
all_ok = True
|
|
|
|
|
print(f"\n{'Agent':<30} {'File':<55} {'Status':<12} {'Size':>7} {'Modified'}")
|
|
|
|
|
print("-" * 115)
|
|
|
|
|
|
|
|
|
|
for agent in agents:
|
|
|
|
|
name = (agent.get("name") or agent.get("id") or "?")[:29]
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
adapter_cfg = agent.get("adapter_config") or {}
|
|
|
|
|
if isinstance(adapter_cfg, str):
|
|
|
|
|
adapter_cfg = json.loads(adapter_cfg)
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
print(f"{name:<30} {'(malformed adapter_config in DB)':<55} {'⚠ ERROR':<12}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
file_path = adapter_cfg.get("instructionsFilePath", "")
|
|
|
|
|
|
|
|
|
|
if not file_path:
|
|
|
|
|
print(f"{name:<30} {'(none)':<55} {'⚠ NOT SET':<12}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(file_path):
|
|
|
|
|
print(f"{name:<30} {file_path[-54:]:<55} {'❌ MISSING':<12}")
|
|
|
|
|
all_ok = False
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
stat = os.stat(file_path)
|
|
|
|
|
size_kb = stat.st_size // 1024
|
|
|
|
|
mtime = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
|
|
|
|
|
|
|
|
|
|
# Check for drift vs DB metadata
|
|
|
|
|
try:
|
|
|
|
|
metadata = agent.get("metadata") or {}
|
|
|
|
|
if isinstance(metadata, str):
|
|
|
|
|
metadata = json.loads(metadata)
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
metadata = {}
|
|
|
|
|
db_mtime = metadata.get("claude_md_mtime", "")
|
|
|
|
|
actual_mtime = str(int(stat.st_mtime))
|
|
|
|
|
drift = " ⚠ DRIFT" if db_mtime and db_mtime != actual_mtime else ""
|
|
|
|
|
|
|
|
|
|
print(f"{name:<30} {file_path[-54:]:<55} {'✅ OK':<12} {size_kb:>5}KB {mtime}{drift}")
|
|
|
|
|
|
|
|
|
|
print()
|
|
|
|
|
return all_ok
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def main() -> None:
|
|
|
|
|
p = argparse.ArgumentParser()
|
|
|
|
|
g = p.add_mutually_exclusive_group(required=True)
|
|
|
|
|
g.add_argument("--verify", action="store_true", help="Show current drift, no changes")
|
|
|
|
|
g.add_argument("--dry-run", action="store_true", help="Show what would change")
|
|
|
|
|
g.add_argument("--apply", action="store_true", help="Backup + apply changes")
|
|
|
|
|
g.add_argument("--check-instructions", action="store_true",
|
|
|
|
|
help="Scan all agents' instructionsFilePath and report missing/outdated files")
|
|
|
|
|
p.add_argument("--only", help="Sync only the named agent (e.g., 'עוזר משפטי')")
|
|
|
|
|
args = p.parse_args()
|
|
|
|
|
|
|
|
|
|
@@ -295,6 +364,11 @@ async def main() -> None:
|
|
|
|
|
finally:
|
|
|
|
|
await conn.close()
|
|
|
|
|
|
|
|
|
|
if args.check_instructions:
|
|
|
|
|
all_agents = master_agents + mirror_agents
|
|
|
|
|
all_ok = await check_instructions(all_agents)
|
|
|
|
|
sys.exit(0 if all_ok else 1)
|
|
|
|
|
|
|
|
|
|
mirror_by_name = {a["name"]: a for a in mirror_agents}
|
|
|
|
|
|
|
|
|
|
print(f"\n=== Master (CMP, 1xxx): {len(master_agents)} agents ===")
|
|
|
|
|
@@ -332,6 +406,14 @@ async def main() -> None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# APPLY
|
|
|
|
|
# Pre-flight: abort if any master agent is missing its instructions file
|
|
|
|
|
print("🔍 Pre-flight: checking instruction files...")
|
|
|
|
|
all_ok = await check_instructions(master_agents)
|
|
|
|
|
if not all_ok:
|
|
|
|
|
print("❌ Abort: one or more instruction files are missing. Fix before --apply.")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
print("✅ Pre-flight passed.\n")
|
|
|
|
|
|
|
|
|
|
print(f"\n=== Backup ===")
|
|
|
|
|
backup_path = backup_agents_table()
|
|
|
|
|
print(f" ✓ {backup_path}")
|
|
|
|
|
@@ -340,6 +422,11 @@ async def main() -> None:
|
|
|
|
|
all_errors: list[str] = []
|
|
|
|
|
for master, mirror, diff in plan:
|
|
|
|
|
print(f"\n → {master['name']} ({mirror['id']})")
|
|
|
|
|
# Inject mtime into diff so apply_diff can stamp metadata
|
|
|
|
|
master_ac = master.get("adapter_config") or {}
|
|
|
|
|
mtime = get_claude_md_mtime(master_ac)
|
|
|
|
|
if mtime:
|
|
|
|
|
diff["_claude_md_mtime"] = mtime
|
|
|
|
|
errors = await apply_diff(mirror["id"], master["name"], diff)
|
|
|
|
|
if errors:
|
|
|
|
|
for e in errors:
|
|
|
|
|
|