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:
|
if "runtime_config" in diff:
|
||||||
patch_body["runtimeConfig"] = diff["runtime_config"]["to"]
|
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:
|
if patch_body:
|
||||||
status, data = await call_patch(mirror_id, patch_body)
|
status, data = await call_patch(mirror_id, patch_body)
|
||||||
if status >= 400:
|
if status >= 400:
|
||||||
@@ -278,12 +286,65 @@ async def apply_diff(mirror_id: str, agent_name: str, diff: dict) -> list[str]:
|
|||||||
return errors
|
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:
|
async def main() -> None:
|
||||||
p = argparse.ArgumentParser()
|
p = argparse.ArgumentParser()
|
||||||
g = p.add_mutually_exclusive_group(required=True)
|
g = p.add_mutually_exclusive_group(required=True)
|
||||||
g.add_argument("--verify", action="store_true", help="Show current drift, no changes")
|
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("--dry-run", action="store_true", help="Show what would change")
|
||||||
g.add_argument("--apply", action="store_true", help="Backup + apply changes")
|
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., 'עוזר משפטי')")
|
p.add_argument("--only", help="Sync only the named agent (e.g., 'עוזר משפטי')")
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
|
||||||
@@ -295,6 +356,11 @@ async def main() -> None:
|
|||||||
finally:
|
finally:
|
||||||
await conn.close()
|
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}
|
mirror_by_name = {a["name"]: a for a in mirror_agents}
|
||||||
|
|
||||||
print(f"\n=== Master (CMP, 1xxx): {len(master_agents)} agents ===")
|
print(f"\n=== Master (CMP, 1xxx): {len(master_agents)} agents ===")
|
||||||
@@ -332,6 +398,14 @@ async def main() -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# APPLY
|
# 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 ===")
|
print(f"\n=== Backup ===")
|
||||||
backup_path = backup_agents_table()
|
backup_path = backup_agents_table()
|
||||||
print(f" ✓ {backup_path}")
|
print(f" ✓ {backup_path}")
|
||||||
@@ -340,6 +414,11 @@ async def main() -> None:
|
|||||||
all_errors: list[str] = []
|
all_errors: list[str] = []
|
||||||
for master, mirror, diff in plan:
|
for master, mirror, diff in plan:
|
||||||
print(f"\n → {master['name']} ({mirror['id']})")
|
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)
|
errors = await apply_diff(mirror["id"], master["name"], diff)
|
||||||
if errors:
|
if errors:
|
||||||
for e in errors:
|
for e in errors:
|
||||||
|
|||||||
Reference in New Issue
Block a user