feat(sync): --verify exits non-zero on drift; adapter mismatch = loud drift (GAP-21, FU-8a)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 11:14:44 +00:00
parent adc196ac20
commit aac383acb7
2 changed files with 90 additions and 18 deletions

View File

@@ -345,6 +345,33 @@ async def check_instructions(agents: list[dict]) -> bool:
return all_ok
def build_drift_report(master_agents, mirror_by_name, mirror_skills, only=None) -> dict:
"""Pure drift computation (no DB, no printing). Returns:
{"plan": [(master, mirror, diff), ...], "mismatches": [name, ...], "missing": [name, ...]}.
adapter_type mismatch and missing-in-mirror are recorded as drift, not skipped silently.
"""
plan, mismatches, missing = [], [], []
for m in master_agents:
if only and m["name"] != only:
continue
mirror = mirror_by_name.get(m["name"])
if not mirror:
missing.append(m["name"])
continue
if m["adapter_type"] != mirror["adapter_type"]:
mismatches.append(m["name"])
continue
diff = compute_diff(m, mirror, mirror_skills)
if diff:
plan.append((m, mirror, diff))
return {"plan": plan, "mismatches": mismatches, "missing": missing}
def _verify_exit_code(plan, mismatches, missing) -> int:
"""0 iff fully in sync; 1 if any drift (needs-sync / adapter mismatch / missing-in-mirror)."""
return 1 if (plan or mismatches or missing) else 0
async def main() -> None:
p = argparse.ArgumentParser()
g = p.add_mutually_exclusive_group(required=True)
@@ -376,26 +403,23 @@ async def main() -> None:
print(f"=== Mirror has {len(mirror_skills)} local skills available ===\n")
print(f"=== Drift report ===")
plan: list[tuple[dict, dict, dict]] = [] # (master, mirror, diff)
for m in master_agents:
if args.only and m["name"] != args.only:
continue
mirror = mirror_by_name.get(m["name"])
if not mirror:
print(f"{m['name']:14s} — NOT FOUND in mirror (skipping; we never auto-create)")
continue
if m["adapter_type"] != mirror["adapter_type"]:
print(f"{m['name']:14s} — adapter_type mismatch ({m['adapter_type']} vs {mirror['adapter_type']}) — SKIPPING")
continue
diff = compute_diff(m, mirror, mirror_skills)
print_diff(m["name"], diff, m["id"], mirror["id"])
if diff:
plan.append((m, mirror, diff))
report = build_drift_report(master_agents, mirror_by_name, mirror_skills, only=args.only)
plan = report["plan"]
for name in report["missing"]:
print(f"{name:14s} — NOT FOUND in mirror (we never auto-create) — DRIFT")
for name in report["mismatches"]:
m = next(a for a in master_agents if a["name"] == name)
mi = mirror_by_name[name]
print(f"{name:14s} — adapter_type mismatch ({m['adapter_type']} vs {mi['adapter_type']}) "
f"— DRIFT (apply skips it; fix manually in both companies)")
for master, mirror, diff in plan:
print_diff(master["name"], diff, master["id"], mirror["id"])
if args.verify:
print(f"\n(verify mode — exiting without changes)")
print(f"\nSummary: {len(plan)} agent(s) need sync, {len(master_agents) - len(plan)} in sync")
return
code = _verify_exit_code(plan, report["mismatches"], report["missing"])
print(f"\nSummary: {len(plan)} need sync, {len(report['mismatches'])} adapter-mismatch, "
f"{len(report['missing'])} missing-in-mirror → {'DRIFT' if code else 'IN SYNC'}")
sys.exit(code)
if not plan:
print(f"\n✓ All agents in sync — nothing to do.")