From cf5f6fe274a3a55cf84e32a0671291a841a4ac2b Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 4 May 2026 17:25:45 +0000 Subject: [PATCH] feat(paperclip): close 11 integration gaps (#16-#28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings the legal-ai ↔ Paperclip integration in line with the official Paperclip skill. Net effect: HEARTBEAT.md -47% (370→195 lines), all 14 agents on uniform runtime_config + budget + instructionsBundleMode, and two cross-company helpers replacing manual SQL. Highlights: - HEARTBEAT.md refactor: project-specific only, delegates to the official paperclipai/paperclip skill (loaded per agent). Adds heartbeat-context fast-path (§1.7) and PAPERCLIP_WAKE_PAYLOAD_JSON shortcut (§1.5). - Issue Thread Interactions API: legal-ceo.md now uses ask_user_questions / request_confirmation / suggest_tasks instead of free-text comments — gives chair structured UI with idempotency keys. - pc.sh + paperclip_api.pc_request: every API call goes through helpers that inject Authorization + X-Paperclip-Run-Id (audit trail). - sync_agents_across_companies.py: master(CMP)→mirror(CMPA) sync via Paperclip API, idempotent, with --verify and --apply modes. - skills/new-company-setup: 11-step blueprint distilling all 11 gaps into a single onboarding runbook for the next company. - .taskmaster: 12 tasks covering each gap (one already closed: #29). Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/agents/HEARTBEAT.md | 281 ++++++++--------- .claude/agents/legal-analyst.md | 121 ++++++-- .claude/agents/legal-ceo.md | 345 +++++++++++++-------- .claude/agents/legal-exporter.md | 18 +- .claude/agents/legal-proofreader.md | 18 +- .claude/agents/legal-qa.md | 42 ++- .claude/agents/legal-researcher.md | 99 ++++-- .claude/agents/legal-writer.md | 18 +- .taskmaster/state.json | 5 +- .taskmaster/tasks/tasks.json | 221 +++++++++++++- CLAUDE.md | 20 ++ scripts/SCRIPTS.md | 3 + scripts/pc.sh | 52 ++++ scripts/sync_agents_across_companies.py | 382 ++++++++++++++++++++++++ scripts/sync_missing_agent_skills.py | 191 ++++++++++++ skills/new-company-setup/SKILL.md | 316 ++++++++++++++++++++ 16 files changed, 1724 insertions(+), 408 deletions(-) create mode 100755 scripts/pc.sh create mode 100644 scripts/sync_agents_across_companies.py create mode 100644 scripts/sync_missing_agent_skills.py create mode 100644 skills/new-company-setup/SKILL.md diff --git a/.claude/agents/HEARTBEAT.md b/.claude/agents/HEARTBEAT.md index 4eefb26..476a999 100644 --- a/.claude/agents/HEARTBEAT.md +++ b/.claude/agents/HEARTBEAT.md @@ -1,197 +1,165 @@ -# HEARTBEAT.md — רשימת ביצוע לכל ריצה +# HEARTBEAT.md — רשימת ביצוע לכל ריצה (Project-Specific) -## שפה — כלל עליון - -**כל הפלט שלך חייב להיות בעברית בלבד.** זה כולל: -- Comments ב-Paperclip -- הודעות סטטוס -- תיאורי שגיאות -- סיכומים ודיווחים -- חשיבה פנימית (thinking) - -אין יוצאים מן הכלל. גם שמות tools, פקודות, ונתיבי קבצים — ההסבר סביבם בעברית. +> **🎯 קובץ זה — Project-specific only.** ה-skill הרשמי `paperclipai/paperclip/paperclip` (טעון אוטומטית בכל heartbeat דרך `paperclipSkillSync`) מכיל את כל ה-API patterns הגנריים: identity (`/api/agents/me`), `PAPERCLIP_WAKE_PAYLOAD_JSON`, `APPROVAL_ID`, inbox, comments, checkout, status updates, וכו'. **קובץ זה מתעד רק התאמות שלנו** — סינון חברה, helpers, workarounds, ו-quirks. +> +> **בקונפליקט:** קובץ זה גובר על ה-skill (project-specific מנצח default). --- -הרץ את הרשימה הזו בכל heartbeat. +## שפה — כלל עליון -## 1. זיהוי וסינון חברה +**כל הפלט שלך חייב להיות בעברית בלבד.** כולל: comments, סטטוס, שגיאות, סיכומים, ו-thinking פנימי. אין יוצאים מן הכלל. גם שמות tools, פקודות, ונתיבי קבצים — ההסבר סביבם בעברית. ה-skill הרשמי באנגלית — תרגם אם נדרש. -- וודא שאתה יודע מי אתה: `$PAPERCLIP_AGENT_ID` -- בדוק הקשר: `$PAPERCLIP_TASK_ID`, `$PAPERCLIP_WAKE_REASON` -- **זהה את החברה שלך**: `$PAPERCLIP_COMPANY_ID` +--- -### ⚠️ סינון תיקים לפי חברה — כלל ברזל +## §0. כל קריאה ל-Paperclip API — דרך `pc.sh` בלבד -**אתה אחראי רק על תיקים ששייכים לחברה שלך.** הספרה הראשונה של מספר התיק קובעת: - -| חברה | COMPANY_ID | סוגי תיקים | טווח מספרים | -|------|------------|-------------|-------------| -| ועדת ערר רישוי ובניה | `42a7acd0-30c5-4cbd-ac97-7424f65df294` | רישוי ובניה | **1xxx** | -| ועדת ערר היטלי השבחה | `8639e837-4c9d-47fa-a76b-95788d651896` | היטל השבחה + פיצויים ס' 197 | **8xxx, 9xxx** | - -- אם `$PAPERCLIP_COMPANY_ID` = `42a7acd0...` → עבוד רק על תיקים שמתחילים ב-**1** -- אם `$PAPERCLIP_COMPANY_ID` = `8639e837...` → עבוד רק על תיקים שמתחילים ב-**8** או **9** -- **לעולם אל תיצור פרויקט, issue, או תוכן לתיק שלא בטווח שלך** -- אם issue שהוקצה לך מכוון לתיק שלא בטווח שלך — סרב בנימוס ודווח ב-comment - -## 2. בדוק תיבת דואר +**ה-skill הרשמי משתמש ב-`curl` ישיר. אצלנו אסור.** משתמשים ב-helper שלנו: ```bash -curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" "$PAPERCLIP_API_URL/api/agents/me/inbox-lite" +~/legal-ai/scripts/pc.sh [BODY_JSON] [extra curl args...] ``` -- תעדוף: `in_progress` קודם, אחר כך `todo` -- אם `PAPERCLIP_TASK_ID` מוגדר — תעדף אותו +מוסיף אוטומטית: `Authorization`, `X-Paperclip-Run-Id` (audit), `Content-Type`, base URL. -## 2b. קרא תגובות אחרונות על ה-issue +**דוגמאות:** +```bash +~/legal-ai/scripts/pc.sh GET "/api/agents/me/inbox-lite" +~/legal-ai/scripts/pc.sh POST "/api/issues/$ISSUE_ID/checkout" +~/legal-ai/scripts/pc.sh PATCH "/api/issues/$ISSUE_ID" '{"status":"done"}' +``` -לפני שאתה מתחיל לעבוד, בדוק אם יש comments חדשים מחיים: +**ל-body גדול עם backticks** — `Write` ל-temp file, אז `pc.sh ... "" -H "Content-Type: application/json" -d @/tmp/comment.json`. ראה §דיווח למה. + +--- + +## §1. זיהוי וסינון חברה — כלל ברזל ⚠️ + +| חברה | COMPANY_ID | סוגי תיקים | טווח מספרים | CEO Agent ID | +|------|------------|-------------|---------------|---------------| +| ועדת ערר רישוי ובניה (CMP) | `42a7acd0-30c5-4cbd-ac97-7424f65df294` | רישוי ובניה | **1xxx** | `752cebdd-6748-4a04-aacd-c7ab0294ef33` | +| ועדת ערר היטלי השבחה (CMPA) | `8639e837-4c9d-47fa-a76b-95788d651896` | היטל השבחה + פיצויים ס' 197 | **8xxx, 9xxx** | `cdbfa8bc-3d61-41a4-a2e7-677ec7d34562` | + +- אם `$PAPERCLIP_COMPANY_ID` = `42a7acd0...` → רק תיקים ש-**1xxx** +- אם `$PAPERCLIP_COMPANY_ID` = `8639e837...` → רק תיקים ש-**8xxx/9xxx** +- **אסור** ליצור פרויקט/issue/תוכן לתיק שלא בטווח שלך +- אם issue שהוקצה לך מכוון לתיק שלא בטווח — סרב בנימוס ב-comment, והעֵר את ה-CEO של החברה הנכונה + +--- + +## §1.5. טיפול ב-wake (skill הרשמי + תוספות שלנו) + +ה-skill מסביר `PAPERCLIP_WAKE_PAYLOAD_JSON`, `APPROVAL_ID`, ו-`heartbeat-context` (Step 6). הוסף עליו: + +**1.5א. אם `$PAPERCLIP_WAKE_PAYLOAD_JSON` מכיל comment חדש מחיים** — התייחס אליו ב-comment הראשון שלך ("ראיתי שביקשת X — מבצע Y") **לפני** עבודה רחבה. זה מבטיח שחיים יודע שקלטת. + +**1.5ב. תמיד לקרוא `heartbeat-context`** — לא רק מה ש-skill ממליץ ("Prefer"). אצלנו ה-`attachments` המוחזרים חיוניים (חיים מעלה DOCX/PDF דרך comments). ראה §2. ```bash -curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}/comments" | jq '[.[] | select(.authorUserId != null)] | .[-3:]' +CONTEXT=$(~/legal-ai/scripts/pc.sh GET "/api/issues/$ISSUE_ID/heartbeat-context?wakeCommentId=$LATEST_COMMENT_ID") +ATTACHMENTS=$(echo "$CONTEXT" | jq '.attachments') ``` -- אם יש comment מחיים (authorUserId, לא authorAgentId) שנכתב **אחרי** ה-comment האחרון שלך — **קרא אותו בתשומת לב** -- אם ה-comment מכיל הוראות עבודה — **עקוב אחריהן** -- אם ה-comment מזכיר קובץ שהועלה — בדוק attachments (ראה 2c) -- אם ה-comment מבקש להעביר לסוכן אחר — **עצור**, פרסם comment שמאשר, והעֵר את ה-CEO +**1.5ג. APPROVAL_ID flow** — אם חיים ענה על interaction (ראה `legal-ceo.md` §B/§C/§D), קרא תשובה דרך: +```bash +~/legal-ai/scripts/pc.sh GET "/api/issues/$PAPERCLIP_TASK_ID/interactions/$PAPERCLIP_APPROVAL_ID" | jq '{status, kind, response}' +``` +**אסור** לפענח טקסט מ-comment חופשי כשיש APPROVAL_ID — זה הקלט הסטרוקטורלי. -## 2c. בדוק קבצים מצורפים +--- -אם comment מחיים מזכיר קובץ או טיוטה: +## §2. קבצים מצורפים — דרך `heartbeat-context`, **לא psql** + +ה-attachments זמינים ב-`$CONTEXT.attachments` (מ-§1.5ב): ```bash -PGPASSWORD="paperclip" psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c " -SELECT a.original_filename, a.content_type, a.object_key, a.byte_size -FROM issue_attachments ia -JOIN assets a ON a.id = ia.asset_id -WHERE ia.issue_id = '{issue-id}' -ORDER BY ia.created_at DESC LIMIT 5;" +echo "$CONTEXT" | jq '.attachments[] | {filename, contentPath, contentType, byteSize}' + +# נתיב מלא לקובץ: +CONTENT_PATH=$(echo "$CONTEXT" | jq -r '.attachments[0].contentPath') +FULL_PATH="/home/chaim/.paperclip/instances/default/data/storage/$CONTENT_PATH" ``` -- נתיב מלא לקובץ: `/home/chaim/.paperclip/instances/default/data/storage/{object_key}` -- קבצי DOCX — קרא אותם עם `Read` -- השתמש בתוכן הקובץ כקלט לעבודתך +קבצי DOCX/PDF — קרא עם `Read` tool ב-`$FULL_PATH`. -## 3. Checkout ועבודה +⚠️ **`psql` ישיר ל-`issue_attachments` — אסור.** ה-API הוא ה-source of truth (Gap #21). +--- + +## §3. self-recovery — `issue.released` bug + +⚠️ **Paperclip quirk ידוע**: לאחר ש-issue מסומן `done`, מנגנון `issue.released` עלול להחזיר אותו ל-`todo` תוך ~30s, וגורם ל-wakeup חוזר על משימה שכבר בוצעה (תועד ב-`docs/paperclip-quirks.md §1`). + +**לפני שמתחילים עבודה — בדוק שלא בוצעה כבר:** + +1. **תוצרים בדיסק**: `Glob` על תיקיות output הצפויות (`{case_dir}/documents/research/*.md` לחוקר, `analysis-and-research.md` למנתח, וכו') +2. **תוצרים ב-DB**: דרך MCP — `precedent_list`, `get_claims`, `extract_appraiser_facts` (status=completed) +3. **comments קודמים** — חפש "הושלם בהצלחה" מסוף-מצב + +**אם הכל קיים ותקין:** פרסם comment קצר ("אין שינוי — תוצרים קיימים מהריצה הקודמת"), `PATCH status=done`, צא נקי. **לא לעבוד פעמיים.** + +**אם משהו חסר/שונה:** עבוד רק על מה שחסר. + +--- + +## §4. דיווח — חובה! + +**כל heartbeat שמסיים משימה:** comment + status + wake CEO. הסעיף הזה מתעד רק workarounds שלנו לא ב-skill. + +### §4א. dual-comment workaround ל-`backtick trap` + +**ל-body קצר (<500 תווים, בלי backticks/קוד/נתיבים)** — pattern רגיל: ```bash -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}/checkout" +~/legal-ai/scripts/pc.sh POST "/api/issues/{issue-id}/comments" '{"body": "סיכום..."}' ``` -- עבוד על המשימה לפי ההוראות ב-AGENTS.md שלך -- השתמש בכלים המשפטיים (legal-ai MCP) +**ל-body ארוך עם markdown/backticks/נתיבים — חובה שתי פעולות נפרדות:** -### ⚠️ self-recovery — issue ב-`todo` עם תוצרים קיימים - -ל-Paperclip יש באג ידוע: לאחר ש-issue מתעדכן ל-`done`, מנגנון `issue.released` מחזיר אותו ל-`todo` תוך כ-30 שניות (תועד ב-`docs/paperclip-quirks.md §1`). זה גורם ל-wakeup חוזר של אותו סוכן על משימה שכבר בוצעה. - -**לפני שאתה מתחיל עבודה — בדוק שהמשימה לא בוצעה כבר**: - -1. **בדוק תוצרים בדיסק**: `Glob` על תיקיות ה-output הצפויות (`{case_dir}/documents/research/*.md` לחוקר, `analysis-and-research.md` למנתח, וכו') -2. **בדוק תוצרים ב-DB**: דרך MCP — `precedent_list`, `get_claims`, `extract_appraiser_facts` (status=completed) -3. **בדוק comments קודמים על ה-issue** — אם הסוכן הקודם פרסם "הושלם בהצלחה" מסוף-מצב - -**אם הכל קיים ותקין**: אל תבצע עבודה כפולה. במקום זאת: -- פרסם comment קצר: "אין שינוי — כל התוצרים קיימים מהריצה הקודמת (X פריטים ב-DB, קובץ Y בדיסק). סוגר את ה-issue." -- `PATCH /api/issues/{id}` → `done` -- צא נקי - -**אם משהו חסר/שונה**: עבוד על מה שחסר בלבד, לא על הכל מחדש. - -## 4. דיווח — חובה! - -**לפני שאתה מסיים, תמיד:** - -### 4א. פרסם comment על ה-issue - -**ל-body קצר (<500 תווים, בלי backticks/קוד/נתיבים):** -```bash -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}/comments" \ - -d '{"body": "סיכום העבודה..."}' -``` - -**ל-body ארוך / markdown עם נתיבים בbacktick / קוד — חובה שתי פעולות נפרדות:** - -1. כתוב את ה-JSON לקובץ זמני דרך **Write tool** (לא דרך bash heredoc): +1. כתוב את ה-JSON לקובץ זמני דרך **Write tool** (לא bash heredoc): ``` Write(file_path="/tmp/comment-{issue-id}.json", content=json.dumps({"body": markdown_body}, ensure_ascii=False)) ``` -2. אז `curl -d @file` שקורא את הקובץ ישירות — בלי shell expansion: +2. אז `pc.sh` עם `-d @file` שקורא את הקובץ ישירות: ```bash - curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}/comments" \ - -d @/tmp/comment-{issue-id}.json + ~/legal-ai/scripts/pc.sh POST "/api/issues/{issue-id}/comments" "" \ + -H "Content-Type: application/json" -d @/tmp/comment-{issue-id}.json ``` -**⚠️ למה לא bash heredoc / `python3 -c`:** backticks ב-markdown (`` `path/to/file` ``) ייפרשו על ידי bash כ-command substitution גם כשהם בתוך מחרוזת Python. תקבל שגיאת `Permission denied` מטעה (`bash` מנסה להריץ את הנתיב כפקודה). הפתרון של temp-file חוסם את כל ה-shell quoting traps. תועד ב-`docs/paperclip-quirks.md §2`. +⚠️ **למה לא bash heredoc / `python3 -c`:** backticks ב-markdown (`` `path/to/file` ``) ייפרשו על-ידי bash כ-command substitution גם בתוך מחרוזת Python. תקבל `Permission denied` מטעה. תועד ב-`docs/paperclip-quirks.md §2`. -### 4ב. קבע סטטוס — done או blocked +### §4ב. סטטוס: `done` או `blocked` — לא ביניים -**אם המשימה הושלמה בהצלחה** (כל המסמכים חולצו, כל הבדיקות עברו, אין חסימות): ```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "done"}' +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}' # הצליח +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "blocked"}' # נכשל / חסום ``` -**אם המשימה נכשלה או חסומה** (מסמך לא חולץ, timeout, חוסר מידע, שגיאה שלא ניתנת לפתרון): -```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "blocked"}' -``` -**אסור** לסיים issue כ-"done" אם יש כשל שלא טופל. "done" = הכל הושלם בהצלחה. אם משהו נכשל — "blocked". +**אסור** `done` עם כשל שלא טופל. אם משהו נכשל → `blocked` + comment עם פירוט. -### 4ג. העֵר את העוזר המשפטי (CEO) — חובה! -אחרי כל סיום משימה (done או blocked), **העֵר את העוזר המשפטי של החברה שלך** כדי שיבדוק תוצאות ויחליט על הצעד הבא: +### §4ג. wake CEO לפי חברה -**⚠️ בחר CEO לפי חברה:** -| חברה | COMPANY_ID | CEO Agent ID | -|------|------------|-------------| -| רישוי ובניה (CMP) | `42a7acd0-...` | `752cebdd-6748-4a04-aacd-c7ab0294ef33` | -| היטלי השבחה (CMPA) | `8639e837-...` | `cdbfa8bc-3d61-41a4-a2e7-677ec7d34562` | +**⚠️ CEO שונה לכל חברה** (ראה §1). UUID hardcoded **אסור** — תמיד דרך `$PAPERCLIP_COMPANY_ID`: ```bash -# קבע CEO_ID לפי חברה: if [ "$PAPERCLIP_COMPANY_ID" = "8639e837-4c9d-47fa-a76b-95788d651896" ]; then - CEO_ID="cdbfa8bc-3d61-41a4-a2e7-677ec7d34562" + CEO_ID="cdbfa8bc-3d61-41a4-a2e7-677ec7d34562" # CMPA else - CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" + CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP fi -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/agents/$CEO_ID/wakeup" \ - -d '{"source":"automation","triggerDetail":"system","reason":"סוכן [שמך] סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}' +~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" \ + '{"source":"automation","triggerDetail":"system","reason":"סוכן [שם] סיים [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}' ``` -**⚠️ כללי ברזל — Paperclip API:** -1. **אסור** `INSERT INTO agent_wakeup_requests` — לא יוצר heartbeat_run, הסוכן לא יתעורר לעולם -2. **חובה** `payload.issueId` בכל wakeup — בלי זה הסוכן מתעורר בלי הקשר (בלי תיק, בלי cwd) -3. **agent JWT לא יכול להעיר סוכנים אחרים** — רק את עצמו. כדי להעיר סוכן אחר → צור issue + הקצה אליו (Paperclip מפעיל wakeup אוטומטי) +⚠️ **חובה `payload.issueId`** — בלי זה הסוכן מתעורר בלי הקשר (בלי תיק, בלי cwd). +⚠️ **wakeup לחברה אחרת נדחה** — `Agent key cannot access another company`. +⚠️ **אסור** `INSERT INTO agent_wakeup_requests` ישיר — לא יוצר heartbeat_run, הסוכן לא מתעורר. -**נתיבי API:** -| פעולה | נתיב | -|-------|-------| -| פרסום comment | `POST /api/issues/{issue-id}/comments` | -| יצירת issue | `POST /api/companies/{company-id}/issues` | -| עדכון issue | `PATCH /api/issues/{issue-id}` | -| wakeup עצמי/CEO | `POST /api/agents/{agent-id}/wakeup` (עם payload!) | +--- -## 5. התראת מייל — כשנדרשת תשובה אנושית - -**כשהתוצאה דורשת החלטה או תשובה של חיים**, שלח מייל: +## §5. התראת מייל — כשנדרשת תשובה אנושית ```bash python3 /home/chaim/legal-ai/scripts/notify.py \ @@ -199,22 +167,29 @@ python3 /home/chaim/legal-ai/scripts/notify.py \ "תוכן ההודעה עם סיכום מה נדרש" ``` -**מתי לשלוח — תמיד:** -- **סיום כל משימה** — עם סיכום קצר של מה בוצע -- בקשה לקביעת תוצאה (דחייה/קבלה/חלקית) -- בקשה לאישור כיוון נימוק -- דוח QA שנכשל (צריך החלטה על תיקונים) -- החלטה מוכנה לביקורת דפנה -- כל מצב שדורש פעולה אנושית ולא יכול להתקדם לבד -- שגיאה שלא ניתן לפתור ללא התערבות +**מתי לשלוח (תמיד):** סיום כל משימה (סיכום קצר), בקשת תוצאה/כיוון, QA fail, החלטה מוכנה לדפנה, מצב שדורש פעולה אנושית, שגיאה לא פתירה. -**מתי לא לשלוח:** -- עדכוני סטטוס ביניים (רק בסיום) -- שגיאות טכניות שאפשר לפתור לבד +**מתי לא:** עדכוני סטטוס ביניים, שגיאות טכניות שאפשר לפתור לבד. -## 6. Release +--- + +## §6. Release ```bash -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}/release" +~/legal-ai/scripts/pc.sh POST "/api/issues/{issue-id}/release" ``` + +--- + +## נתיבי API — הפניה ל-skill הרשמי + +| פעולה | איפה ב-skill | +|--------|---------------| +| Identity, inbox, pick work | Step 1, 3, 4 | +| Wake payload + APPROVAL handling | Authentication + Step 2 | +| Heartbeat-context, comments, attachments | Step 6 | +| Checkout (with the `checkedOutByHarness` skip) | Step 5 | +| Comment, status update, exit | Step 7-8 | +| Routines, workflows, references | `references/` ב-skill | + +**שינויים project-specific מה-skill:** תועדו בקובץ זה (§0 pc.sh, §1 חברה, §2 attachments, §3 quirk, §4 dual-comment + CEO wakeup, §5 notify). diff --git a/.claude/agents/legal-analyst.md b/.claude/agents/legal-analyst.md index 90df935..cfc66d2 100644 --- a/.claude/agents/legal-analyst.md +++ b/.claude/agents/legal-analyst.md @@ -170,11 +170,75 @@ tools: - **לא להמציא פסיקה** — אם יש אזכור במסמכי התיק, ניתן להתייחס. אם לא — נסח ללא הפניה - שימוש במונחים מקובלים בפסיקה הישראלית (מתאים לחיפוש ב-nevo/law-mate) -## שלב 5: חיפוש פנימי בקורפוס -חפש תקדימים רלוונטיים בקורפוס הפנימי: -- `search_decisions` — בהחלטות קודמות של דפנה -- `find_similar_cases` — תיקים דומים -הוסף תוצאות רלוונטיות תחת כל סוגיה כ-"תקדימים מהקורפוס הפנימי". +## שלב 5: חיפוש בשלושת הקורפוסים — חובה, עם תיעוד queries + +**חובה לבצע** — לא הצעה. בלי השלב הזה הניתוח חסר תקדימי-עליון רלוונטיים, וה-writer לא יוכל לכתוב CREAC מלא. נבחן ב-QA. + +### 5א. חיפוש בקורפוס הסמכותי (`search_precedent_library`) — חובה + +לכל **טענת סף** ולכל **סוגיה מרכזית** שזיהית — הרץ לפחות שאילתה אחת ל-`search_precedent_library` עם פילטרים: + +| סיווג תיק | practice_area | +|------------|---------------| +| 1xxx (רישוי ובניה) | `rishuy_uvniya` | +| 8xxx (היטל השבחה) | `histael_hashbacha` | +| 9xxx (פיצויים ס' 197) | `pitsuim_197` | + +אם הסוגיה מאוזכרת ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "חריגות בנייה", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר. צמצום מוקדם > הרחבה מאוחרת. + +דוגמה: +``` +search_precedent_library( + query="שימוש חורג מסחרי בייעוד נופש", + practice_area="rishuy_uvniya", + appeal_subtype="שימוש חורג", + limit=10 +) +``` + +### 5ב. חיפוש בקאנון של דפנה (`search_decisions`) + +לכל סוגיה — הרץ `search_decisions` כדי למצוא החלטות קודמות של דפנה באותה קטגוריה. אם דפנה כבר הכריעה בסוגיה דומה — תקדם אישי הוא חלק חובה מההנמקה (חיסכון או הבחנה). + +### 5ג. תיקים דומים (`find_similar_cases`) + +לכל סוגיה מרכזית — הרץ `find_similar_cases` לזיהוי דפוסים מבניים דומים בארכיון. + +### 5ד. תיעוד מחייב — סעיף "שאילתות לקורפוסים" ב-`analysis-and-research.md` + +ב-artifact הסופי, חובה להופיע סעיף חדש בשם **"7א. שאילתות לקורפוסים — log מלא"**, עם הפורמט הבא: + +```markdown +## 7א. שאילתות לקורפוסים — log מלא + +### קורפוס סמכותי (search_precedent_library) + +#### Q1 — סוגיה: [שם הסוגיה] +- **שאילתה:** "..." +- **פילטרים:** practice_area=..., appeal_subtype=... +- **תוצאות:** N +- **נבחרו:** + - `[case_number]` — [למה רלוונטי, איזה headnote תומך] +- **נדחו:** + - `[case_number]` — [למה לא רלוונטי] +- **0 results?** ציין מפורש + נמק (אין מה למצוא, או הפילטר צר מדי) + +#### Q2 — ... + +### קאנון דפנה (search_decisions) + +#### Q1 — סוגיה: [שם] +- **שאילתה:** "..." +- **תוצאות:** N +- **תקדים אישי שזוהה:** [שם תיק] — חיסכון/הבחנה? + +### תיקים דומים (find_similar_cases) +- ... +``` + +**negative evidence חובה:** גם כששאילתה החזירה 0 תוצאות, חובה לתעד אותה. זה ההבדל בין "הקורפוס נסרק וריק" ל"הקורפוס לא נסרק". ה-QA יחזיר `needs_revision` אם הסעיף חסר או חסר queries. + +**מינימום:** מספר queries ב-Q1+Q2+Q3 לקורפוס הסמכותי = מספר טענות סף + מספר סוגיות מרכזיות. אם זיהית 5 סוגיות + 2 טענות סף → לפחות 7 queries. ## שלב 6: בדיקת שלמות — לפני שמסיימים! @@ -224,19 +288,11 @@ FROM documents d WHERE d.case_id = '{case_id}' AND d.doc_type IN ('appeal', 'res **אם הכל עבר בהצלחה (בדיקות שלב 6 + טענות + עובדות שמאי):** ```bash - curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "done"}' - ``` + ~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'``` **אם בדיקות שלב 6 נכשלו או חילוץ נכשל:** ```bash - curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "blocked"}' - ``` + ~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "blocked"}'``` **אסור** לסיים `done` עם פלט חסר — אם ניסיון חוזר נכשל, סטטוס = `blocked` + comment עם פירוט. 5. **שלח מייל**: @@ -255,11 +311,7 @@ else CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה fi -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/agents/$CEO_ID/wakeup" \ - -d '{"source":"automation","triggerDetail":"system","reason":"מנתח משפטי סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}' -``` +~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" '{"source":"automation","triggerDetail":"system","reason":"מנתח משפטי סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}'``` **⚠️ אסור להשתמש ב-INSERT INTO agent_wakeup_requests ישירות!** הכנסה ישירה ל-DB יוצרת רק את הבקשה בלי heartbeat_run — והסוכן לא יתעורר לעולם. **תמיד להשתמש ב-API בלבד.** **⚠️ אסור לקבע UUID של CEO** — UUID שונה לכל חברה. תמיד דרך `$PAPERCLIP_COMPANY_ID`. wakeup לחברה אחרת נדחה: `Agent key cannot access another company`. @@ -337,8 +389,12 @@ X שאלות עומדות להכרעה: - סעיף X לחוק... (הערה: התחל מלשון הטקסט הנורמטיבי. תקדים נדרש רק כשהטקסט עמום.) -**תקדימים מהקורפוס הפנימי:** -- [אם נמצאו] +**תקדימים מהקורפוס הסמכותי (search_precedent_library):** +- [תקדים שנבחר עם citation, headnote, רלוונטיות] +- (חובה לפחות שאילתה אחת ב-Q1 בסעיף 7א — גם אם 0 תוצאות, יש לתעד שם) + +**תקדימים מהקאנון של דפנה (search_decisions):** +- [אם נמצאו — חיסכון או הבחנה?] **עמדת ועדת הערר:** [ימולא ע"י יו"ר הוועדה — עמדה/הנחיה לגבי סוגיה זו שתשמש את סוכן הכתיבה] @@ -362,6 +418,9 @@ X שאלות עומדות להכרעה: - **סדר דיון מומלץ**: הסדר המומלץ לדיון בסוגיות בהחלטה - **תלויות**: סוגיות שהכרעתן תלויה בהכרעה בסוגיה אחרת - **הערכה כללית**: לאן נוטה הניתוח ומהם הסיכויים הכלליים של הערר + +## 7א. שאילתות לקורפוסים — log מלא +[סעיף חובה לפי שלב 5ד — log כל קריאה ל-search_precedent_library, search_decisions, find_similar_cases. גם 0 results.] ``` ## שלב 8: העמקת ניתוח (pass 2) — אחרי אישור כיוון @@ -373,10 +432,14 @@ X שאלות עומדות להכרעה: ### 8א. אימות פסיקה סרוק את עמדות היו"ר וזהה כל אזכור פסיקה (בג"ץ, עע"מ, עת"מ, ע"א, ערר וכו'). לכל פסק דין שמוזכר: -1. חפש בקורפוס הפנימי (`search_decisions`, `find_similar_cases`) -2. חפש במסמכי התיק (`search_case_documents`) — אולי מצוטט בכתבי הטענות -3. **אם נמצא** — חלץ ציטוט מדויק, הקשר, רלוונטיות -4. **אם לא נמצא** — סמן: "דורש אימות חיצוני" + נסח הנחיות חיפוש +1. חפש ב**קורפוס הסמכותי** (`search_precedent_library`) — חובה ראשונה. שם נמצאות הלכות מאושרות עם supporting_quote מוכן לציטוט. +2. חפש בקאנון דפנה (`search_decisions`, `find_similar_cases`) +3. חפש במסמכי התיק (`search_case_documents`) — אולי מצוטט בכתבי הטענות +4. **אם נמצא ב-precedent_library** — צטט citation+supporting_quote מדויקים מהקורפוס. +5. **אם נמצא רק במסמכי התיק** — סמן: "מקור: כתבי טענות, דורש אימות מול הקורפוס". +6. **אם לא נמצא בכלל** — סמן: "דורש אימות חיצוני" + נסח הנחיות חיפוש. + +הוסף לסעיף "7א. שאילתות לקורפוסים" כל query נוסף שהורצה ב-pass 2. הוסף לכל סוגיה תת-סעיף: @@ -419,11 +482,7 @@ X שאלות עומדות להכרעה: CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה fi - curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/agents/$CEO_ID/wakeup" \ - -d '{"source":"automation","triggerDetail":"system","reason":"מנתח משפטי סיים העמקת ניתוח (pass 2) [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}' - ``` + ~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" '{"source":"automation","triggerDetail":"system","reason":"מנתח משפטי סיים העמקת ניתוח (pass 2) [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}'``` **⚠️ אם ה-API מחזיר שגיאה — אל תיגע ב-DB.** `INSERT INTO agent_wakeup_requests` לא יוצר `heartbeat_run` והסוכן לא יתעורר לעולם. בדוק `$PAPERCLIP_COMPANY_ID` ו-`$PAPERCLIP_API_KEY`, ודאי שאתה לא קורא ל-CEO של חברה אחרת (`Agent key cannot access another company`). ## כללים קריטיים diff --git a/.claude/agents/legal-ceo.md b/.claude/agents/legal-ceo.md index 4c5d796..3061bf5 100644 --- a/.claude/agents/legal-ceo.md +++ b/.claude/agents/legal-ceo.md @@ -36,6 +36,9 @@ tools: - mcp__legal-ai__precedent_library_list - mcp__legal-ai__halacha_review - mcp__legal-ai__halachot_pending + - mcp__legal-ai__extract_appraiser_facts + - mcp__legal-ai__write_interim_draft + - mcp__legal-ai__export_interim_draft --- # עוזר משפטי — מנהל תהליך כתיבת החלטות @@ -92,10 +95,7 @@ tools: ```bash # שלב 1: יצירת issue -ISSUE_ID=$(curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/issues" \ - -d '{"title": "[ערר CASE_NUMBER] ....", "description": "...", "parentId": "'$PAPERCLIP_TASK_ID'", "assigneeAgentId": "..."}' \ +ISSUE_ID=$(~/legal-ai/scripts/pc.sh POST "/api/companies/$PAPERCLIP_COMPANY_ID/issues" '{"title": "[ערר CASE_NUMBER] ....", "description": "...", "parentId": "'$PAPERCLIP_TASK_ID'", "assigneeAgentId": "..."}' \ | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") # שלב 2 (חובה!): קישור ל-case number בעוזר המשפטי @@ -223,7 +223,9 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru **מתי:** כשיש טענות מחולצות + מחקר תקדימים, אבל אין תוצאה עדיין -פרסם comment ב-Paperclip: +**שיטה — dual dispatch:** קודם פרסם comment עם הסיכום המלא (לתיעוד), ואז צור interaction עם כפתורים (לחיים). + +#### B.1 פרסם comment עם הסיכום ``` ## סיכום תיק {case_number} — מוכן להחלטה @@ -259,135 +261,151 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru - כלל: ... - עובדות: ... - שאלה: ... - ---- - -**מה התוצאה הצפויה?** -1. 🔴 **דחייה** — הערר נדחה -2. 🟡 **קבלה חלקית** — מתקבל עם תנאים -3. 🟢 **קבלה מלאה** — הערר מתקבל - -@chaim — הגב עם מספר (1/2/3) + הערות אם יש ``` -**אחרי פרסום ה-comment:** עדכן את ה-issue הראשי ל-`status=in_review` (ראה "כלל קריטי: ניהול סטטוס issue" בראש הסעיף). +#### B.2 צור interaction לבחירת תוצאה + טיפול בטענות -לאחר שחיים בחר תוצאה, שאל אותו לסמן טיפול בכל טענה: - -``` -## טיפול בטענות — {case_number} - -סמן לכל טענה את סוג הטיפול: - -| # | טענה | טיפול | -|---|------|-------| -| 1 | {טענה 1} | דיון מלא / קיבוץ / דילוג | -| 2 | {טענה 2} | דיון מלא / קיבוץ / דילוג | -| 3 | {טענה 3} | דיון מלא / קיבוץ / דילוג | -| ... | ... | ... | - -**הסבר:** -- **דיון מלא** — ניתוח סילוגיסטי מלא (כלל → עובדות → מסקנה) -- **קיבוץ** — טענות שמכוונות לאותה נקודה ייאגדו יחד -- **דילוג** — "לא מצאנו ממש" או "אין צורך להכריע נוכח מסקנתנו" - -@chaim — סמן בטבלה והחזר +```bash +~/legal-ai/scripts/pc.sh POST "/api/issues/$PAPERCLIP_TASK_ID/interactions" '{ + "kind": "ask_user_questions", + "idempotencyKey": "outcome:'"$PAPERCLIP_TASK_ID"':v1", + "title": "תוצאה וטיפול בטענות — {case_number}", + "summary": "ראה את הסיכום ב-comment לעיל. שתי שאלות מובנות.", + "continuationPolicy": "wake_assignee", + "payload": { + "version": 1, + "submitLabel": "המשך לכיוונים", + "questions": [ + { + "id": "outcome", + "prompt": "מה התוצאה?", + "selectionMode": "single", + "required": true, + "options": [ + {"id":"reject", "label":"דחייה", "description":"הערר נדחה"}, + {"id":"partial","label":"קבלה חלקית","description":"מתקבל עם תנאים"}, + {"id":"accept", "label":"קבלה מלאה","description":"הערר מתקבל"} + ] + }, + { + "id": "claims_treatment", + "prompt": "אילו טענות לדון בנפרד? (multi)", + "selectionMode": "multi", + "helpText": "סמן רק טענות שצריכות דיון מלא. השאר → קיבוץ או דילוג.", + "options": [ + {"id":"claim_1","label":"{טענה 1 מקוצר}"}, + {"id":"claim_2","label":"{טענה 2 מקוצר}"}, + {"id":"claim_3","label":"{טענה 3 מקוצר}"} + ] + } + ] + } +}' ``` -**אחרי פרסום ה-comment:** עדכן את ה-issue הראשי ל-`status=in_review`. +**אחרי יצירת ה-interaction:** עדכן את ה-issue הראשי ל-`status=in_review` (ראה "כלל קריטי: ניהול סטטוס issue" בראש הסעיף). חיים יקבל UI עם dropdowns וכפתורי radio במקום להקליד מספרים. + +⚠️ **`idempotencyKey`** — חובה. אם תתעורר פעמיים, Paperclip לא יוצר 2 interactions זהים. **מתי לחזור אחורה:** אם הסיכום לא מצליח לנסח שאלות כסילוגיזמים מכווצים — ייתכן שחסר מידע עובדתי או נורמטיבי. חזור למנתח/חוקר להשלמה. ### שלב C: קליטת תוצאה וכיוונים סילוגיסטיים -**מתי:** חיים הגיב עם מספר תוצאה + טיפול בטענות +**מתי:** התעוררת עם `$PAPERCLIP_APPROVAL_ID` שמצביע על interaction מ-§B (תשובת תוצאה+טענות). 0. **החזר את ה-issue הראשי ל-`status=in_progress`** (קיבלת קלט והמשכת לעבוד). -1. קרא את ה-comment של חיים -2. זהה את הבחירה (1=rejected, 2=partial, 3=accepted) -3. הרץ `set_outcome(case_number, outcome, reasoning)` -4. **חשוב סילוגיסטית** על 2-3 כיוונים לנימוק — אתה כבר Claude, אתה יודע את הטענות והתקדימים. בנה כל כיוון כסילוגיזם מלא. +1. **קרא את תשובת חיים מה-API** (לא מ-comment חופשי): + ```bash + ~/legal-ai/scripts/pc.sh GET "/api/issues/$PAPERCLIP_TASK_ID/interactions/$PAPERCLIP_APPROVAL_ID" \ + | jq '{status, payload: .response}' + ``` + - תשובת `outcome`: `reject` / `partial` / `accept` (זהה ל-1/2/3 הישן) + - תשובת `claims_treatment`: array של claim IDs לדיון מלא +2. הרץ `set_outcome(case_number, outcome, reasoning)` +3. **חשוב סילוגיסטית** על 2-3 כיוונים לנימוק — אתה כבר Claude, אתה יודע את הטענות והתקדימים. בנה כל כיוון כסילוגיזם מלא. > **הערה טכנית:** אל תקרא ל-`brainstorm_directions` — זה מפעיל Claude בתוך Claude ולוקח יותר מדי זמן. -5. פרסם comment עם **סדר סוגיות מוצע**: +4. פרסם comment קצר עם **סדר סוגיות מוצע** (לתיעוד thread): ``` -## כיוונים אפשריים לנימוק — {outcome_hebrew} +## כיוונים לנימוק — {outcome_hebrew} ### סדר הסוגיות המוצע 1. {שאלת סף — אם רלוונטית} 2. {הסוגיה המכריעה} 3. {סוגיות נוספות לפי חוזק} ---- - -### כיוון 1: {title} - -**כלל (הנחה עליונה):** -{הוראת תכנית / סעיף חוק / הלכה פסוקה} - -**עובדות (הנחה תחתונה):** -{העובדות הספציפיות של הערר שנבחנות לאור הכלל} - -**מסקנה:** -{התוצאה שנובעת מהחלת הכלל על העובדות} - -**תקדימים תומכים:** {precedents} - ---- - -### כיוון 2: {title} - -**כלל (הנחה עליונה):** -{...} - -**עובדות (הנחה תחתונה):** -{...} - -**מסקנה:** -{...} - -**תקדימים תומכים:** {precedents} - ---- - -### כיוון 3: {title} - -**כלל (הנחה עליונה):** -{...} - -**עובדות (הנחה תחתונה):** -{...} - -**מסקנה:** -{...} - -**תקדימים תומכים:** {precedents} - ---- - -@chaim — איזה כיוון מועדף? (1/2/3) -אפשר גם לשלב כיוונים או להוסיף הערות. +(הכיוונים המלאים — בinteraction למטה) ``` -**אחרי פרסום ה-comment:** עדכן את ה-issue הראשי ל-`status=in_review`. +5. צור **interaction לבחירת כיוון** עם detailsMarkdown מלא: + +```bash +~/legal-ai/scripts/pc.sh POST "/api/issues/$PAPERCLIP_TASK_ID/interactions" '{ + "kind": "ask_user_questions", + "idempotencyKey": "direction:'"$PAPERCLIP_TASK_ID"':v1", + "title": "בחירת כיוון לנימוק — {case_number}", + "summary": "3 כיוונים סילוגיסטיים. בחר אחד או שלב.", + "continuationPolicy": "wake_assignee", + "payload": { + "version": 1, + "submitLabel": "אישור כיוון — להעברה לכותב", + "questions": [ + { + "id": "direction", + "prompt": "איזה כיוון מועדף?", + "selectionMode": "single", + "required": true, + "helpText": "ניתן לשלב כיוונים בהערות ב-comment נפרד אחרי הבחירה.", + "options": [ + { + "id": "direction_1", + "label": "כיוון 1: {title}", + "description": "כלל: {הוראת תכנית/סעיף חוק/הלכה}\nעובדות: {ספציפיות הערר}\nמסקנה: {התוצאה}\nתקדימים: {precedents}" + }, + { + "id": "direction_2", + "label": "כיוון 2: {title}", + "description": "כלל: {...}\nעובדות: {...}\nמסקנה: {...}\nתקדימים: {precedents}" + }, + { + "id": "direction_3", + "label": "כיוון 3: {title}", + "description": "כלל: {...}\nעובדות: {...}\nמסקנה: {...}\nתקדימים: {precedents}" + } + ] + } + ] + } +}' +``` + +⚠️ ה-`description` של כל option בעברית. ה-`label` קצר (3-4 מילים), ה-`description` הוא הסילוגיזם המלא — חיים רואה הכל בלי להקליד. + +**אחרי יצירת ה-interaction:** עדכן את ה-issue הראשי ל-`status=in_review`. **מתי לחזור אחורה:** אם לא ניתן לבנות סילוגיזם מלא (חסר כלל, חסרות עובדות, או המסקנה לא נובעת) — חזור לחוקר תקדימים או למנתח להשלמת החסר. ### שלב D: אישור כיוון והפעלת כתיבה -**מתי:** חיים הגיב עם בחירת כיוון +**מתי:** התעוררת עם `$PAPERCLIP_APPROVAL_ID` שמצביע על interaction מ-§C (תשובת כיוון). 0. **החזר את ה-issue הראשי ל-`status=in_progress`** (קיבלת קלט והמשכת לעבוד). -1. קרא את ה-comment של חיים -2. זהה כיוון (1/2/3) + הערות נוספות +1. **קרא את תשובת חיים מה-API:** + ```bash + ~/legal-ai/scripts/pc.sh GET "/api/issues/$PAPERCLIP_TASK_ID/interactions/$PAPERCLIP_APPROVAL_ID" \ + | jq '{status, response: .response}' + ``` + - `response.direction` יחזיר `direction_1` / `direction_2` / `direction_3` + - אם יש הערות נוספות — חיים יוסיף ב-comment נפרד; קרא את ה-comments האחרונים +2. זהה את הכיוון מהתשובה (1/2/3 → לפי המספר ב-id) 3. **אימות שלמות chair_directions** — לפני שליחה לכותב, ודא: - - [ ] טיפול בטענות (דיון מלא / קיבוץ / דילוג) מוגדר לכל טענה - - [ ] כיוון סילוגיסטי נבחר ומאושר + - [ ] טיפול בטענות (דיון מלא / קיבוץ / דילוג) מוגדר לכל טענה (מ-§B) + - [ ] כיוון סילוגיסטי נבחר ומאושר (מ-§C — interaction status=`answered`) - [ ] סדר סוגיות מוגדר - [ ] תקן ביקורת מצוין - - אם חסר פריט כלשהו — **שאל את חיים** לפני שממשיכים + - אם חסר פריט כלשהו — צור interaction חדש (`request_confirmation` או `ask_user_questions`) **לפני** שממשיכים. אסור לקרוא לחיים בcomment חופשי. 4. הרץ `approve_direction(case_number, direction_index, additional_notes)` 5. עדכן סטטוס: `case_update(status=direction_approved)` 6. צור issue חדש ב-Paperclip: @@ -396,7 +414,7 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru - תיאור: "כיוון אושר. בצע pass 2: אמת פסיקה מעמדות היו"ר, העמק עובדות לאור הכיוון שנבחר." 7. פרסם comment: "כיוון אושר. הועבר למנתח להעמקת ניתוח לפני כתיבה." -**מתי לחזור אחורה:** אם חיים שינה דעתו לגבי התוצאה או הכיוון, או אם חסר מידע — חזור לשלב B או C בהתאם. +**מתי לחזור אחורה:** אם חיים דחה את ה-interaction (`status=rejected`) או שינה דעתו לגבי התוצאה או הכיוון, או אם חסר מידע — חזור לשלב B או C בהתאם וצור interaction חדש עם `idempotencyKey` מעודכן (לדוגמה `:v2`). ### שלב D2: אחרי העמקת ניתוח (pass 2) @@ -474,6 +492,72 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru - השתמש ב-`revise_draft` בלבד במצב ג'. - אם המשתמש ביקש שינוי מאסיבי (שכתוב מלא של בלוק) — עדיף להציע לו לעבוד על זה בעריכה נוספת מצדו ולא לייצר revisions ארוכים. +### שלב H: טיוטת ביניים (לבקשת חיים, לפני דיון והכרעה) + +**מתי:** חיים מבקש בקומנט "טיוטת ביניים" / "interim draft" / "טיוטה לפני דיון" / "תכין לי את הטיוטה עם טענות הצדדים". בכל שלב לפני שיש תוצאה (בד"כ כשהתיק ב-`research_complete` או `analyst_verified`). + +**מטרה:** ייצור מסמך עבודה לחיים עם פתיחה ניטרלית, רקע, תכניות+היתרים, טענות הצדדים, והליכים — **בלי דיון והכרעה**. חיים יכתוב את בלוק י בעצמו ואז נמשיך לזרימה הרגילה (QA + ייצוא סופי). + +**זה side-quest, לא חלק מהזרימה B-F.** אל תשנה `cases.status`. אל תייצר issues לסוכני משנה. הכלים `write_interim_draft` ו-`export_interim_draft` עושים הכל בעצמם. + +**זרימה (~5-10 דקות):** + +1. פרסם comment קצר: "מתחיל יצירת טיוטת ביניים — אעדכן בסיום." עדכן את ה-issue הראשי ל-`status=in_progress`. + +2. **חילוץ עובדות שמאיות** (אם תיק 8xxx/9xxx ויש מסמכי שומה): + ``` + mcp__legal-ai__extract_appraiser_facts(case_number="...") + ``` + ⚠️ אם מחזיר `status="sides_missing"` → דווח לחיים שאין תיוג `appraiser_side` במסמכי השומה (`document_update` עם `appraiser_side` בערכים `committee`/`appellant`/`deciding`). עצור עד שיתוקן. + + אם הטבלה כבר מלאה — `write_interim_draft` ידלג על ההרצה אוטומטית, אז גם בלי הצעד הזה זה יעבוד. + +3. **כתיבת 5 הבלוקים:** + ``` + mcp__legal-ai__write_interim_draft( + case_number="...", + instructions="לבלוק ה (פתיחה): נוסח ניטרלי לחלוטין — 'לפנינו ערר על שומה מכרעת...' + הגדרות 'להלן' בלבד. אין לרמוז על תוצאת הדיון, אין מילות שיפוט, אין אזכור 'דין הערר להידחות/להתקבל'. רק זיהוי הצדדים, השומה המכרעת, המקרקעין והגורם המחליט." + ) + ``` + הכלי כותב ל-DB את בלוקים ה (פתיחה), ו (רקע), ט (תכניות+היתרים מורחב), ז (טענות), ח (הליכים). מחזיר `word_count` לכל בלוק. + +4. **ייצוא DOCX:** + ``` + mcp__legal-ai__export_interim_draft(case_number="...") + ``` + מייצר `data/cases/{case_number}/exports/טיוטת-ביניים-v{N}.docx`, מעדכן `active_draft_path`. + +5. **דווח לחיים** (כולל מייל דרך `scripts/notify.py`): + ``` + ## טיוטת ביניים מוכנה — ערר {case_number} + + 📄 **קובץ:** `data/cases/{case_number}/exports/טיוטת-ביניים-v{N}.docx` + + ### מה כלול + | בלוק | כותרת | מילים | + |------|-------|-------| + | ה | פתיחה (ניטרלית) | {N} | + | ו | רקע עובדתי | {N} | + | ט | תכניות + היתרים | {N} | + | ז | טענות הצדדים | {N} | + | ח | הליכים | {N} | + | **סה"כ** | | **{N}** | + + ### סתירות שמאיות שזוהו + {אם יש — רשימה קצרה: "תכנית X — שמאי A קבע ..., שמאי B קבע ...". אם אין — "לא זוהו סתירות בין שמאים."} + + ### מה הלאה + הטיוטה מוכנה לעבודה. כשתסיים לכתוב את בלוק י, חזור ב-comment ונמשיך + לשלב F (QA + ייצוא סופי). + ``` + +6. **סטטוס issue הראשי:** עדכן ל-`in_review` (ממתין לחיים שיכתוב את בלוק י). + +**אזהרות:** +- אל תייצא DOCX סופי (`export_docx`) — זה לא תחליף לטיוטת ביניים. +- אל תפעיל את שלב B (סיכום + שאלת תוצאה) במקביל — חיים מחליט מתי לעבור לזרימה הראשית. +- אם בלוק ח חסר (אין פרוטוקול דיון/סיור) — ציין זאת בדוח. הכלי כותב מה שיש, אבל המשתמש צריך לדעת אם חסר. + ## מפת סטטוסים **סטטוסים של התיק (`cases.status`) — כל סטטוס מתאים לפעולה אחת בדיוק:** @@ -592,22 +676,18 @@ case_prefix="${case_number:0:1}" 0. **החזר את ה-issue הראשי ל-`status=in_progress`** — אם ה-issue ב-`in_review` (כי המתנת לחיים) או ב-`blocked` (כי Paperclip חסם אוטומטית), הראשון דבר: עדכן ל-`in_progress` כדי לסמן שאתה עובד עליו. -1. **קרא את ה-comments האחרונים** על ה-issue שצוין ב-prompt: +1. **קרא את ההקשר המלא** — issue + ancestors + project + goal + comments + attachments בקריאה אחת (ראה `HEARTBEAT.md §1.7`): ```bash - curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}/comments" | jq '[.[] | select(.authorUserId != null)] | .[-3:]' + CONTEXT=$(~/legal-ai/scripts/pc.sh GET "/api/issues/$ISSUE_ID/heartbeat-context") ``` -2. **בדוק attachments** — אם חיים ציין קובץ שהועלה: +2. **בדוק attachments** — אם חיים ציין קובץ שהועלה, הוא כבר ב-`$CONTEXT.attachments`: ```bash - PGPASSWORD="paperclip" psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c " - SELECT a.original_filename, a.content_type, a.object_key - FROM issue_attachments ia - JOIN assets a ON a.id = ia.asset_id - WHERE ia.issue_id = '{issue-id}' - ORDER BY ia.created_at DESC LIMIT 5;" + echo "$CONTEXT" | jq '.attachments[] | {filename, contentPath, contentType, byteSize}' ``` - נתיב מלא לקובץ: `/home/chaim/.paperclip/instances/default/data/storage/{object_key}` + נתיב מלא לקובץ: `/home/chaim/.paperclip/instances/default/data/storage/$(echo $CONTEXT | jq -r '.attachments[0].contentPath')` + + ⚠️ **אסור** psql ישיר ל-`issue_attachments` — ה-API הוא ה-source of truth. 3. **אם יש טיוטה/קובץ — קרא אותו מילה במילה.** חפש בתוכו: - הוראות עריכה (טקסט כמו "צריך לערוך", "להוסיף", "חסר", "הוראות כתיבה") @@ -658,34 +738,35 @@ case_prefix="${case_number:0:1}" ## נתיבי API — חובה! ```bash -# קרא comments על issue -curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}/comments" | jq '.[-1].body' +# קרא comments על issue (אבל בד"כ עדיף heartbeat-context — ראה HEARTBEAT.md §1.7) +~/legal-ai/scripts/pc.sh GET "/api/issues/{issue-id}/comments" | jq '.[-1].body' # פרסם comment -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}/comments" \ - -d '{"body": "..."}' +~/legal-ai/scripts/pc.sh POST "/api/issues/{issue-id}/comments" '{"body": "..."}' # צור issue חדש (עם הקצאה לסוכן → מפעיל wakeup אוטומטי!) -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/companies/42a7acd0-30c5-4cbd-ac97-7424f65df294/issues" \ - -d '{"title":"...","projectId":"25c1b4a1-2c0e-4a2d-9938-8ae56ccda6f1","assigneeAgentId":"{agent-id}","description":"...","status":"todo"}' +~/legal-ai/scripts/pc.sh POST "/api/companies/42a7acd0-30c5-4cbd-ac97-7424f65df294/issues" \ + '{"title":"...","projectId":"25c1b4a1-2c0e-4a2d-9938-8ae56ccda6f1","assigneeAgentId":"{agent-id}","description":"...","status":"todo"}' # עדכן issue -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "done"}' +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}' + +# צור interaction מובנה לחיים (ראה §B/§C למעלה למבנה payload) +~/legal-ai/scripts/pc.sh POST "/api/issues/{issue-id}/interactions" '{"kind":"...","payload":{...}}' + +# קרא תשובת interaction (כשהתעוררת עם $PAPERCLIP_APPROVAL_ID) +~/legal-ai/scripts/pc.sh GET "/api/issues/{issue-id}/interactions/$PAPERCLIP_APPROVAL_ID" | jq '.' ``` **⚠️ agent JWT לא יכול להעיר סוכנים אחרים ישירות.** כדי להעיר סוכן → **צור issue חדש + הקצה אליו** (Paperclip מפעיל wakeup אוטומטי על assignment). -חפש ב-comment של חיים: -- מספר (1/2/3) → בחירה -- "כיוון" + מספר → אישור כיוון -- טבלת טיפול בטענות → סימון claim_handling -- שאלה → ענה -- הערה → שלב בתהליך +## מתי להשתמש בinteraction לעומת comment + +| מצב | פתרון | +|------|--------| +| נדרשת בחירה מובנית מחיים (תוצאה, כיוון, אישור) | **interaction** (`ask_user_questions` / `request_confirmation`) — UI עם כפתורים | +| הצעת עץ משימות לאישור | **interaction** (`suggest_tasks`) | +| עדכון סטטוס/תיעוד מסע (לא דורש פעולה) | **comment** רגיל | +| הסבר ארוך + שאלת בחירה | **dual** — comment עם הסבר + interaction עם options (ראה §B) | + +**אסור:** "@chaim — ענה 1/2/3 בcomment". זה anti-pattern. תמיד interaction עם options. diff --git a/.claude/agents/legal-exporter.md b/.claude/agents/legal-exporter.md index 9f8d3bb..df6e922 100644 --- a/.claude/agents/legal-exporter.md +++ b/.claude/agents/legal-exporter.md @@ -122,19 +122,11 @@ tools: **אם הכל עבר בהצלחה (כל בדיקות השלב הקודם עברו, אין כשל בפלט):** ```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "done"}' -``` +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'``` **אם בדיקות נכשלו, חסר פלט, או חסר מידע קריטי:** ```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "blocked"}' -``` +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "blocked"}'``` **אסור** לסיים `done` עם פלט חסר — אם משהו נכשל, סטטוס = `blocked` + comment עם פירוט. ### העֵר את העוזר המשפטי (CEO) — חובה! @@ -146,11 +138,7 @@ else CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה fi -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/agents/$CEO_ID/wakeup" \ - -d '{"source":"automation","triggerDetail":"system","reason":"מייצא טיוטה סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}' -``` +~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" '{"source":"automation","triggerDetail":"system","reason":"מייצא טיוטה סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}'``` **⚠️ אסור להשתמש ב-INSERT INTO agent_wakeup_requests ישירות!** הכנסה ישירה ל-DB יוצרת רק את הבקשה בלי heartbeat_run — והסוכן לא יתעורר לעולם. **תמיד להשתמש ב-API בלבד.** **⚠️ אסור לקבע UUID של CEO** — UUID שונה לכל חברה. תמיד דרך `$PAPERCLIP_COMPANY_ID`. wakeup לחברה אחרת נדחה: `Agent key cannot access another company`. diff --git a/.claude/agents/legal-proofreader.md b/.claude/agents/legal-proofreader.md index 1c4a263..6cd5df2 100644 --- a/.claude/agents/legal-proofreader.md +++ b/.claude/agents/legal-proofreader.md @@ -92,19 +92,11 @@ tools: **אם הכל עבר בהצלחה:** ```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "done"}' -``` +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'``` **אם נכשלו תיקונים קריטיים או יש markers `[?]` רבים:** ```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "blocked"}' -``` +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "blocked"}'``` **אסור** לסיים `done` עם פלט חסר — אם נכשל, סטטוס = `blocked` + comment עם פירוט. ### העֵר את העוזר המשפטי (CEO) — חובה! @@ -117,10 +109,6 @@ else CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה fi -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/agents/$CEO_ID/wakeup" \ - -d '{"source":"automation","triggerDetail":"system","reason":"מגיה סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}' -``` +~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" '{"source":"automation","triggerDetail":"system","reason":"מגיה סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}'``` **⚠️ אסור להשתמש ב-INSERT INTO agent_wakeup_requests ישירות!** הכנסה ישירה ל-DB יוצרת רק את הבקשה בלי heartbeat_run — והסוכן לא יתעורר לעולם. **תמיד להשתמש ב-API בלבד.** **⚠️ אסור לקבע UUID של CEO** — UUID שונה לכל חברה. תמיד דרך `$PAPERCLIP_COMPANY_ID`. wakeup לחברה אחרת נדחה: `Agent key cannot access another company`. diff --git a/.claude/agents/legal-qa.md b/.claude/agents/legal-qa.md index 809ff71..d1c4a75 100644 --- a/.claude/agents/legal-qa.md +++ b/.claude/agents/legal-qa.md @@ -79,6 +79,29 @@ tools: - סעיפים 1, 2, 3... ללא איפוס בין בלוקים - ללא כפילויות במספור +### 7א. שלמות חיפוש בקורפוסים (corpus_queries_logged) — critical + +ה-analyst וה-researcher חייבים לתעד queries לקורפוסים שלהם. בלי תיעוד — אין דרך לוודא שתקדימי עליון רלוונטיים לא הוחמצו. + +בדוק: +1. **קיום סעיף "שאילתות לקורפוסים"**: + - ב-`{case_dir}/documents/research/analysis-and-research.md` — סעיף **7א** (לפי שלב 5ד של ה-analyst) + - ב-`{case_dir}/documents/research/precedent-research.md` — סעיף **ז** (לפי שלב 2ב.4 של ה-researcher) + - אם חסר באחד מהם — `corpus_queries_logged = fail` (critical, חוסם המשך). + +2. **מספר queries מינימלי לקורפוס הסמכותי (`search_precedent_library`):** + - `analyst >= (מספר טענות סף + מספר סוגיות מרכזיות)` + - `researcher >= מספר סוגיות מרכזיות` + - חישוב: ספור את הסוגיות בסעיף 6 של `analysis-and-research.md`. מתחת לסף → `fail`. + +3. **negative evidence מתועד:** גם 0-result query חייבת להופיע. אם מצאת queries שכולן 0-result — לא fail; פשוט תיעוד שהקורפוס דליל בנושא. + +4. **אצליבה הצלבה (cross-check):** + - הרץ `mcp__legal-ai__precedent_library_list(practice_area=X, search="")` עם practice_area של התיק. + - אם החזיר תוצאות שלא מופיעות בסעיף "נבחרו" או "נדחו" של ה-analyst/researcher → `corpus_queries_logged = warning` (לא חוסם, אבל דווח לחיים). + +חומרה: **critical** — בלי queries מתועדות אין דרך לאמת שלא הוחמצה הלכה מחייבת. + ### 7. עמידה במתודולוגיה (methodology_compliance) ראה `docs/decision-methodology.md` לעקרונות המלאים. בדוק: - לכל סוגיה בבלוק י — ניתן לזהות מבנה סילוגיסטי: כלל + עובדות + מסקנה? @@ -137,6 +160,7 @@ tools: | משקלות | warning | מדווח, לא חוסם | | כפילות | warning | מדווח, לא חוסם | | מספור | warning | מדווח, לא חוסם | +| **שאילתות לקורפוסים** | **critical** | **חוסם ייצוא** | | מתודולוגיה | critical | חוסם ייצוא | | **קול דפנה** | **critical** | **חוסם ייצוא** | @@ -173,19 +197,11 @@ tools: **אם הכל עבר בהצלחה (כל בדיקות השלב הקודם עברו, אין כשל בפלט):** ```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "done"}' -``` +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'``` **אם בדיקות נכשלו, חסר פלט, או חסר מידע קריטי:** ```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "blocked"}' -``` +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "blocked"}'``` **אסור** לסיים `done` עם פלט חסר — אם משהו נכשל, סטטוס = `blocked` + comment עם פירוט. ### העֵר את העוזר המשפטי (CEO) — חובה! @@ -197,10 +213,6 @@ else CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה fi -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/agents/$CEO_ID/wakeup" \ - -d '{"source":"automation","triggerDetail":"system","reason":"בודק איכות סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}' -``` +~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" '{"source":"automation","triggerDetail":"system","reason":"בודק איכות סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}'``` **⚠️ אסור להשתמש ב-INSERT INTO agent_wakeup_requests ישירות!** הכנסה ישירה ל-DB יוצרת רק את הבקשה בלי heartbeat_run — והסוכן לא יתעורר לעולם. **תמיד להשתמש ב-API בלבד.** **⚠️ אסור לקבע UUID של CEO** — UUID שונה לכל חברה. תמיד דרך `$PAPERCLIP_COMPANY_ID`. wakeup לחברה אחרת נדחה: `Agent key cannot access another company`. diff --git a/.claude/agents/legal-researcher.md b/.claude/agents/legal-researcher.md index c35ec05..14a6318 100644 --- a/.claude/agents/legal-researcher.md +++ b/.claude/agents/legal-researcher.md @@ -85,22 +85,77 @@ tools: - **האם זה תקדם מהקאנון של דפנה?** (בדוק `docs/daphna-precedent-network.md` — אם כן, ציין שזה התקדם המועדף שלה לסוגיה) 4. הפק הפניות (`extract_references`) -### שלב 2ב: בדיקה מצטלבת מול הקאנון של דפנה -אחרי שאספת את הפסיקה הרלוונטית בתיק: -1. **לכל סוגיה משפטית** בתיק — בדוק ב-`daphna-precedent-network.md`: - - האם יש תקדם מועדף של דפנה לסוגיה? - - האם הוא הוצג בכתבי הטענות? אם לא — סמן כתקדם שיש להוסיף -2. **תקדמים אישיים**: `search_decisions` בקטגוריה זהה לתיק. אם דפנה כבר הכריעה בסוגיה דומה: - - אם תוצאה דומה: תקדם לחיסכון דוקטרינרי ("כפי שקבענו ב-X") - - אם תוצאה הפוכה: ציין כי **חובה** הבחנה (distinguishing) -3. **קורפוס פסיקה סמכותית**: `search_precedent_library` — חיפוש סמנטי בהלכות שאושרו ע"י דפנה (פסיקת עליון/מנהלי/ועדות ערר אחרות). מחזיר rule_statement + supporting_quote + citation מוכנים לציטוט בבלוק י. אם הצדדים הפנו לפסק דין שלא בקורפוס — הוסף אותו דרך `precedent_attach` (לתיק) או דרך ממשק ההעלאה ב-`/precedents` (לקורפוס הקבוע). -4. **דווח** איזה תקדמים מהקאנון רלוונטיים, איזה תקדמים אישיים נמצאו, ואילו הלכות מהקורפוס הסמכותי תומכות. +### שלב 2ב: חיפוש מובנה בשלושת הקורפוסים — חובה, עם תיעוד queries -**שלושת המקורות — אל תבלבל:** -- `search_decisions` = החלטות דפנה (style_corpus). -- `search_precedent_library` = פסיקה חיצונית סמכותית עם הלכות מאושרות. +**חובה לבצע** — לא הצעה. הניתוח קודם הראה (ערר 1200-25) שאם הקורפוס לא נסרק במפורש, מפספסים תקדימי עליון רלוונטיים שיושבים בו. ה-QA יחזיר `needs_revision` אם סעיף ה-queries חסר. + +**שלושת הקורפוסים — אל תבלבל:** +- `search_precedent_library` = פסיקה חיצונית סמכותית עם הלכות מאושרות (עליון/מנהלי/ועדות ערר אחרות) + supporting_quote מוכן. +- `search_decisions` = החלטות דפנה (style_corpus) — הקאנון האישי שלה. - `precedent_search_library` = ציטוטים שדפנה צירפה ידנית לתיקים בעבר (case_precedents). +#### 2ב.1 — קורפוס סמכותי (`search_precedent_library`) — חובה + +לכל **סוגיה משפטית מרכזית** בתיק — הרץ לפחות שאילתה אחת עם פילטרים: + +| סיווג תיק | practice_area | +|------------|---------------| +| 1xxx (רישוי ובניה) | `rishuy_uvniya` | +| 8xxx (היטל השבחה) | `histael_hashbacha` | +| 9xxx (פיצויים ס' 197) | `pitsuim_197` | + +אם הסוגיה ב-`appeal_subtype` ידוע (כמו "שימוש חורג", "סטייה ניכרת") — הוסף `appeal_subtype` לפילטר. + +``` +search_precedent_library( + query="...", + practice_area="rishuy_uvniya", + appeal_subtype="שימוש חורג", + limit=10 +) +``` + +#### 2ב.2 — קאנון דפנה (`search_decisions`) + +לכל סוגיה — בדוק אם דפנה כבר הכריעה: +- אם תוצאה דומה: תקדם לחיסכון דוקטרינרי ("כפי שקבענו ב-X") +- אם תוצאה הפוכה: ציין כי **חובה** הבחנה (distinguishing) + +#### 2ב.3 — בדיקה מצטלבת מול `daphna-precedent-network.md` + +לכל סוגיה — בדוק במסמך: +- האם יש תקדם מועדף של דפנה? +- האם הוצג בכתבי הטענות? אם לא — סמן כתקדם שיש להוסיף. + +#### 2ב.4 — תיעוד מחייב — סעיף "שאילתות לקורפוסים" ב-`precedent-research.md` + +חובה להופיע סעיף בשם **"ז. שאילתות לקורפוסים — log מלא"** עם: + +```markdown +## ז. שאילתות לקורפוסים — log מלא + +### קורפוס סמכותי (search_precedent_library) + +#### Q1 — סוגיה: [שם] +- **שאילתה:** "..." +- **פילטרים:** practice_area=..., appeal_subtype=... +- **תוצאות:** N +- **נבחרו:** [case_number] — headnote/למה רלוונטי +- **נדחו:** [case_number] — למה לא +- **0 results?** ציין מפורש + נמק + +#### Q2 — ... + +### קאנון דפנה (search_decisions) +#### Q1 — ... +``` + +**negative evidence חובה:** גם 0 results נרשם. זה ההבדל בין "נסרק וריק" ל"לא נסרק". + +**מינימום:** queries לקורפוס הסמכותי = מספר סוגיות מרכזיות שזוהו. + +5. **דווח** איזה תקדמים מהקאנון רלוונטיים, איזה תקדמים אישיים נמצאו, ואילו הלכות מהקורפוס הסמכותי תומכות. + ### שלב 3: מיפוי תכנית 1. קרא הוראות התכנית **במלואן** — לא רק את הסעיף הנטען 2. זהה סעיפים רלוונטיים למחלוקת @@ -158,19 +213,11 @@ python3 /home/chaim/legal-ai/scripts/notify.py \ **אם הכל עבר בהצלחה (כל בדיקות השלב הקודם עברו, אין כשל בפלט):** ```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "done"}' -``` +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'``` **אם בדיקות נכשלו, חסר פלט, או חסר מידע קריטי:** ```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "blocked"}' -``` +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "blocked"}'``` **אסור** לסיים `done` עם פלט חסר — אם משהו נכשל, סטטוס = `blocked` + comment עם פירוט. ### העֵר את העוזר המשפטי (CEO) — חובה! @@ -182,11 +229,7 @@ else CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה fi -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/agents/$CEO_ID/wakeup" \ - -d '{"source":"automation","triggerDetail":"system","reason":"חוקר תקדימים סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}' -``` +~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" '{"source":"automation","triggerDetail":"system","reason":"חוקר תקדימים סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}'``` **⚠️ אסור להשתמש ב-INSERT INTO agent_wakeup_requests ישירות!** הכנסה ישירה ל-DB יוצרת רק את הבקשה בלי heartbeat_run — והסוכן לא יתעורר לעולם. **תמיד להשתמש ב-API בלבד.** **⚠️ אסור לקבע UUID של CEO** — UUID שונה לכל חברה. תמיד דרך `$PAPERCLIP_COMPANY_ID`. wakeup לחברה אחרת נדחה: `Agent key cannot access another company`. diff --git a/.claude/agents/legal-writer.md b/.claude/agents/legal-writer.md index 2bec767..fb6bb06 100644 --- a/.claude/agents/legal-writer.md +++ b/.claude/agents/legal-writer.md @@ -210,19 +210,11 @@ case_update(case_number, status="drafted") **אם הכל עבר בהצלחה (כל בדיקות השלב הקודם עברו, אין כשל בפלט):** ```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "done"}' -``` +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'``` **אם בדיקות נכשלו, חסר פלט, או חסר מידע קריטי:** ```bash -curl -s -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/issues/{issue-id}" \ - -d '{"status": "blocked"}' -``` +~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "blocked"}'``` **אסור** לסיים `done` עם פלט חסר — אם משהו נכשל, סטטוס = `blocked` + comment עם פירוט. ### העֵר את העוזר המשפטי (CEO) — חובה! @@ -234,11 +226,7 @@ else CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה fi -curl -s -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" \ - -H "Content-Type: application/json" \ - "$PAPERCLIP_API_URL/api/agents/$CEO_ID/wakeup" \ - -d '{"source":"automation","triggerDetail":"system","reason":"כותב החלטה סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}' -``` +~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" '{"source":"automation","triggerDetail":"system","reason":"כותב החלטה סיים משימה [issue-id] בסטטוס [done/blocked]","payload":{"issueId":"[issue-id]","mutation":"agent_completion"}}'``` **⚠️ אסור להשתמש ב-INSERT INTO agent_wakeup_requests ישירות!** הכנסה ישירה ל-DB יוצרת רק את הבקשה בלי heartbeat_run — והסוכן לא יתעורר לעולם. **תמיד להשתמש ב-API בלבד.** **⚠️ אסור לקבע UUID של CEO** — UUID שונה לכל חברה. תמיד דרך `$PAPERCLIP_COMPANY_ID`. wakeup לחברה אחרת נדחה: `Agent key cannot access another company`. diff --git a/.taskmaster/state.json b/.taskmaster/state.json index 83236fc..28d71ac 100644 --- a/.taskmaster/state.json +++ b/.taskmaster/state.json @@ -1,3 +1,6 @@ { - "migrationNoticeShown": true + "migrationNoticeShown": true, + "currentTag": "legal-ai", + "lastSwitched": "2026-05-03T20:31:48.957Z", + "branchTagMapping": {} } \ No newline at end of file diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 2f4c4ed..3de4c72 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -1155,13 +1155,228 @@ ], "priority": "medium", "subtasks": [] + }, + { + "id": "14", + "title": "Upgrade: speed up halacha+metadata extraction", + "description": "Halacha extraction on long rulings is slow (5-15 min for typical court ruling, 30-50 min for a 207-chunk appeals committee decision). Root cause: each chunk spawns a separate `claude -p` subprocess (5-10 sec startup overhead each), Hebrew prompts on cold cache run 30-90 sec, and there's no prompt-cache sharing between chunks. Acceleration options to evaluate later when speed becomes a real blocker.\n\nOptions (each can be combined):\n\n1. Concurrency 3 -> 6 in halacha_extractor.CHUNK_CONCURRENCY. ~2x faster wall-clock. Cost: 6x ~300MB RSS = 1.8GB peak — verify on Nautilus headroom.\n\n2. Larger chunks 12K -> 18-25K chars (CHUNK_TARGET_CHARS in claims_extractor.py / halacha_extractor.py). Fewer waves. Risk: timeout on cold cache (currently 1800s ceiling), and may degrade extraction precision for very long sections.\n\n3. Anthropic SDK direct with 5-min ephemeral prompt caching on the static instruction prefix (already wired the parameter as system= in claude_session.query). Estimated 5-10x faster because cache reads are ~10% of cold cost. Costs ~$0.30-2 per long ruling on Sonnet 4.6. Chair previously rejected this path for ALL traffic ('we work only with claude session'). Compromise: SDK only for the precedent-library corpus build (static, one-time), claude session for live decision drafting (interactive, frequent).\n\n4. Two-tier prompt: a short 'classification' pass with claude -p deciding which chunks contain halachot, then deep extraction only on positive chunks. Could cut total LLM time by 40-60% on rulings with lots of factual chapters.\n\n5. Already implemented (Apr 3, 2026): skip non-extractable sections — only run on chunks where section_type IN (legal_analysis, ruling, conclusion); fallback to all chunks when chunker labels nothing. So that win is already banked.\n\nRe-evaluate when: a chair drops a 200K+ char ruling into the queue and the wait becomes painful, OR when the precedent-library has 50+ pending entries and bulk processing matters.", + "details": "", + "testStrategy": "", + "status": "deferred", + "dependencies": [], + "priority": "low", + "subtasks": [], + "updatedAt": "2026-05-03T16:03:07.222Z" + }, + { + "id": "15", + "title": "Backfill multimodal — החלטה על rollout מורחב לאחר A/B עם דפנה", + "description": "תזכורת לבדוק עם דפנה אם voyage-multimodal-3 על 8174-24 + 8137-24 עוזר בפועל, ולהחליט אם להריץ backfill על שאר הקורפוס (~236 docs, ~17,700 pages, ~2 שעות זמן API, ~350MB disk).", + "details": "תאריך יעד מומלץ: ~2026-05-10 (שבוע מהיום, 2026-05-03).\n\nקריטריונים להחלטה (אם מתקיים אחד — להריץ rollout):\n • דפנה זיהתה לפחות פעמיים ערך מוסף ב-8174-24 או 8137-24 (תקדים שלא הייתה מוצאת בלי image side, או חתימה/טבלה/תרשים שצף ב-top results)\n • היא ביקשה במפורש להפעיל על תיק נוסף ספציפי\n • היא מבקשת לעבור ל-search מצטלב (search_decisions, find_similar_cases) מעבר לתיק הנוכחי\n\nאם דפנה לא ראתה ערך — להחליט: לבטל / לכוונן MULTIMODAL_TEXT_WEIGHT (0.5 → 0.55-0.65) / לחכות עוד שבוע.\n\nאם החליטו להריץ — סדר עדיפויות:\n 1. שמאי-heavy: 8xxx (היטל השבחה) ו-9xxx (פיצויים) — שם הערך הגדול ביותר\n 2. תיקי 1xxx (רישוי ובניה) אחרון\n\nהרצה:\n CONTAINER=$(sudo docker ps --format '{{.Names}}' | grep gyjo | head -1)\n sudo docker cp scripts/multimodal_backfill.py $CONTAINER:/tmp/\n sudo docker cp scripts/backfill_chunk_pages.py $CONTAINER:/tmp/\n sudo docker exec $CONTAINER python /tmp/multimodal_backfill.py 8xxx-yy 9xxx-yy ...\n sudo docker exec $CONTAINER python /tmp/backfill_chunk_pages.py 8xxx-yy 9xxx-yy ...\n\nרפרנסים:\n • docs/voyage-upgrades-plan.md סעיף 'שלב C — voyage-multimodal-3 (✅ בוצע)'\n • commits 242f668..d12cdb1 על main\n • זיכרון: project_multimodal_stage_c.md, feedback_hybrid_retrieval_rrf.md", + "testStrategy": "", + "status": "pending", + "dependencies": [], + "priority": "low", + "subtasks": [] + }, + { + "id": "16", + "title": "[Paperclip Gap 1] runtime_config ריק — חסרים graceSec/cooldownSec/maxConcurrentRuns", + "description": "runtime_config = '{}' לכל 14 הסוכנים. מסתבר שעיקר ההגדרות החשובות (timeoutSec=3600, maxTurnsPerRun=500) יושבות ב-adapter_config ולא ב-runtime_config — אז המצב פחות חמור. אבל graceSec/cooldownSec/maxConcurrentRuns עדיין חסרים.", + "details": "תיקון לניתוח המקורי שגוי בעקבות בדיקה ב-DB:\n\nמה שכן יש לנו (ב-adapter_config, לא runtime_config):\n- timeoutSec: 3600 (לכל הסוכנים)\n- maxTurnsPerRun: 500 (לכל הסוכנים)\n- model + effort=high (לכל הסוכנים)\n- paperclipSkillSync.desiredSkills (5/7 סוכנים — חסר אצל הגהת מסמכים ומנתח משפטי)\n\nמה שבאמת חסר ב-runtime_config:\n- heartbeat.graceSec — זמן grace לפני SIGKILL אחרי timeout. מהקוד: Math.max(1, graceSec)*1000. אם לא מוגדר → 1ms grace. בעיה אם הסוכן נחתך באמצע commit ל-DB.\n- heartbeat.cooldownSec — default ביצירה חדשה: 10. אצלנו לא מוגדר.\n- heartbeat.maxConcurrentRuns — default מ-AGENT_DEFAULT_MAX_CONCURRENT_RUNS (כנראה 1).\n- heartbeat.wakeOnDemand — default=true בקוד. אצלנו לא מוגדר אבל בפועל true.\n- heartbeat.enabled — default=false (timer off). זה הרצוי אצלנו.\n\nפעולה (Phase 1):\n1. עדכון runtime_config של כל סוכן: { heartbeat: { graceSec: 60, cooldownSec: 10, maxConcurrentRuns: 1, wakeOnDemand: true } }\n2. בעיקר graceSec — בלעדיו commit באמצע יכול להיכשל\n3. cooldownSec=10 (זהה לdefault ב-UI ליצירת agent חדש)\n\nהשפעה: minimal — רוב המקרים עובדים עם defaults. graceSec הוא העיקר.", + "testStrategy": "1. SELECT name, runtime_config->'heartbeat' FROM agents → לראות שכל סוכן מקבל graceSec/cooldownSec/maxConcurrentRuns/wakeOnDemand.\n2. בדיקה: סוכן ארוך נחתך ב-timeout — לבדוק שהיתה הזדמנות לציין graceful shutdown ב-30-60 שניות", + "status": "done", + "dependencies": [], + "priority": "medium", + "subtasks": [], + "updatedAt": "2026-05-04T07:47:02.008Z" + }, + { + "id": "17", + "title": "[Paperclip Gap 2] תקציבים = 0 לכל הסוכנים — אין budget enforcement", + "description": "budget_monthly_cents = 0 ו-spent_monthly_cents = 0 לכל 14 הסוכנים. Paperclip מציע cost control מובנה — אנחנו מתעלמים.", + "details": "ממצא: SELECT name, budget_monthly_cents, spent_monthly_cents FROM agents → הכל אפס.\n\nסיכון: לולאה חבויה יכולה לשרוף מאות $. אין auto-pause ב-80% spend (דפוס ש-CEO HEARTBEAT הרשמי מצפה לו).\n\nפעולה (Phase 3):\n1. מדידה: כמה כל סוכן באמת מוציא בחודש כיום (דרך לוגי claude-code, או Anthropic dashboard).\n2. הגדרת budget_monthly_cents סביר לכל סוכן (כותב Opus ≫ מנתח Sonnet).\n3. בדיקה שהמנגנון מפסיק כשמגיעים ל-100%.\n\nשאלה לחיים לפני ביצוע: באיזו רזולוציה למדוד? לפי Anthropic invoice, או לפי טוקנים בלוגים של claude_session?", + "testStrategy": "בדיקה ידנית: להגדיר budget קטן לסוכן ניסוי (1 cent), לעורר אותו על משימה, לוודא שמתעורר ונחסם. לעקוב ב-spent_monthly_cents.", + "status": "done", + "dependencies": [], + "priority": "high", + "subtasks": [], + "updatedAt": "2026-05-04T10:18:08.046Z" + }, + { + "id": "18", + "title": "[Paperclip Gap 3] חסר X-Paperclip-Run-Id header בקריאות API", + "description": "ה-skill הרשמי קובע: 'You MUST include -H X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID on ALL API requests that modify issues'. ב-HEARTBEAT.md שלנו אין זכר לכך.", + "details": "ממצא: grep -n 'X-Paperclip-Run-Id' .claude/agents/ → 0 hits. כל curl ב-checkout/comments/PATCH issues — בלי הheader.\n\nסיכון: audit trail שבור. שאלה 'איזו ריצה שינתה את ה-issue X?' אין לה תשובה ב-DB.\n\nפעולה (Phase 1):\n1. עדכון .claude/agents/HEARTBEAT.md — דוגמאות ה-curl יכללו את הheader\n2. עדכון 6 קבצי הסוכנים (legal-ceo.md, legal-analyst.md, legal-researcher.md, legal-writer.md, legal-qa.md, legal-exporter.md) — כל מקום שיש curl POST/PATCH\n3. בדיקה שיש env var $PAPERCLIP_RUN_ID זמין בכל heartbeat", + "testStrategy": "בלוגי Paperclip (heartbeat_runs טבלה) לראות שהפעולות שלנו מקושרות ל-run_id. SELECT * FROM activity_log WHERE run_id IS NOT NULL ORDER BY created_at DESC LIMIT 10.", + "status": "done", + "dependencies": [], + "priority": "high", + "subtasks": [], + "updatedAt": "2026-05-04T08:49:44.646Z" + }, + { + "id": "19", + "title": "[Paperclip Gap 4] לא משתמשים ב-/api/issues/{id}/interactions לאישורים", + "description": "Paperclip מציע API מובנה לאישור/שאלות (request_confirmation, ask_user_questions, suggest_tasks) עם idempotency keys ו-auto-wake. אנחנו עדיין כותבים 'חיים, מה לעשות?' כ-comment חופשי.", + "details": "סוגי interaction:\n- ask_user_questions — שאלות מובנות\n- request_confirmation — yes/no עם idempotency key (confirmation:{issueId}:plan:{revisionId})\n- suggest_tasks — הצעת עץ משימות\n- continuationPolicy: wake_assignee — wake אוטומטי על מענה\n- supersedeOnUserComment: true — בטל אם חיים עונה\n\nסיכון: אין UI מובנה לחיים (כפתורים), רק טקסט. אם הסוכן מתעורר פעמיים — שתי שאלות זהות.\n\nפעולה (Phase 2):\n1. בlegal-ceo.md — להחליף 'אם חיים לא הגדיר outcome: שאל בcomment' ב-request_confirmation\n2. בbrainstorm_directions — suggest_tasks במקום רשימת bullet\n3. בlegal-qa.md — request_confirmation לאישור export\n\nשאלה לחיים: האם תרצה לראות UI חדש או להישאר ב-Markdown comments?", + "testStrategy": "יצירת request_confirmation מסוכן ניסוי, בדיקה ב-UI שמופיעים כפתורי אישור/דחייה, בדיקה שהסוכן מתעורר אוטומטית עם PAPERCLIP_APPROVAL_ID env.", + "status": "done", + "dependencies": [ + "16", + "17", + "18" + ], + "priority": "medium", + "subtasks": [], + "updatedAt": "2026-05-04T11:18:59.050Z" + }, + { + "id": "20", + "title": "[Paperclip Gap 5] לא משתמשים ב-PAPERCLIP_WAKE_PAYLOAD_JSON fast-path", + "description": "בwake שמכוון ל-issue ספציפי, ה-env var מכיל כבר issue summary + comments חדשים דחוסים. ה-skill הרשמי אומר 'skip Steps 1-4 entirely'. שלנו תמיד fetcher גם ה-API.", + "details": "ממצא: HEARTBEAT.md סעיפים 2-2c תמיד פונים ל-API גם אם ה-payload כבר מכיל את הכל.\n\nתועלת: חיסכון 3-4 קריאות API לכל ריצה. בwakeups תכופים (CEO על comments) — חיסכון ניכר.\n\nפעולה (Phase 2):\n1. הוספה ל-HEARTBEAT.md בראש הסעיפים: 'אם $PAPERCLIP_WAKE_PAYLOAD_JSON קיים — קרא אותו ראשון. רק אם fallbackFetchNeeded:true או חסר הקשר רחב — fetch'.\n2. דוגמה לפענוח JSON: jq עם key paths\n3. בדיקה איזה wake reasons בכלל מקבלים payload (כנראה comment-driven בלבד)", + "testStrategy": "בWakeup דרך API עם payload, לבדוק בלוגי הסוכן שאין fetch לcomments. timeit על מספר ריצות לפני/אחרי.", + "status": "done", + "dependencies": [ + "18" + ], + "priority": "medium", + "subtasks": [], + "updatedAt": "2026-05-04T09:15:46.339Z" + }, + { + "id": "21", + "title": "[Paperclip Gap 6] שאילתות psql ישירות ל-issue_attachments — שובר אבסטרקציה", + "description": "HEARTBEAT.md סעיף 2c משתמש ב-psql ישיר ל-issue_attachments + assets. אם schema ישתנה (כפי שצפוי בעדכוני Paperclip) — כל הסוכנים נשברים.", + "details": "ממצא: 6 קבצי סוכן + HEARTBEAT.md מכילים PGPASSWORD=paperclip psql ... FROM issue_attachments ia JOIN assets a.\n\nסיכון: breakage בעדכון Paperclip. כפילות לוגיקה (copy-paste בכל סוכן).\n\nפעולה (Phase 2):\n1. בדיקה אם קיים endpoint רשמי /api/issues/{id}/attachments (curl + grep ב-server/src/routes)\n2. אם כן — להחליף את כל ה-psql\n3. אם לא — להעביר את ה-psql למקום יחיד: helper ב-mcp-server (mcp__legal-ai__list_issue_attachments tool)\n4. אופציה ג: לפתוח issue ב-paperclipai/paperclip לבקש endpoint\n\nתלוי במחקר API.", + "testStrategy": "אחרי החלפה: grep -rn 'issue_attachments' .claude/agents/ → 0 hits. בדיקה שסוכן עדיין רואה attachments בריצה.", + "status": "done", + "dependencies": [ + "20" + ], + "priority": "medium", + "subtasks": [], + "updatedAt": "2026-05-04T09:28:18.058Z" + }, + { + "id": "22", + "title": "[Paperclip Gap 7] לא משתמשים ב-/api/issues/{id}/heartbeat-context", + "description": "Endpoint רשמי שמחזיר issue + ancestors + goal/project + comment cursor בקריאה אחת. אנחנו עושים 3 קריאות נפרדות.", + "details": "ה-skill הרשמי: 'Prefer GET /api/issues/{issueId}/heartbeat-context first. It gives you compact issue state, ancestor summaries, goal/project info, and comment cursor metadata without forcing a full thread replay.'\n\nשלנו: HEARTBEAT.md סעיפים 2 + 2b → שלוש קריאות (inbox-lite, issue, comments).\n\nפעולה (Phase 2):\n1. הוספת endpoint כצעד 6 ב-HEARTBEAT.md לפני 'Do the work'\n2. הסרת קריאות מיותרות שכבר ב-context\n3. שמירת comment cursor (after={last-seen-id}) לקריאות עוקבות", + "testStrategy": "בדיקה שהendpoint מחזיר את כל המידע הדרוש. ספירת קריאות API לפני/אחרי בריצה אמיתית.", + "status": "done", + "dependencies": [ + "20" + ], + "priority": "medium", + "subtasks": [], + "updatedAt": "2026-05-04T09:28:14.247Z" + }, + { + "id": "23", + "title": "[Paperclip Gap 8+11] HEARTBEAT.md ארוך + אין שימוש ב-skills של Paperclip", + "description": "HEARTBEAT.md שלנו 220 שורות (vs upstream 85). Paperclip מציע 8 skills מוכנים (paperclip, paperclip-create-agent, וכו') שאנחנו לא משתמשים באף אחד.", + "details": "תיקון לניתוח: מסתבר ש-CEO + 4 סוכנים אחרים כן משתמשים ב-paperclipSkillSync עם 4 paperclip skills (paperclip, paperclip-create-agent, paperclip-create-plugin, para-memory-files). חסר אצל: הגהת מסמכים ומנתח משפטי (skills_count=0).\n\nממצא: ls skills/ ב-paperclip repo → 8 skills. שלנו: 0 skills של Paperclip בשימוש.\n\nרלוונטיים לנו:\n- paperclip — API patterns + heartbeat checklist (יכול להחליף חלק מ-HEARTBEAT.md)\n- paperclip-create-agent — אם נוסיף סוכן\n- paperclip-create-plugin — לעדכוני plugin-legal-ai\n- paperclip-converting-plans-to-tasks — יכול להחליף brainstorm_directions\n- diagnose-why-work-stopped — לתחזוקה\n\nפעולה (Phase 3):\n1. קריאת skills/paperclip/SKILL.md מלא\n2. הזרקת skill לסביבת הסוכנים (כנראה דרך CLI: paperclipai agent local-cli)\n3. שכתוב HEARTBEAT.md לפי הדפוס: project-specific only, delegation לskill הרשמי לכלל ה-API\n4. יעד: ~120 שורות ב-HEARTBEAT.md שלנו\n\nשאלה לחיים: האם להזריק skills כסימלינקים ל-symlinks קיימים, או דרך paperclipai CLI?", + "testStrategy": "אחרי שכתוב: סוכן ניסוי קורא את HEARTBEAT.md + paperclip skill, מבצע heartbeat מלא בלי שגיאות. השוואת אורך לפני/אחרי.", + "status": "done", + "dependencies": [ + "16", + "17", + "18", + "19", + "20", + "21", + "22" + ], + "priority": "medium", + "subtasks": [], + "updatedAt": "2026-05-04T16:44:27.553Z" + }, + { + "id": "24", + "title": "[Paperclip Gap 9] לבדוק bootstrapPromptTemplate deprecated באף סוכן", + "description": "מ-docs/agents-runtime.md: 'bootstrapPromptTemplate is deprecated... should be migrated to the managed instructions bundle system.' לבדוק האם adapter_config שלנו משתמש בזה.", + "details": "פעולה (Phase 1):\n1. SELECT name, adapter_config->'promptTemplate' as pt, adapter_config->'bootstrapPromptTemplate' as bpt FROM agents WHERE adapter_type = 'claude_local';\n2. אם בשימוש אצל סוכן כלשהו — מיגרציה למבנה החדש\n3. ייעוד: לבדוק תיעוד managed instructions bundle ב-paperclip docs\n\nהערה: זה כנראה לא ישפיע אצלנו (אנחנו משתמשים ב-symlinks ל-AGENTS.md/HEARTBEAT.md ישירות) — אבל חובה לוודא.", + "testStrategy": "SELECT הנ\"ל. אם 0 שורות מחזירות bpt לא-NULL — סגור את המשימה.", + "status": "done", + "dependencies": [], + "priority": "high", + "subtasks": [], + "updatedAt": "2026-05-04T08:19:27.766Z" + }, + { + "id": "25", + "title": "[Paperclip Gap 10] סוכנים מוכפלים בין 2 חברות — אין סנכרון", + "description": "14 שורות = 7 סוכנים × 2 חברות (1xxx, 8xxx). כל שינוי בהגדרות הסוכן צריך להיעשות פעמיים. אין מנגנון סנכרון או הורשה.", + "details": "ממצא: SELECT name, COUNT(*) FROM agents GROUP BY name → 2 לכל אחד.\n\nסיכון: drift בין החברות. שינוי runtime_config ל-CEO של 1xxx יכול לפספס את CEO של 8xxx.\n\nפעולה (Phase 3):\n1. בדיקה: האם Paperclip תומך ב-shared agents או chainOfCommand? (לקרוא docs/companies/)\n2. אם כן — מיגרציה למבנה משותף\n3. אם לא — סקריפט סנכרון: scripts/sync_agents_across_companies.py שמעתיק כל שינוי מחברה לחברה\n\nשאלה לחיים: בעתיד אם יהיו עוד סוגי ערר (10xxx?) — להוסיף עוד חברה או להשאיר 2?", + "testStrategy": "אם סקריפט: dry-run שמראה הבדלים בין 2 ה-CEOs. ואז apply ולוודא runtime_config זהה.", + "status": "done", + "dependencies": [ + "16" + ], + "priority": "medium", + "subtasks": [], + "updatedAt": "2026-05-04T09:52:14.263Z" + }, + { + "id": "26", + "title": "[Paperclip Gap 12] עדכון @paperclipai/plugin-sdk + capabilities חדשות", + "description": "ה-plugin שלנו: @paperclipai/plugin-sdk@^2026.325.0, apiVersion: 1, minimumHostVersion: 2026.325.0. ה-host: 2026.428.0. ייתכן capabilities חדשות (issue.interactions.create, וכו').", + "details": "פעולה (Phase 4 — אחרי שדרוג Paperclip stable):\n1. cd /home/chaim/plugin-legal-ai && npm view @paperclipai/plugin-sdk version\n2. אם חדשה: npm install @paperclipai/plugin-sdk@latest\n3. קריאת adapter-plugin.md המעודכן ב-paperclip repo\n4. בדיקה אם apiVersion: 2 קיים\n5. הוספת capabilities חדשות אם רלוונטי (בעיקר issue.interactions.create אחרי gap #4)\n6. npm run build && reinstall plugin\n\nתלוי בgap #19 (interactions API) — אם אנחנו רוצים שהplugin יוכל ליצור interactions, חייב capability חדש.", + "testStrategy": "אחרי npm install: בדיקה ש-plugin עולה ב-Paperclip בלי last_error. SELECT status, last_error FROM plugins WHERE plugin_key='marcusgroup.legal-ai'.", + "status": "pending", + "dependencies": [ + "27", + "19" + ], + "priority": "low", + "subtasks": [] + }, + { + "id": "27", + "title": "[Paperclip Phase 4] שדרוג Paperclip לגרסה stable הבאה (לא 2026.428.0)", + "description": "כרגע אנחנו על 2026.428.0 — הגרסה היציבה האחרונה. כשיופיע stable חדש (כנראה 2026.5xx.x), לבצע שדרוג מבוקר.", + "details": "טריגר: npm view paperclipai dist-tags.latest מחזיר משהו ≠ 2026.428.0.\n\nפעולה:\n1. קריאת releases/v2026.5xx.x.md ב-GitHub\n2. בדיקת שינויים שעלולים להשפיע (CUSTOMIZATIONS.md סעיפים: hebrew, RTL, plugin driver, heartbeat)\n3. גיבוי: pg_dump של paperclip DB + cp -r ~/.npm/_npx/43414d9b790239bb /tmp/\n4. pm2 stop paperclip\n5. rm -rf ~/.npm/_npx/43414d9b790239bb\n6. npx paperclipai@latest run (יוריד גרסה חדשה)\n7. הרצה מחדש: ~/.paperclip/hebrew/apply-hebrew.sh && ~/.paperclip/issue-link-fix/apply-issue-link-fix.sh\n8. pm2 restart paperclip\n9. בדיקה ב-pc.nautilus.marcusgroup.org: עברית + plugin פעיל + סוכן מתעורר על comment\n\nתלוי בלי dependencies (יכול להיות מבוצע בכל עת אחרי שיש stable חדש).", + "testStrategy": "אחרי שדרוג: cat ~/.npm/_npx/43414d9b790239bb/node_modules/paperclipai/package.json | grep version → גרסה חדשה. UI עברית. test wakeup על issue.", + "status": "pending", + "dependencies": [], + "priority": "low", + "subtasks": [] + }, + { + "id": "28", + "title": "[Paperclip Auxiliary] להפעיל skill-sync ל-2 סוכנים שפיספסו", + "description": "הגהת מסמכים ומנתח משפטי לא קיבלו אף פעם revision מסוג skill-sync (לעומת 5 האחרים שכן). לבצע sync.", + "details": "ממצא: בדיקה ב-agent_config_revisions:\n- עוזר משפטי: 3 skill-sync revisions (יש 7 skills)\n- חוקר תקדימים: 3 (יש 5)\n- מייצא טיוטה: 5 (יש 5)\n- בודק איכות: 1 (יש 5)\n- כותב החלטה: 1 (יש 5)\n- הגהת מסמכים: 0 (יש 0) ❌\n- מנתח משפטי: 0 (יש 0) ❌\n\nאופציות:\n1. UI: agent settings → 'sync skills'\n2. API: POST /api/agents/{id}/skills-sync (לאתר)\n3. CLI: paperclipai agent skill-sync (לבדוק אם קיים)\n4. SQL ידני (לא מומלץ — דורף revision tracking)\n\nSkills להעתקה (לפי בודק איכות):\n- paperclipai/paperclip/paperclip\n- paperclipai/paperclip/paperclip-create-agent\n- paperclipai/paperclip/paperclip-create-plugin\n- paperclipai/paperclip/para-memory-files\n- (אופציונלי) local/eba6210d5a/legal-decision", + "testStrategy": "SELECT name, jsonb_array_length(adapter_config->'paperclipSkillSync'->'desiredSkills') FROM agents WHERE name IN ('הגהת מסמכים', 'מנתח משפטי') → 4-5. revision חדש ב-agent_config_revisions עם source='skill-sync'.", + "status": "done", + "dependencies": [], + "priority": "medium", + "subtasks": [], + "updatedAt": "2026-05-04T09:46:32.092Z" + }, + { + "id": "29", + "title": "[legal-ai UI] מסך הגדרות סוכנים — הצגה + עריכה + שמירה", + "description": "מסך אדמין ב-legal-ai UI שמציג את כל הגדרות הסוכנים (model, timeout, runtime_config, skills, budget) ומאפשר עריכה ושמירה. מונע SQL ישיר.", + "details": "מטרה: ממשק אדמין מרכזי במקום שעריכה תהיה רק ב-UI של Paperclip + SQL ישיר + CUSTOMIZATIONS.md.\n\nשדות (לכל סוכן × 2 חברות):\n1. adapter_config: model, effort, timeoutSec, maxTurnsPerRun, extraArgs[], paperclipSkillSync.desiredSkills[]\n2. runtime_config.heartbeat: graceSec, cooldownSec, wakeOnDemand, maxConcurrentRuns, enabled, intervalSec\n3. budget_monthly_cents (לקראת gap #2)\n4. status / pause_reason (קריאה + כפתור pause/resume)\n\nאופציות מימוש:\nA. עמוד חדש ב-legal-ai/web-ui (Next.js 16) — קורא Paperclip DB דרך FastAPI endpoint חדש (/api/admin/paperclip-agents)\nB. קריאה ל-Paperclip API (/api/companies/{id}/agents) — REST טהור, פחות שדות זמינים\nC. iframe ל-Paperclip UI — שטחי\n\nהמלצה: A. שולט מלא + ולידציה משפטית (timeoutSec >= 1800 כי OCR).\n\nתלוי ב: gap #25 (סוכנים מוכפלים) — אם נעבור לshared, המסך יתאים.\n\nשאלות פתוחות לחיים:\n- auth: מי יכול לגשת? (כיום אין auth ב-legal-ai)\n- bulk edit ל-2 חברות יחד או נפרד?\n- חשיפת skill marketplace (להוסיף/להוריד skills) או רק קריאה?", + "testStrategy": "1. עמוד עולה ב-/admin/agents בlegal-ai UI. 2. עריכת timeoutSec ושמירה → SELECT ב-DB מאמת. 3. revision חדש ב-agent_config_revisions עם source מתאים.", + "status": "pending", + "dependencies": [ + "16", + "17", + "25" + ], + "priority": "medium", + "subtasks": [] } ], "metadata": { "version": "1.0.0", - "lastModified": "2026-05-03T10:19:15.134Z", - "taskCount": 13, - "completedCount": 12, + "lastModified": "2026-05-04T16:44:27.554Z", + "taskCount": 29, + "completedCount": 23, "tags": [ "legal-ai" ] diff --git a/CLAUDE.md b/CLAUDE.md index 5f52443..d323cbd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,6 +48,7 @@ | [`docs/corpus-analysis.md`](docs/corpus-analysis.md) | ניתוח שיטתי של 24 החלטות — מפת תוכן, דפוסי דיון תכנוני, פערים | **לפני כל כתיבת החלטה** | | [`docs/product-specification.md`](docs/product-specification.md) | איפיון מוצר מלא — personas, תהליכים עסקיים, דרישות | להתמצאות עסקית/מוצרית | | [`docs/new-company-setup-guide.md`](docs/new-company-setup-guide.md) | מדריך הקמת חברה חדשה (CMPA) — skills, corpus, style analysis | לפני הוספת חברה/סוג ערר חדש | +| [`skills/new-company-setup/SKILL.md`](skills/new-company-setup/SKILL.md) | **Blueprint טכני מלא להוספת חברה** — 11 שלבים מסודרים (companies, agents, runtime/adapter, skills, instructions, code, mappings) + checklist 10 מלכודות מ-Gap analysis #16-#28 | **חובה לפני הוספת חברה** (יותר actionable מ-doc) | | [`docs/audit-report.md`](docs/audit-report.md) | דוח audit של המערכת | רקע כללי | | [`docs/case-migration-tracker.md`](docs/case-migration-tracker.md) | מעקב מיגרציה של תיקים קיימים | לצורך מעקב | | [`docs/case-deletion-runbook.md`](docs/case-deletion-runbook.md) | runbook מלא למחיקת תיק — legal-ai DB + disk + Paperclip + Gitea, FK ordering, fallback ל-SQL ישיר | לפני reset שלם של תיק (מבחן, מחיקה בטעות) | @@ -160,6 +161,25 @@ - ה-CEO קורא את ה-comment, מחליט על ניתוב, ויוצר issue לסוכן המתאים - כל הסוכנים חייבים לקרוא comments אחרונים לפני שהם מתחילים לעבוד (HEARTBEAT שלבים 2b-2c) +### קריאות API — תמיד דרך helper, לעולם לא `curl` ישיר +- **bash (סוכנים):** `~/legal-ai/scripts/pc.sh [BODY_JSON]` — מוסיף Authorization, X-Paperclip-Run-Id, Content-Type, base URL. ראה `HEARTBEAT.md §0`. +- **Python (FastAPI):** `from web.paperclip_api import pc_request; await pc_request("POST", "/api/...", json={...})` — שימוש ב-board API key. +- **אסור** `curl ... $PAPERCLIP_API_URL` ישיר ב-bash; **אסור** `httpx.AsyncClient` ישיר ל-Paperclip ב-Python. +- **למה:** ה-skill הרשמי דורש `X-Paperclip-Run-Id` בכל קריאה משנה issue. אצלנו ה-audit trail עבד ממילא דרך JWT claims (`runId: runIdHeader || claims.run_id`), אבל ה-helper מבטיח עקביות + תאימות ל-board API keys (long-lived) שלא נושאות JWT claims. + +### Cross-company agent sync — אחרי כל שינוי הגדרות +- יש 14 סוכנים = 7 × 2 חברות (CMP=1xxx, CMPA=8xxx). Paperclip מחייב `agents.company_id NOT NULL` — אין shared agents. +- **Master = CMP (1xxx)**, **Mirror = CMPA (8xxx)**. +- אחרי כל שינוי ב-`adapter_config`, `runtime_config`, `budget_monthly_cents`, או skills של סוכן ב-master (UI, SQL, או API), **חובה להריץ:** + ```bash + PAPERCLIP_BOARD_API_KEY=$(...infisical...) \ + python ~/legal-ai/scripts/sync_agents_across_companies.py --verify # לבדיקה + PAPERCLIP_BOARD_API_KEY=$(...) \ + python ~/legal-ai/scripts/sync_agents_across_companies.py --apply # לסנכרן + ``` +- הסקריפט מסנן local skills שלא קיימים ב-CMPA (מציג אזהרה), משתמש ב-API (לא DB ישיר), יוצר revisions, idempotent. +- שאלות ה-skill הרשמי של Paperclip — `paperclip` skill תחת `paperclipai/paperclip`. + --- ## עקרונות כתיבה קריטיים diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index 859c261..f7e4683 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -8,6 +8,9 @@ | Script | Type | Purpose | Scheduled | |--------|------|---------|-----------| +| `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.** | ידני אחרי כל שינוי | | `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/pc.sh b/scripts/pc.sh new file mode 100755 index 0000000..ba09cc6 --- /dev/null +++ b/scripts/pc.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# pc.sh — Paperclip API wrapper for agents. +# +# Usage: +# pc.sh [body_json] [extra_curl_args...] +# +# Adds: +# - Authorization: Bearer $PAPERCLIP_API_KEY +# - X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID (audit trail; falls back to JWT claims if empty) +# - Content-Type: application/json (when body provided) +# - Base URL: $PAPERCLIP_API_URL +# +# Examples: +# ~/legal-ai/scripts/pc.sh GET "/api/agents/me/inbox-lite" +# ~/legal-ai/scripts/pc.sh POST "/api/issues/$ISSUE_ID/checkout" +# ~/legal-ai/scripts/pc.sh POST "/api/issues/$ISSUE_ID/comments" '{"body":"שלום"}' +# ~/legal-ai/scripts/pc.sh PATCH "/api/issues/$ISSUE_ID" '{"status":"done"}' +# ~/legal-ai/scripts/pc.sh DELETE "/api/issues/$ISSUE_ID" +# +# Sourcing as a function (optional): +# source ~/legal-ai/scripts/pc.sh && pc POST "/api/issues/$ISSUE_ID/checkout" + +set -euo pipefail + +pc() { + local method="${1:-}" + local path="${2:-}" + local body="${3:-}" + if [ $# -ge 3 ]; then shift 3; else shift "$#"; fi + + if [ -z "$method" ] || [ -z "$path" ]; then + echo "usage: pc.sh [BODY_JSON] [extra curl args...]" >&2 + return 2 + fi + : "${PAPERCLIP_API_URL:?PAPERCLIP_API_URL not set}" + : "${PAPERCLIP_API_KEY:?PAPERCLIP_API_KEY not set}" + + local args=(-s -X "$method" + -H "Authorization: Bearer $PAPERCLIP_API_KEY" + -H "X-Paperclip-Run-Id: ${PAPERCLIP_RUN_ID:-}") + + if [ -n "$body" ]; then + args+=(-H "Content-Type: application/json" -d "$body") + fi + + curl "${args[@]}" "$@" "${PAPERCLIP_API_URL}${path}" +} + +# When invoked directly (not sourced), forward args to pc(). +if [ "${BASH_SOURCE[0]}" = "${0}" ]; then + pc "$@" +fi diff --git a/scripts/sync_agents_across_companies.py b/scripts/sync_agents_across_companies.py new file mode 100644 index 0000000..fc70971 --- /dev/null +++ b/scripts/sync_agents_across_companies.py @@ -0,0 +1,382 @@ +#!/usr/bin/env python3 +"""sync_agents_across_companies.py — Mirror agent configs from CMP (1xxx) to CMPA (8xxx). + +Gap #25: Paperclip enforces ``agents.company_id NOT NULL``, so we have 14 +agents (7 × 2 companies). Without sync, settings drift between the master +(CMP, 1xxx) and the mirror (CMPA, 8xxx). This script copies the relevant +fields one-way: CMP → CMPA. + +Design: "אל-כשל" — backup before apply, idempotent, dry-run by default, +clear field-level diff, rollback path printed on failure. + +Synced fields: + - adapter_config.{model, effort, timeoutSec, maxTurnsPerRun, + instructionsBundleMode, instructionsRootPath, + instructionsEntryFile, instructionsFilePath, + dangerouslySkipPermissions, extraArgs, cwd} + - adapter_config.paperclipSkillSync.desiredSkills (filtered for skills + that exist in the mirror company — local skills like + ``local/eba6210d5a/legal-decision`` only exist in CMP) + - runtime_config (full replace — heartbeat config) + - budget_monthly_cents + - metadata, icon, title, role + +Not synced (intentionally per-company): + - id, company_id, name, reports_to, default_environment_id + - adapter_type, agent_api_keys + - status, pause_reason, paused_at, last_heartbeat_at + - spent_monthly_cents (separate usage) + - permissions (per-company access policies) + +Usage: + python sync_agents_across_companies.py --verify # show drift only + python sync_agents_across_companies.py --dry-run # show plan + python sync_agents_across_companies.py --apply # backup + apply + +Requires: + PAPERCLIP_BOARD_API_KEY (Infisical: /paperclip @ nautilus) +""" +from __future__ import annotations + +import argparse +import asyncio +import json +import os +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import asyncpg +import httpx + +PAPERCLIP_DB_URL = os.environ.get( + "PAPERCLIP_DB_URL", "postgresql://paperclip:paperclip@127.0.0.1:54329/paperclip" +) +PAPERCLIP_API_URL = os.environ.get("PAPERCLIP_API_URL", "http://localhost:3100") +PAPERCLIP_BOARD_API_KEY = os.environ.get("PAPERCLIP_BOARD_API_KEY", "") + +BACKUP_DIR = Path("/home/chaim/.paperclip/instances/default/data/backups/manual") + +CMP_COMPANY_ID = "42a7acd0-30c5-4cbd-ac97-7424f65df294" # MASTER (1xxx) +CMPA_COMPANY_ID = "8639e837-4c9d-47fa-a76b-95788d651896" # MIRROR (8xxx) + +# adapter_config keys to sync (top-level only; paperclipSkillSync handled separately) +ADAPTER_CONFIG_SYNC_KEYS = [ + "model", "effort", "timeoutSec", "maxTurnsPerRun", + "instructionsBundleMode", "instructionsRootPath", "instructionsEntryFile", "instructionsFilePath", + "dangerouslySkipPermissions", "extraArgs", "cwd", +] + +# Top-level agent fields to sync +TOP_LEVEL_SYNC_FIELDS = [ + "budget_monthly_cents", "metadata", "icon", "title", "role", +] + + +def fail(msg: str) -> None: + print(f"❌ {msg}", file=sys.stderr) + sys.exit(1) + + +async def fetch_agents(conn: asyncpg.Connection, company_id: str) -> list[dict[str, Any]]: + rows = await conn.fetch( + """ + SELECT id::text, name, role, title, icon, + adapter_type, adapter_config, runtime_config, metadata, + budget_monthly_cents + FROM agents + WHERE company_id = $1::uuid + ORDER BY name + """, + company_id, + ) + out = [] + for r in rows: + d = dict(r) + # asyncpg returns jsonb as str; parse + for k in ("adapter_config", "runtime_config", "metadata"): + if isinstance(d.get(k), str): + d[k] = json.loads(d[k]) if d[k] else None + out.append(d) + return out + + +async def fetch_company_skills(conn: asyncpg.Connection, company_id: str) -> set[str]: + rows = await conn.fetch( + "SELECT key FROM company_skills WHERE company_id = $1::uuid", + company_id, + ) + return {r["key"] for r in rows} + + +def _get(d: dict | None, key: str, default=None): + return d.get(key, default) if isinstance(d, dict) else default + + +def compute_diff(master: dict, mirror: dict, mirror_skills: set[str]) -> dict[str, Any]: + """Return a dict describing what would change in mirror to match master. + Empty dict = in sync.""" + diff: dict[str, Any] = {} + + # Top-level fields + for field in TOP_LEVEL_SYNC_FIELDS: + if master.get(field) != mirror.get(field): + diff[field] = {"from": mirror.get(field), "to": master.get(field)} + + # adapter_config (per key) + m_ac = master.get("adapter_config") or {} + r_ac = mirror.get("adapter_config") or {} + ac_changes = {} + for key in ADAPTER_CONFIG_SYNC_KEYS: + if _get(m_ac, key) != _get(r_ac, key): + ac_changes[key] = {"from": _get(r_ac, key), "to": _get(m_ac, key)} + if ac_changes: + diff["adapter_config"] = ac_changes + + # paperclipSkillSync.desiredSkills — compare as a SUBSET check. + # The Paperclip API auto-adds company-level required runtime skills + # (e.g. paperclip-dev) to the desiredSkills list, so the mirror can + # legitimately have MORE skills than master. We only need master's + # filtered skills to be a subset of mirror's actual list. + master_desired = list((_get(m_ac, "paperclipSkillSync") or {}).get("desiredSkills") or []) + mirror_desired = list((_get(r_ac, "paperclipSkillSync") or {}).get("desiredSkills") or []) + master_filtered = [s for s in master_desired if s in mirror_skills] + skipped = [s for s in master_desired if s not in mirror_skills] + missing_in_mirror = set(master_filtered) - set(mirror_desired) + if missing_in_mirror: + diff["paperclipSkillSync.desiredSkills"] = { + "from": mirror_desired, + "to": master_filtered, + "missing_in_mirror": sorted(missing_in_mirror), + "skipped_unavailable_in_mirror": skipped, + } + + # runtime_config (full replace) + if (master.get("runtime_config") or {}) != (mirror.get("runtime_config") or {}): + diff["runtime_config"] = {"from": mirror.get("runtime_config"), "to": master.get("runtime_config")} + + return diff + + +def backup_agents_table() -> Path: + BACKUP_DIR.mkdir(parents=True, exist_ok=True) + stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") + out = BACKUP_DIR / f"agents-pre-cross-company-sync-{stamp}.sql" + env = {**os.environ, "PGPASSWORD": "paperclip"} + subprocess.run( + ["pg_dump", "-h", "127.0.0.1", "-p", "54329", "-U", "paperclip", + "-d", "paperclip", "-t", "agents", "--data-only", "-f", str(out)], + check=True, env=env, + ) + return out + + +def _short(value, max_len=80) -> str: + s = json.dumps(value, ensure_ascii=False, default=str) if not isinstance(value, str) else value + if len(s) > max_len: + return s[:max_len] + "..." + return s + + +def print_diff(agent_name: str, diff: dict, master_id: str, mirror_id: str) -> None: + if not diff: + print(f" ✓ {agent_name:14s} — in sync (no changes)") + return + print(f" ⚠ {agent_name:14s} — {len(diff)} change(s): master={master_id[:8]}… → mirror={mirror_id[:8]}…") + for key, change in diff.items(): + if key == "adapter_config": + for ac_key, ac_change in change.items(): + print(f" adapter_config.{ac_key}: {_short(ac_change['from'])} → {_short(ac_change['to'])}") + elif key == "paperclipSkillSync.desiredSkills": + print(f" paperclipSkillSync.desiredSkills: {len(change['from'])} → {len(change['to'])} skills") + for s in change.get("skipped_unavailable_in_mirror", []): + print(f" (skipped, not in mirror company: {s})") + elif key == "runtime_config": + print(f" runtime_config: full replace") + print(f" from: {_short(change['from'], 100)}") + print(f" to: {_short(change['to'], 100)}") + else: + print(f" {key}: {_short(change['from'])} → {_short(change['to'])}") + + +async def call_patch(agent_id: str, body: dict) -> tuple[int, dict]: + if not PAPERCLIP_BOARD_API_KEY: + fail("PAPERCLIP_BOARD_API_KEY not set") + headers = { + "Authorization": f"Bearer {PAPERCLIP_BOARD_API_KEY}", + "X-Paperclip-Run-Id": "", + "Content-Type": "application/json", + } + url = f"{PAPERCLIP_API_URL}/api/agents/{agent_id}" + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.patch(url, headers=headers, json=body) + try: + data = resp.json() + except Exception: + data = {"raw": resp.text[:500]} + return resp.status_code, data + + +async def call_skill_sync(agent_id: str, desired_skills: list[str]) -> tuple[int, dict]: + if not PAPERCLIP_BOARD_API_KEY: + fail("PAPERCLIP_BOARD_API_KEY not set") + headers = { + "Authorization": f"Bearer {PAPERCLIP_BOARD_API_KEY}", + "X-Paperclip-Run-Id": "", + "Content-Type": "application/json", + } + url = f"{PAPERCLIP_API_URL}/api/agents/{agent_id}/skills/sync" + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.post(url, headers=headers, json={"desiredSkills": desired_skills}) + try: + data = resp.json() + except Exception: + data = {"raw": resp.text[:500]} + return resp.status_code, data + + +async def apply_diff(mirror_id: str, agent_name: str, diff: dict) -> list[str]: + """Apply the computed diff to the mirror agent. Returns list of error strings.""" + errors: list[str] = [] + + # Build PATCH body for top-level + adapter_config (skills handled separately) + patch_body: dict[str, Any] = {} + for field in TOP_LEVEL_SYNC_FIELDS: + if field in diff: + # snake_case → camelCase for the API + api_key = { + "budget_monthly_cents": "budgetMonthlyCents", + "metadata": "metadata", + "icon": "icon", + "title": "title", + "role": "role", + }[field] + patch_body[api_key] = diff[field]["to"] + if "adapter_config" in diff: + patch_body["adapterConfig"] = {k: v["to"] for k, v in diff["adapter_config"].items()} + if "runtime_config" in diff: + patch_body["runtimeConfig"] = diff["runtime_config"]["to"] + + if patch_body: + status, data = await call_patch(mirror_id, patch_body) + if status >= 400: + errors.append(f"PATCH HTTP {status}: {json.dumps(data)[:300]}") + else: + print(f" ✓ PATCH applied ({len(patch_body)} top-level keys)") + + # Skills via dedicated endpoint (creates 'skill-sync' revision) + if "paperclipSkillSync.desiredSkills" in diff: + desired = diff["paperclipSkillSync.desiredSkills"]["to"] + status, data = await call_skill_sync(mirror_id, desired) + if status >= 400: + errors.append(f"skills/sync HTTP {status}: {json.dumps(data)[:300]}") + else: + print(f" ✓ skills/sync applied ({len(desired)} skills)") + + return errors + + +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") + p.add_argument("--only", help="Sync only the named agent (e.g., 'עוזר משפטי')") + args = p.parse_args() + + conn = await asyncpg.connect(PAPERCLIP_DB_URL) + try: + master_agents = await fetch_agents(conn, CMP_COMPANY_ID) + mirror_agents = await fetch_agents(conn, CMPA_COMPANY_ID) + mirror_skills = await fetch_company_skills(conn, CMPA_COMPANY_ID) + finally: + await conn.close() + + mirror_by_name = {a["name"]: a for a in mirror_agents} + + print(f"\n=== Master (CMP, 1xxx): {len(master_agents)} agents ===") + print(f"=== Mirror (CMPA, 8xxx): {len(mirror_agents)} agents ===") + 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)) + + 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 + + if not plan: + print(f"\n✓ All agents in sync — nothing to do.") + return + + if args.dry_run: + print(f"\n(dry-run mode — exiting without changes)\nRe-run with --apply to execute.") + return + + # APPLY + print(f"\n=== Backup ===") + backup_path = backup_agents_table() + print(f" ✓ {backup_path}") + + print(f"\n=== Applying ({len(plan)} agents) ===") + all_errors: list[str] = [] + for master, mirror, diff in plan: + print(f"\n → {master['name']} ({mirror['id']})") + errors = await apply_diff(mirror["id"], master["name"], diff) + if errors: + for e in errors: + print(f" ❌ {e}") + all_errors.extend([f"{master['name']}: {e}" for e in errors]) + + if all_errors: + print(f"\n=== ⚠️ {len(all_errors)} error(s) ===") + print(f"Rollback option: psql ... -f {backup_path}") + sys.exit(1) + + print(f"\n=== ✓ Sync complete — re-running --verify to confirm ===\n") + # Re-verify + conn = await asyncpg.connect(PAPERCLIP_DB_URL) + try: + master_agents = await fetch_agents(conn, CMP_COMPANY_ID) + mirror_agents = await fetch_agents(conn, CMPA_COMPANY_ID) + mirror_skills = await fetch_company_skills(conn, CMPA_COMPANY_ID) + finally: + await conn.close() + mirror_by_name = {a["name"]: a for a in mirror_agents} + + still_drifting = 0 + for m in master_agents: + mirror = mirror_by_name.get(m["name"]) + if not mirror or m["adapter_type"] != mirror["adapter_type"]: + continue + diff = compute_diff(m, mirror, mirror_skills) + if diff: + still_drifting += 1 + print(f" ⚠ {m['name']:14s} — STILL has {len(diff)} change(s) after apply (review!)") + + if still_drifting == 0: + print(f" ✓ All {len(master_agents)} agents in sync.") + else: + print(f"\n⚠️ {still_drifting} agents still drifting — investigate.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/sync_missing_agent_skills.py b/scripts/sync_missing_agent_skills.py new file mode 100644 index 0000000..856a2c1 --- /dev/null +++ b/scripts/sync_missing_agent_skills.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""sync_missing_agent_skills.py — One-shot fix for Gap #28. + +Adds the missing paperclipSkillSync to הגהת מסמכים and מנתח משפטי +in both companies (1xxx CMP, 8xxx CMPA). Idempotent: safe to re-run. + +Design: "אל-כשל" — backup, dry-run mode, idempotent, clear errors. + +Usage: + python sync_missing_agent_skills.py --dry-run # show plan only + python sync_missing_agent_skills.py --apply # actually do it + python sync_missing_agent_skills.py --verify # check current state +""" +from __future__ import annotations + +import argparse +import asyncio +import json +import os +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + +import asyncpg +import httpx + +PAPERCLIP_DB_URL = os.environ.get( + "PAPERCLIP_DB_URL", "postgresql://paperclip:paperclip@127.0.0.1:54329/paperclip" +) +PAPERCLIP_API_URL = os.environ.get("PAPERCLIP_API_URL", "http://localhost:3100") +PAPERCLIP_BOARD_API_KEY = os.environ.get("PAPERCLIP_BOARD_API_KEY", "") + +BACKUP_DIR = Path("/home/chaim/.paperclip/instances/default/data/backups/manual") + +PAPERCLIP_BASE_SKILLS = [ + "paperclipai/paperclip/paperclip", + "paperclipai/paperclip/paperclip-create-agent", + "paperclipai/paperclip/paperclip-create-plugin", + "paperclipai/paperclip/para-memory-files", +] + +CMP_COMPANY_ID = "42a7acd0-30c5-4cbd-ac97-7424f65df294" # 1xxx — רישוי ובניה +CMPA_COMPANY_ID = "8639e837-4c9d-47fa-a76b-95788d651896" # 8xxx — היטלי השבחה + +# Per-agent + per-company desired skills +PLAN: dict[tuple[str, str], list[str]] = { + # (agent_name, company_id) -> desired skills + ("מנתח משפטי", CMP_COMPANY_ID): PAPERCLIP_BASE_SKILLS + ["local/eba6210d5a/legal-decision"], + ("מנתח משפטי", CMPA_COMPANY_ID): PAPERCLIP_BASE_SKILLS, # CMPA has no local skills + ("הגהת מסמכים", CMP_COMPANY_ID): PAPERCLIP_BASE_SKILLS, + ("הגהת מסמכים", CMPA_COMPANY_ID): PAPERCLIP_BASE_SKILLS, +} + + +def fail(msg: str) -> None: + print(f"❌ {msg}", file=sys.stderr) + sys.exit(1) + + +async def fetch_targets() -> list[dict[str, Any]]: + """Return rows for the agents we plan to update.""" + conn = await asyncpg.connect(PAPERCLIP_DB_URL) + try: + rows = await conn.fetch( + """ + SELECT a.id, a.name, a.company_id::text as company_id, + COALESCE( + jsonb_array_length(a.adapter_config->'paperclipSkillSync'->'desiredSkills'), + 0 + ) as current_skill_count + FROM agents a + WHERE a.name IN ('מנתח משפטי', 'הגהת מסמכים') + ORDER BY a.name, a.company_id + """ + ) + finally: + await conn.close() + return [dict(r) for r in rows] + + +def backup_agents_table() -> Path: + BACKUP_DIR.mkdir(parents=True, exist_ok=True) + stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") + out = BACKUP_DIR / f"agents-pre-skill-sync-{stamp}.sql" + env = {**os.environ, "PGPASSWORD": "paperclip"} + subprocess.run( + ["pg_dump", "-h", "127.0.0.1", "-p", "54329", "-U", "paperclip", + "-d", "paperclip", "-t", "agents", "--data-only", "-f", str(out)], + check=True, env=env, + ) + return out + + +async def call_skill_sync(agent_id: str, desired_skills: list[str]) -> tuple[int, dict[str, Any]]: + """Call POST /api/agents/{id}/skills/sync with the desired skills list.""" + if not PAPERCLIP_BOARD_API_KEY: + fail("PAPERCLIP_BOARD_API_KEY not set — needed for /api/agents/.../skills/sync") + url = f"{PAPERCLIP_API_URL}/api/agents/{agent_id}/skills/sync" + headers = { + "Authorization": f"Bearer {PAPERCLIP_BOARD_API_KEY}", + "X-Paperclip-Run-Id": "", + "Content-Type": "application/json", + } + body = {"desiredSkills": desired_skills} + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.post(url, headers=headers, json=body) + try: + data = resp.json() + except Exception: + data = {"raw": resp.text[:500]} + return resp.status_code, data + + +async def main() -> None: + p = argparse.ArgumentParser() + g = p.add_mutually_exclusive_group(required=True) + g.add_argument("--dry-run", action="store_true", help="Show plan, do not apply") + g.add_argument("--apply", action="store_true", help="Actually call the skill-sync API") + g.add_argument("--verify", action="store_true", help="Show current state only") + args = p.parse_args() + + targets = await fetch_targets() + if len(targets) != 4: + fail(f"Expected 4 target rows (2 agents × 2 companies), got {len(targets)}") + + # Build a map for plan + by_key = {(r["name"], r["company_id"]): r for r in targets} + + print(f"\n=== Targets in DB ({len(targets)} rows) ===") + for r in targets: + company_label = "1xxx CMP" if r["company_id"] == CMP_COMPANY_ID else "8xxx CMPA" + print(f" {r['name']:14s} | {company_label} | id={r['id']} | currently {r['current_skill_count']} skills") + + print(f"\n=== Plan ===") + for (agent_name, company_id), desired in PLAN.items(): + company_label = "1xxx CMP" if company_id == CMP_COMPANY_ID else "8xxx CMPA" + target = by_key.get((agent_name, company_id)) + if not target: + print(f" ❌ {agent_name} in {company_label}: NOT FOUND in DB") + continue + print(f" {agent_name:14s} | {company_label} | will set {len(desired)} skills:") + for s in desired: + print(f" - {s}") + + if args.verify: + print("\n(verify mode — exiting without changes)") + return + if args.dry_run: + print("\n(dry-run mode — exiting without changes)\nRe-run with --apply to execute.") + return + + # APPLY mode + print(f"\n=== Backup ===") + backup_path = backup_agents_table() + print(f" ✓ Backed up agents table → {backup_path}") + + print(f"\n=== Applying skill-sync via API ===") + failures = [] + for (agent_name, company_id), desired in PLAN.items(): + target = by_key.get((agent_name, company_id)) + if not target: + failures.append(f"{agent_name} in {company_id}: not found") + continue + status, data = await call_skill_sync(target["id"], desired) + if status >= 400: + failures.append(f"{agent_name} ({company_id[:8]}...): HTTP {status} — {json.dumps(data)[:200]}") + print(f" ❌ {agent_name} ({target['id']}): HTTP {status}") + else: + new_count = len(data.get("desiredSkills") or data.get("skills") or []) + print(f" ✓ {agent_name} ({target['id']}): HTTP {status} (now {new_count or len(desired)} skills)") + + if failures: + print(f"\n=== ⚠️ {len(failures)} failures ===") + for f in failures: + print(f" - {f}") + print(f"\nRollback: psql ... -f {backup_path}") + sys.exit(1) + + # Verify + print(f"\n=== Post-apply verification ===") + final = await fetch_targets() + for r in final: + company_label = "1xxx CMP" if r["company_id"] == CMP_COMPANY_ID else "8xxx CMPA" + emoji = "✓" if r["current_skill_count"] >= 4 else "❌" + print(f" {emoji} {r['name']:14s} | {company_label} | now {r['current_skill_count']} skills") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/skills/new-company-setup/SKILL.md b/skills/new-company-setup/SKILL.md new file mode 100644 index 0000000..3a53313 --- /dev/null +++ b/skills/new-company-setup/SKILL.md @@ -0,0 +1,316 @@ +--- +name: new-company-setup +description: מדריך מלא להוספת חברה (board) חדשה במערכת legal-ai + Paperclip — יוצר את כל הרכיבים הנדרשים: companies row ב-Paperclip, 7 סוכנים (CEO + 6 specialists), runtime/adapter config, paperclipSkillSync, instructionsBundleMode, budget, plugin_state mappings, ועדכון קוד legal-ai. השתמש ב-skill זה כאשר המשתמש מבקש להוסיף סוג ערר חדש (למשל 5xxx, 7xxx) או להפריד תחום קיים לחברה משלו. ה-skill מכיל את כל ההגדרות שנקבעו ב-Gaps #16, #17, #19, #21, #22, #24, #25, #28 — אסור להחסיר שלב. +--- + +# הקמת חברה חדשה — Blueprint מלא + +> **קונטקסט**: עד 2026-05-04 יש לנו 2 חברות (CMP=1xxx רישוי, CMPA=8xxx היטל השבחה + 9xxx פיצויים). הוספת חברה שלישית (לדוגמה 5xxx, 7xxx) דורשת 11 שלבים בסדר מסוים. ה-skill הזה מכיל את כל הלקחים מ-Gap analysis ועדכוני 2026-04 → 2026-05. + +## רקע — ארכיטקטורה דו-חברתית +מקור החברות: Paperclip מחייב `agents.company_id NOT NULL` — אין shared agents. לכן כל סוג ערר מקבל company משלו ב-Paperclip, עם סט מלא של 7 סוכנים. החברה הראשונה (CMP) היא **master** — שינויים בה מסונכרנים אוטומטית ל-mirrors דרך `scripts/sync_agents_across_companies.py`. + +**מודל מומלץ לחברה חדשה**: להפוך אותה ל-mirror של CMP במבנה — כל הסוכנים זהים, רק `company_id`, `id`, `reports_to` שונים. ככה הסקריפט הקיים יסנכרן אוטומטית. + +--- + +## ⚠️ לפני שמתחילים — checklist הבנה + +לפני שמרצים אף פקודה, ודא שאתה יודע: + +- [ ] **מספרי תיקים** של החברה החדשה (לדוגמה: 5xxx, 7xxx) — חייב להיות disjoint מ-1xxx/8xxx/9xxx +- [ ] **שם בעברית** של הוועדה (לדוגמה: "ועדת ערר לתכנון ובניה צפון") +- [ ] **prefix לidentifiers** של issues (לדוגמה: `CMPN`) +- [ ] **`appeal_type` tag** — מחרוזת קצרה לניתוב (לדוגמה: `licensing_north`) +- [ ] **המודלים והעלויות** — האם זהה ל-CMP (Opus opus-4-6 ל-CEO+writer, Sonnet sonnet-4-6 לאחרים)? +- [ ] **גישה ל-Infisical** ל-`PAPERCLIP_BOARD_API_KEY` (`/paperclip` ב-nautilus env) +- [ ] **PostgreSQL access** ל-Paperclip DB (`localhost:54329`, user `paperclip`) + +--- + +## שלב 1 — יצירת `companies` row ב-Paperclip DB + +```sql +INSERT INTO companies ( + id, name, issue_prefix, status, + attachment_max_bytes, + require_board_approval_for_new_agents, + hire_approval_required +) VALUES ( + gen_random_uuid(), + 'ועדת ערר {שם}', -- בעברית + 'CMPN', -- 4 תווים אנגלית, ייחודי + 'active', + 10485760, -- 10MB default + false, -- ברירת מחדל מ-2026.428.0 (Gap docs) + false +) +RETURNING id; +``` + +**שמור את ה-UUID** — תצטרך אותו ב-כל השלבים הבאים. נקרא לו `$NEW_COMPANY_ID`. + +⚠️ אל תיצור project ראשוני ידנית — Paperclip יוצר אוטומטית כשהחברה נשמרת. + +--- + +## שלב 2 — יצירת 7 סוכנים + +צור את הסוכנים בסדר הבא (ה-CEO ראשון, כי בכל הסוכנים `reports_to = CEO_id`): + +| # | name (עברית) | role | model | budget_cents | +|---|---------------|------|-------|---------------| +| 1 | עוזר משפטי | `ceo` | claude-opus-4-6 | 1500 | +| 2 | מנתח משפטי | `researcher` | claude-opus-4-6 | 1500 | +| 3 | חוקר תקדימים | `researcher` | claude-sonnet-4-6 | 1500 | +| 4 | כותב החלטה | `engineer` | claude-opus-4-6 | 1500 | +| 5 | בודק איכות | `qa` | claude-sonnet-4-6 | 1500 | +| 6 | מייצא טיוטה | `engineer` | claude-sonnet-4-6 | 1500 | +| 7 | הגהת מסמכים | `engineer` | claude-opus-4-6 | 1500 | + +### דרך 1 — sync from master (מומלץ) + +הדרך הקלה ביותר: צור 7 סוכנים ב-CMPN עם **שמות זהים** ל-CMP, ואז הרץ `sync_agents_across_companies.py` שיעתיק את כל ההגדרות. + +```sql +-- לכל אחד מ-7 הסוכנים (שנה את name ו-role בכל פעם): +INSERT INTO agents ( + id, company_id, name, role, adapter_type, + adapter_config, runtime_config, budget_monthly_cents, + permissions, status +) VALUES ( + gen_random_uuid(), + '{NEW_COMPANY_ID}'::uuid, + 'עוזר משפטי', -- שנה ב-7 שורות + 'ceo', -- שנה לפי הטבלה למעלה + 'claude_local', + '{}'::jsonb, -- ייטען בשלב 4 + '{}'::jsonb, -- ייטען בשלב 4 + 1500, + '{}'::jsonb, + 'idle' +) +RETURNING id, name; +``` + +שמור את 7 ה-UUIDs לטבלה לעיון מהיר. + +### עדכון `reports_to` (אחרי שיש לך CEO_id) + +```sql +UPDATE agents +SET reports_to = '{CEO_id}'::uuid +WHERE company_id = '{NEW_COMPANY_ID}'::uuid + AND name <> 'עוזר משפטי'; +``` + +--- + +## שלב 3 — סנכרון מ-CMP (master) דרך הסקריפט + +```bash +PAPERCLIP_BOARD_API_KEY=$(mcp__infisical__get-secret \ + projectId=9a77b161-f70c-4dd3-9d67-b7ab850cef51 \ + environmentSlug=nautilus secretPath=/ \ + secretName=PAPERCLIP_BOARD_API_KEY) \ +python ~/legal-ai/scripts/sync_agents_across_companies.py --verify +``` + +**אם הסקריפט לא תומך ב-mirror החדש**, יש לעדכן אותו: +1. פתח `scripts/sync_agents_across_companies.py` +2. השתמש בdict structure: master_company → list of mirrors. או הוסף flag `--target-company` +3. הרץ `--apply` להעתיק את כל ההגדרות מ-CMP ל-CMPN + +**מה הסקריפט מסנכרן** (אוטומטית): +- `adapter_config`: model, effort, timeoutSec=3600, maxTurnsPerRun=500, instructionsBundleMode=external, instructionsRootPath/EntryFile, dangerouslySkipPermissions, extraArgs (`--agent legal-{role}`), cwd +- `runtime_config.heartbeat`: graceSec=60, cooldownSec=10, wakeOnDemand=true, maxConcurrentRuns (CEO=2, others=1) +- `budget_monthly_cents` (1500) +- `metadata`, `icon`, `title` + +**מה לא מסונכרן** (חייב לעשות ידנית בהמשך): +- `paperclipSkillSync.desiredSkills` — ראה שלב 4 +- `permissions` — לפי policy של החברה +- local skills (אם החברה החדשה צריכה custom skills) + +--- + +## שלב 4 — Paperclip Skills + +הסקריפט מ-שלב 3 כולל כבר את ה-`paperclipSkillSync.desiredSkills` (מסונן לפי skills זמינים ב-mirror). אבל ה-mirror החדש **לא יקבל local skills** של CMP אם הם לא קיימים גם בו. + +### 4א. יצירת company_skills ל-CMPN + +```sql +-- העתק את 6 ה-paperclip skills הסטנדרטיים מ-CMP ל-CMPN +INSERT INTO company_skills (company_id, key, slug, name, description, markdown, source_type, trust_level, compatibility, file_inventory) +SELECT + '{NEW_COMPANY_ID}'::uuid, + key, slug, name, description, markdown, source_type, trust_level, compatibility, file_inventory +FROM company_skills +WHERE company_id = '42a7acd0-30c5-4cbd-ac97-7424f65df294' -- CMP + AND key LIKE 'paperclipai/paperclip/%'; +``` + +### 4ב. אם החברה צריכה local skills + +החלט אילו local skills (`local/.../legal-decision`, `local/.../attach-precedents`) רלוונטיות — תלוי בסוג הערר. +לדוגמה, חברה ל-"היטלי השבחה צפון" כנראה לא תצריך `attach-precedents` של CMP אלא local skill משלה. + +### 4ג. הפעלת skills/sync לכל סוכן + +הרץ `scripts/sync_missing_agent_skills.py` עם adaptation לחברה החדשה (העתק את הקובץ ושנה את `CMPA_COMPANY_ID` ל-NEW_COMPANY_ID + רשימת ה-skills הרצויה). + +⚠️ **חובה דרך API** (`POST /api/agents/{id}/skills/sync`) — לא דרך SQL ישיר! ה-API יוצר revision מסוג `skill-sync` שנדרש לlogging. SQL ישיר לא יוצר revision. + +--- + +## שלב 5 — Symlinks ל-instructions (managed by Paperclip) + +לכל סוכן Paperclip צופה לקבצי הוראות בנתיב: +`~/.paperclip/instances/default/companies/{COMPANY_ID}/agents/{AGENT_ID}/instructions/` + +```bash +NEW_COMPANY_ID="..." +LEGAL_AI_AGENTS=/home/chaim/legal-ai/.claude/agents + +for ROW in \ + "ceo:legal-ceo.md" \ + "analyst:legal-analyst.md" \ + "researcher:legal-researcher.md" \ + "writer:legal-writer.md" \ + "qa:legal-qa.md" \ + "exporter:legal-exporter.md" \ + "proofreader:legal-proofreader.md"; do + ROLE="${ROW%%:*}" + FILE="${ROW##*:}" + AGENT_ID=$(PGPASSWORD=paperclip psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -tAc \ + "SELECT id FROM agents WHERE company_id='$NEW_COMPANY_ID'::uuid AND adapter_config->>'extraArgs' LIKE '%legal-$ROLE%' LIMIT 1") + DEST=~/.paperclip/instances/default/companies/$NEW_COMPANY_ID/agents/$AGENT_ID/instructions/ + mkdir -p $DEST + ln -sf "$LEGAL_AI_AGENTS/$FILE" "$DEST/AGENTS.md" + ln -sf "$LEGAL_AI_AGENTS/HEARTBEAT.md" "$DEST/HEARTBEAT.md" +done +``` + +**אימות:** `ls -la ~/.paperclip/instances/default/companies/$NEW_COMPANY_ID/agents/*/instructions/` — צריך לראות 14 symlinks (7 agents × 2 קבצים). + +--- + +## שלב 6 — עדכון `web/paperclip_client.py` + +הקובץ מכיל 3 dicts שצריכים את החברה החדשה: + +```python +# COMPANIES dict +COMPANIES = { + "licensing": "42a7acd0-30c5-4cbd-ac97-7424f65df294", + "betterment": "8639e837-4c9d-47fa-a76b-95788d651896", + "{appeal_type_new}": "{NEW_COMPANY_ID}", # ← חדש +} + +# CEO_AGENTS dict — נדרש ל-wakeup routing +CEO_AGENTS = { + COMPANIES["licensing"]: "752cebdd-...", + COMPANIES["betterment"]: "cdbfa8bc-...", + COMPANIES["{appeal_type_new}"]: "{CEO_ID_NEW}", # ← חדש +} + +# _FALLBACK_APPEAL_TYPE_TO_COMPANY — ניתוב לפי tag עברי/אנגלי +_FALLBACK_APPEAL_TYPE_TO_COMPANY = { + # קיימים... + "{שם בעברית}": COMPANIES["{appeal_type_new}"], + "{english_tag}": COMPANIES["{appeal_type_new}"], +} +``` + +⚠️ אחרי השינוי — **deploy** ל-Coolify (FastAPI container חי במכולה — שינוי קוד דורש rebuild). ראה `legal-ai/CLAUDE.md`. + +--- + +## שלב 7 — `tag_company_mappings` ב-legal-ai DB + +ה-FastAPI ראשית מנסה לקרוא ניתוב מ-DB, רק אז fallback ל-dict הקבוע (שלב 6). הוסף mapping: + +```sql +-- ב-legal-ai DB (port 5433) +INSERT INTO tag_company_mappings (tag, company_id) VALUES + ('{שם עברי}', '{NEW_COMPANY_ID}'), + ('{english_tag}', '{NEW_COMPANY_ID}'); +``` + +--- + +## שלב 8 — עדכון `HEARTBEAT.md` §1 + +הסעיף §1 מכיל טבלה של חברות + CEO IDs. הוסף שורה חדשה: + +```markdown +| ועדת ערר {שם} (CMPN) | `{NEW_COMPANY_ID}` | {סוג} | **{Nxxx}** | `{CEO_ID_NEW}` | +``` + +ובסעיף §4ג (CEO wakeup), עדכן את ה-`if` להוסיף אופציה שלישית לחברה החדשה. + +--- + +## שלב 9 — עדכון `legal-ai/CLAUDE.md` + +הקובץ מכיל את אותה טבלה. עדכן בקטעים: +- "סוגי עררים" (אם קיים) +- "Paperclip — כללי אינטגרציה קריטיים" → "ניתוב comments דרך CEO" + +--- + +## שלב 10 — Hebrew translation (אם נדרש) + +אם שם החברה מופיע ב-UI, ייתכן שצריך תרגום ב-`~/.paperclip/hebrew/translate-he.js`. בד"כ לא נדרש — שמות בעברית כבר. + +```bash +# אחרי שינויים בHebrew file: +~/.paperclip/hebrew/apply-hebrew.sh +# ⚠️ לא דורש pm2 restart — UI client-side fix. +``` + +--- + +## שלב 11 — בדיקה end-to-end + +1. **CEO מתעורר על comment**: צור issue test בחברה החדשה, פרסם comment, ודא ש-CEO רץ. +2. **plugin marcusgroup.legal-ai רואה את החברה**: ב-Paperclip UI → Settings → Plugins → marcusgroup.legal-ai → ודא שהחברה החדשה ב-installed companies. +3. **MCP tools פועלים**: דרך Claude Code, הרץ `mcp__legal-ai__case_create` עם appeal_type של החברה החדשה. +4. **Sync script עובד**: `python scripts/sync_agents_across_companies.py --verify` — לא צריך drift. +5. **Budget enforcement**: צור cost_event מבחן, ודא ש-spent_monthly_cents מתעדכן. + +--- + +## ⚠️ מלכודות מתועדות (מ-Gap analysis 2026-04 → 2026-05) + +מבחן בכל שלב מאפשר תפיסת issues שתועדו בעבר: + +| # | מלכודת | פתרון | +|---|---------|--------| +| 1 | סוכנים בלי `paperclipSkillSync` | ראה שלב 4ג (POST /api/agents/{id}/skills/sync, לא SQL) | +| 2 | `runtime_config = '{}'` (default → graceSec=1ms!) | ראה שלב 3 (סקריפט מסנכרן `heartbeat.graceSec=60`) | +| 3 | `budget_monthly_cents = 0` | ראה שלב 2 (insert עם 1500) | +| 4 | `instructionsBundleMode` חסר | ראה שלב 3 (סקריפט מסנכרן `external` + Root + EntryFile) | +| 5 | `bootstrapPromptTemplate` deprecated | אין אצלנו — דלג | +| 6 | drift בין חברות | ראה שלב 3 — סנכרון אוטומטי כל שינוי הגדרות | +| 7 | CEO לא מתעורר על comment | ודא ש-`reports_to` עודכן ושיש symlinks ל-AGENTS.md (שלב 5) | +| 8 | `psql` ישיר ל-`issue_attachments` | אסור — ראה `HEARTBEAT.md §2` (heartbeat-context API) | +| 9 | curl ישיר ל-Paperclip API | אסור — תמיד `pc.sh` (`HEARTBEAT.md §0`) | +| 10 | "@chaim — ענה 1/2/3 בcomment" | אסור — interactions API (`legal-ceo.md §B/§C/§D`) | + +--- + +## רפרנסים + +- [`docs/new-company-setup-guide.md`](../../docs/new-company-setup-guide.md) — היסטוריית הקמת CMPA (חברה שנייה, 2026-04) +- [`scripts/sync_agents_across_companies.py`](../../scripts/sync_agents_across_companies.py) — אוטומציה לסנכרון +- [`scripts/sync_missing_agent_skills.py`](../../scripts/sync_missing_agent_skills.py) — תבנית להפעלת skills/sync +- [`~/.paperclip/CUSTOMIZATIONS.md`](../../../.paperclip/CUSTOMIZATIONS.md) — כל ההתאמות הפעילות (סעיפים: agents runtime, instructions, budgets, interactions, skill-sync) +- [`HEARTBEAT.md`](../../.claude/agents/HEARTBEAT.md) — §1 טבלת חברות (לעדכן בשלב 8) +- [`legal-ai/CLAUDE.md`](../../CLAUDE.md) — Paperclip integration rules + +--- + +## גרסה +- 2026-05-04 — גרסה ראשונה (אחרי Gap #16-#28)