3 Commits

Author SHA1 Message Date
a3468d5b2f fix: use timezone-aware datetime in webhook timestamp
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 3m17s
Replace deprecated datetime.utcnow() with datetime.now(timezone.utc)
to avoid Python 3.12+ DeprecationWarning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 10:15:52 +00:00
5f43659b5a fix: add defensive JSON parsing in check_instructions 2026-05-16 17:53:42 +00:00
86734da210 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>
2026-05-16 17:51:34 +00:00
2 changed files with 89 additions and 2 deletions

View File

@@ -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,73 @@ 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:
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: 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 +364,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 +406,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 +422,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:

View File

@@ -19,7 +19,7 @@ from __future__ import annotations
import logging import logging
import os import os
from datetime import datetime from datetime import datetime, timezone
from typing import Any from typing import Any
import httpx import httpx
@@ -104,7 +104,7 @@ async def emit_case_status_webhook(
"oldStatus": old_status, "oldStatus": old_status,
"newStatus": new_status, "newStatus": new_status,
"companyId": company_id, "companyId": company_id,
"timestamp": datetime.utcnow().isoformat() + "Z", "timestamp": datetime.now(timezone.utc).isoformat(),
}, },
run_id=run_id, run_id=run_id,
timeout=5.0, timeout=5.0,