feat: add --check-instructions, pre-flight validation, and mtime tracking to sync script
- P3-T1: --check-instructions flag + check_instructions() prints a table of all agents' instructionsFilePath with status (✅ OK / ❌ MISSING / ⚠ NOT SET), size, mtime, and ⚠ DRIFT when file has changed since last sync - P3-T2: --apply now runs a pre-flight check on master agents and aborts if any instruction file is missing, before touching the DB or calling any API - P3-T3: get_claude_md_mtime() helper; --apply stamps claude_md_mtime and claude_md_last_synced into each mirror agent's metadata via the PATCH call - P3-T4: alias check-agents added to ~/.bashrc Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,65 @@ 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:
|
||||
adapter_cfg = agent.get("adapter_config") or {}
|
||||
if isinstance(adapter_cfg, str):
|
||||
adapter_cfg = json.loads(adapter_cfg)
|
||||
|
||||
file_path = adapter_cfg.get("instructionsFilePath", "")
|
||||
name = (agent.get("name") or agent.get("id") or "?")[:29]
|
||||
|
||||
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
|
||||
metadata = agent.get("metadata") or {}
|
||||
if isinstance(metadata, str):
|
||||
metadata = json.loads(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 +356,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 +398,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 +414,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:
|
||||
|
||||
Reference in New Issue
Block a user