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:
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:

View File

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