From 1b14e04373035ea98e014e011816509d86821ac6 Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 4 May 2026 17:47:05 +0000 Subject: [PATCH] chore(skills): remove paperclip-dev, scope converting-plans-to-tasks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit paperclip-dev is for maintaining the Paperclip codebase itself — not relevant to legal work. Removed from all 14 agents (was on CMPA mirror). paperclip-converting-plans-to-tasks helps decompose a plan into assigned issues. Useful for the planning-heavy agents (CEO, analyst). Now scoped to those two — removed from the other 5 in CMPA where it had crept in. Net effect: zero drift on paperclipai/* skills across all 7 master+mirror pairs. Verified via the new Agents tab dashboard. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/SCRIPTS.md | 1 + scripts/fix_paperclipai_skills_drift.py | 134 ++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 scripts/fix_paperclipai_skills_drift.py diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index f7e4683..610696a 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -11,6 +11,7 @@ | `pc.sh` | bash | **wrapper לכל קריאות Paperclip API מסוכנים** — מוסיף Authorization, X-Paperclip-Run-Id (audit trail), Content-Type, base URL. תחביר: `pc.sh [BODY_JSON]`. אסור `curl` ישיר ל-`$PAPERCLIP_API_URL`. ראה `HEARTBEAT.md §0`. counterpart ב-Python: `web/paperclip_api.py`. | נקרא ע"י סוכנים | | `sync_missing_agent_skills.py` | python | סקריפט "אל-כשל" להוספת `paperclipSkillSync` ל-`הגהת מסמכים` ו-`מנתח משפטי` שפיספסו את ה-sync ההיסטורי (Gap #28). תומך `--verify`/`--dry-run`/`--apply`. גיבוי אוטומטי ל-`agents-pre-skill-sync-*.sql`. דורש `PAPERCLIP_BOARD_API_KEY` (Infisical /paperclip ב-nautilus env). idempotent. | חד-פעמי (בוצע 2026-05-04). שמור לרפרנס | | `sync_agents_across_companies.py` | python | **סנכרון סוכנים מ-CMP (1xxx, master) ל-CMPA (8xxx, mirror)** — Gap #25. משווה adapter_config (model/timeout/instructions/skills/etc), runtime_config (heartbeat), ושדות top-level (budget/metadata/icon/title/role). מסנן אוטומטית local skills שלא קיימים ב-mirror. לוגיקת subset (mirror יכול להחזיק יותר skills כי ה-API מוסיף required runtime skills). תומך `--verify`/`--dry-run`/`--apply [--only NAME]`. גיבוי אוטומטי. דורש `PAPERCLIP_BOARD_API_KEY`. **להריץ אחרי כל שינוי הגדרות ב-CMP.** | ידני אחרי כל שינוי | +| `fix_paperclipai_skills_drift.py` | python | סקריפט חד-פעמי (בוצע 2026-05-04) שניקה drift על `paperclipai/*` skills בין CMP ל-CMPA. הסיר `paperclip-dev` מכל 14 הסוכנים, ודאג ש-`paperclip-converting-plans-to-tasks` קיים רק על CEO ו-analyst. תומך `--apply` (ברירת מחדל: dry-run). דורש `PAPERCLIP_BOARD_API_KEY`. נשמר לרפרנס למקרה שhdrift חוזר. | חד-פעמי (בוצע) | | `auto-sync-cases.sh` | bash | סנכרון תיקי ערר ל-Gitea — רץ כל דקה | `* * * * *` (cron) | | `backup-db.sh` | bash | גיבוי PostgreSQL יומי ל-`data/backups/` (gzip) | לתזמן: `0 2 * * *` | | `restore-db.sh` | bash | שחזור DB מגיבוי (companion ל-backup-db.sh) | ידני | diff --git a/scripts/fix_paperclipai_skills_drift.py b/scripts/fix_paperclipai_skills_drift.py new file mode 100644 index 0000000..f67a512 --- /dev/null +++ b/scripts/fix_paperclipai_skills_drift.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python3 +"""Fix paperclipai/* skill drift across CMP+CMPA agents. + +Goal: zero drift on paperclipai/* skills between master(CMP) and mirror(CMPA). + +Rules: +* Remove ``paperclipai/paperclip/paperclip-dev`` from all 14 agents (not relevant + for legal work — it's for maintaining Paperclip itself). +* Ensure ``paperclipai/paperclip/paperclip-converting-plans-to-tasks`` exists + on CEO + analyst agents in both companies (planning skill). +* Remove ``paperclipai/paperclip/paperclip-converting-plans-to-tasks`` from any + other agent in either company that currently has it. + +Local/* and company/* skills are not touched — they're scoped to a company +by design and drift is expected. + +Usage:: + + PAPERCLIP_BOARD_API_KEY=pbk_... python scripts/fix_paperclipai_skills_drift.py # dry-run + PAPERCLIP_BOARD_API_KEY=pbk_... python scripts/fix_paperclipai_skills_drift.py --apply # commit +""" + +from __future__ import annotations + +import argparse +import asyncio +import os +import sys + +import httpx + +PAPERCLIP_API_URL = os.environ.get("PAPERCLIP_API_URL", "http://localhost:3100") +PAPERCLIP_BOARD_API_KEY = os.environ.get("PAPERCLIP_BOARD_API_KEY") + +COMPANIES = { + "licensing": ("CMP ", "42a7acd0-30c5-4cbd-ac97-7424f65df294"), + "betterment": ("CMPA", "8639e837-4c9d-47fa-a76b-95788d651896"), +} + +DEV_SKILL = "paperclipai/paperclip/paperclip-dev" +CONVERTING_SKILL = "paperclipai/paperclip/paperclip-converting-plans-to-tasks" + +# Hebrew names of the agents that should retain converting-plans-to-tasks. +CONVERTING_TARGETS = {"עוזר משפטי", "מנתח משפטי"} + + +def headers() -> dict[str, str]: + if not PAPERCLIP_BOARD_API_KEY: + sys.exit("PAPERCLIP_BOARD_API_KEY not set — fetch from Infisical first.") + return { + "Authorization": f"Bearer {PAPERCLIP_BOARD_API_KEY}", + "Content-Type": "application/json", + } + + +async def fetch_company_agents(client: httpx.AsyncClient, company_id: str) -> list[dict]: + r = await client.get(f"{PAPERCLIP_API_URL}/api/companies/{company_id}/agents", headers=headers()) + r.raise_for_status() + return r.json() + + +def compute_changes(agent: dict) -> tuple[bool, list[str], list[str]]: + skill_sync = (agent.get("adapterConfig") or {}).get("paperclipSkillSync") or {} + old = list(skill_sync.get("desiredSkills") or []) + new = [s for s in old if s != DEV_SKILL] + if agent["name"] in CONVERTING_TARGETS: + if CONVERTING_SKILL not in new: + new.append(CONVERTING_SKILL) + else: + new = [s for s in new if s != CONVERTING_SKILL] + return (sorted(old) != sorted(new), old, new) + + +async def patch_agent( + client: httpx.AsyncClient, agent_id: str, current_skill_sync: dict, new_skills: list[str] +) -> None: + body = { + "adapterConfig": { + "paperclipSkillSync": {**current_skill_sync, "desiredSkills": new_skills}, + } + } + r = await client.patch( + f"{PAPERCLIP_API_URL}/api/agents/{agent_id}", headers=headers(), json=body, timeout=15 + ) + r.raise_for_status() + + +async def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--apply", action="store_true", help="commit changes (default: dry-run)") + args = parser.parse_args() + + mode = "APPLY" if args.apply else "DRY-RUN" + print(f"=== {mode}: fixing paperclipai/* skill drift ===\n") + + async with httpx.AsyncClient(timeout=15) as client: + all_agents: list[dict] = [] + for label, (_, cid) in COMPANIES.items(): + agents = await fetch_company_agents(client, cid) + for a in agents: + a["_company_label"] = COMPANIES[label][0] + all_agents.extend(agents) + + changes_planned = 0 + for a in sorted(all_agents, key=lambda x: (x["_company_label"], x["name"])): + changed, old, new = compute_changes(a) + label = a["_company_label"] + if not changed: + print(f" {label} {a['name']:20} no change") + continue + changes_planned += 1 + removed = sorted(set(old) - set(new)) + added = sorted(set(new) - set(old)) + print(f" {label} {a['name']:20} -{len(removed)} +{len(added)}") + for s in removed: + print(f" - {s}") + for s in added: + print(f" + {s}") + if args.apply: + skill_sync = (a.get("adapterConfig") or {}).get("paperclipSkillSync") or {} + try: + await patch_agent(client, a["id"], skill_sync, new) + print(" ✓ patched") + except httpx.HTTPStatusError as e: + print(f" ✗ failed: {e.response.status_code} {e.response.text[:200]}") + raise + + print(f"\n{mode}: {changes_planned} agents would change") + if not args.apply and changes_planned > 0: + print("Run with --apply to commit.") + + +if __name__ == "__main__": + asyncio.run(main())