Files
legal-ai/docs/spec/X2-multi-company.md
2026-05-30 16:47:19 +00:00

158 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# X2 — מודל רב-החברתי וכללי ה-Sync (Multi-Company & Sync)
קובץ-תחום זה כפוף ל-[חוקת המערכת](00-constitution.md) והוא ה-deep-dive על **המבנה הרב-חברתי**
של עוזר משפטי — שתי החברות (CMP/CMPA), 14 הסוכנים, ואיך שינוי-הגדרות מפושט מ-Master ל-Mirror.
הוא אוכף את [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) (מקור-אמת
יחיד — אין מסלולים מקבילים מתפצלים) בהקשר של תצורת-סוכנים: שתי החברות הן שתי העתקות של אותה
מערכת, ואסור להן להתפצל (drift).
> **invariant פרויקטלי-תפעולי.** ה-invariants כאן הם **עובדות על איך המערכת *הזו* מנוהלת**
> רב-חברתית — לא תאוריה הנדסית כללית ולא תוכן משפטי. אין סמכות חיצונית ל"איך מסנכרנים
> CMP↔CMPA"; לכן הם נושאים שדה `מקור-סמכות` = הראנבוקים והקוד של הפרויקט עצמו ([CLAUDE.md](../../CLAUDE.md),
> [HEARTBEAT.md](../../.claude/agents/HEARTBEAT.md), [scripts/sync_agents_across_companies.py](../../scripts/sync_agents_across_companies.py))
> — **לא** ≥3 מקורות חיצוניים ו**ללא** סטטוס verified/UNVERIFIED. אבל כל invariant **נקשר
> לעיקרון הגלובלי שהוא משרת**: כלל אי-ה-drift הוא מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים).
---
## 1. שתי החברות: Master מול Mirror
Paperclip מחייב `agents.company_id NOT NULL` — אין סוכנים משותפים. כדי לשרת את שני סוגי
העררים, המערכת מורצת כ**שתי חברות** נפרדות, כל אחת עם מערך-סוכנים מלא משלה:
| ממד | CMP — **Master** | CMPA — **Mirror** |
|------|------------------|-------------------|
| תפקיד | מקור-האמת לתצורת-סוכנים | העתקה מסונכרנת מ-Master |
| COMPANY_ID | `42a7acd0-30c5-4cbd-ac97-7424f65df294` | `8639e837-4c9d-47fa-a76b-95788d651896` |
| סוגי תיקים | רישוי ובנייה | היטל השבחה + פיצויים ס'197 |
| טווח-מספרים | **1xxx** | **8xxx, 9xxx** |
| CEO Agent ID | `752cebdd-6748-4a04-aacd-c7ab0294ef33` | `cdbfa8bc-3d61-41a4-a2e7-677ec7d34562` |
(המקור: [HEARTBEAT.md §1](../../.claude/agents/HEARTBEAT.md), שורות 3844; מזהי-החברות מקודדים גם
ב-[sync_agents_across_companies.py:62-63](../../scripts/sync_agents_across_companies.py).)
**14 סוכנים = 7 × 2.** כל חברה מחזיקה את אותם 7 תפקידי-סוכן (CEO, writer, analyst, researcher,
qa, proofreader, exporter — ראה [X4-agents.md](X4-agents.md)). מאחר ש-`company_id` הוא `NOT NULL`,
כל תפקיד מיוצג בשתי **רשומות-סוכן נפרדות** — אחת ל-CMP, אחת ל-CMPA. אין רשומה משותפת.
**Master = CMP, Mirror = CMPA.** התצורה נכתבת ומתוחזקת בחברת ה-Master (CMP, 1xxx), והסנכרון
הוא **חד-כיווני** CMP → CMPA ([sync...py:1-7,361-362](../../scripts/sync_agents_across_companies.py)).
---
## 2. ניתוב לפי חברה — סינון ב-`company_id`
הזרימה התפעולית נאכפת לפי `$PAPERCLIP_COMPANY_ID` של הסוכן הפועל ([HEARTBEAT.md §1](../../.claude/agents/HEARTBEAT.md)):
- `42a7acd0…` → הסוכן מטפל **רק** בתיקי 1xxx; `8639e837…`**רק** בתיקי 8xxx/9xxx (שורות 4344).
- **אסור** ליצור פרויקט/issue/תוכן לתיק מחוץ לטווח-החברה (שורה 45); issue שמכוון לתיק מחוץ
לטווח → סירוב מנומס ב-comment + העֵרת ה-CEO של החברה הנכונה (שורה 46).
- **CEO שונה לכל חברה** — בחירת ה-CEO ל-wakeup נגזרת מ-`$PAPERCLIP_COMPANY_ID`, **לעולם לא**
UUID hardcoded ([HEARTBEAT.md §4ג](../../.claude/agents/HEARTBEAT.md), שורות 143150).
- **גבול-חברה נאכף בצד-Paperclip:** wakeup לחברה אחרת נדחה — `Agent key cannot access another
company` ([HEARTBEAT.md §4ג](../../.claude/agents/HEARTBEAT.md), שורה 157).
---
## 3. כלל ה-Sync — אחרי כל שינוי-הגדרות ב-Master
> **טריגר:** כל שינוי ב-`adapter_config`, `runtime_config`, `budget_monthly_cents`, או skills
> של סוכן ב-Master (UI / SQL / API). מקור: סעיף "Cross-company agent sync" ב-[legal-ai/CLAUDE.md](../../CLAUDE.md)
> וב-[root CLAUDE.md](../../../CLAUDE.md).
הפעולה החובה — קודם בדיקה, אז החלה:
```bash
PAPERCLIP_BOARD_API_KEY=$(…infisical…) \
python ~/legal-ai/scripts/sync_agents_across_companies.py --verify # drift report
PAPERCLIP_BOARD_API_KEY=$(…) \
python ~/legal-ai/scripts/sync_agents_across_companies.py --apply # backup + apply
```
**מה הסקריפט עושה** (מאומת מול הקוד):
- **חד-כיווני CMP → CMPA**, סינכרון של שדות-תצורה מוגדרים: top-level (`budget_monthly_cents`,
`metadata`, `icon`, `title`, `role`), מפתחות `adapter_config` נבחרים (`model`, `effort`,
`timeoutSec`, `maxTurnsPerRun`, נתיבי-instructions, `cwd`…), ו-`runtime_config` כ-full-replace
([sync...py:66-75,124-160](../../scripts/sync_agents_across_companies.py)). שדות פר-חברה
(`id`, `company_id`, `adapter_type`, `agent_api_keys`, `status`, `spent_monthly_cents`,
`permissions`) **אינם** מסונכרנים ([sync...py:24-29](../../scripts/sync_agents_across_companies.py)).
- **מבוסס-API, לא DB ישיר.** ה-PATCH דרך `PATCH /api/agents/{id}` וה-skills דרך
`POST /api/agents/{id}/skills/sync` עם `Authorization: Bearer` ([sync...py:204-237](../../scripts/sync_agents_across_companies.py)).
- **מסנן skills מקומיים שלא קיימים ב-Mirror.** `desiredSkills` מושוות כ-subset; skills מקומיים
של CMP (למשל `local/eba6210d5a/legal-decision`) שלא קיימים ב-CMPA נשמטים עם אזהרה
([sync...py:138-154,194-195](../../scripts/sync_agents_across_companies.py)).
- **יוצר revisions.** סנכרון skills עובר דרך endpoint ייעודי שמייצר `skill-sync` revision
([sync...py:277-284](../../scripts/sync_agents_across_companies.py)).
- **idempotent + אל-כשל.** `--verify`/`--dry-run` כברירת-מחדל, גיבוי `pg_dump` לפני `--apply`,
pre-flight על קבצי-instructions, ו-re-verify אוטומטי אחרי ההחלה ([sync...py:9,163-173,408-465](../../scripts/sync_agents_across_companies.py)).
- **מדלג על סוכן עם `adapter_type` שונה בין החברות.** אם ל-Master ול-Mirror `adapter_type`
שונה → `SKIPPING`, ללא סנכרון ([sync...py:387-389](../../scripts/sync_agents_across_companies.py)).
זו המלכודת ב-INV-MC1 (להלן).
---
## 4. Invariants של התחום (פרויקטלי-תפעולי)
### INV-MC1: תצורת-סוכן ב-Master מפושטת ל-Mirror — אין drift בין החברות
**כלל:** כל שינוי ב-`adapter_config` / `runtime_config` / `budget_monthly_cents` / skills של
סוכן בחברת ה-Master (CMP) **חייב** להיות מפושט ל-Mirror (CMPA) דרך סקריפט ה-Sync המבוסס-API
(`--verify` ואז `--apply`). שתי החברות **לא מתפצלות** — הן שתי העתקות מסונכרנות של אותה תצורה
(מופע של [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים) — מקור-אמת
יחיד, אין מסלולים מקבילים מתפצלים; וכלל-ההנדסה "סימטריה", [חוקה §6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
**מקור-סמכות:** סעיף "Cross-company agent sync" ב-[legal-ai/CLAUDE.md](../../CLAUDE.md) +
ב-[root CLAUDE.md](../../../CLAUDE.md) +
[scripts/sync_agents_across_companies.py](../../scripts/sync_agents_across_companies.py) +
[HEARTBEAT.md §1, §4ג](../../.claude/agents/HEARTBEAT.md). (invariant פרויקטלי-תפעולי — ללא
פרוטוקול ≥3-המקורות; משרת את העיקרון הגלובלי G2.)
**אכיפה:** סקריפט ה-Sync (idempotent, מבוסס-API, גיבוי+re-verify) — מורץ **ידנית** אחרי כל
שינוי-תצורה ב-Master. **אין אכיפה אוטומטית** (ראה §5).
**הפרה ידועה:** הסקריפט **מדלג** על סוכן ש-`adapter_type` שונה בין CMP ל-CMPA
([sync...py:387-389](../../scripts/sync_agents_across_companies.py)). כשמעבירים סוכן ל-`deepseek_local`
ב-Master, ה-Mirror נשאר על ה-adapter הישן והסנכרון מדלג עליו — **חובה להחיל את שינוי ה-`adapter_type`
ידנית בשתי החברות לפני הרצת ה-Sync** ([CLAUDE.md "External adapters — deepseek_local"](../../CLAUDE.md)),
אחרת נוצר drift שקט באותו סוכן.
### INV-MC2: אין סוכן משותף — רשומה נפרדת לכל חברה
**כלל:** סוכן **לעולם אינו רשומה משותפת** בין החברות. כל אחד מ-7 התפקידים מיוצג בשתי
רשומות-סוכן נפרדות (CMP + CMPA), שכן Paperclip מחייב `agents.company_id NOT NULL`. הסנכרון
מעתיק *ערכי-תצורה* בין שתי רשומות — לא ממזג אותן לרשומה אחת (תואם [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים):
מקור-אמת יחיד לתצורה, גם כשהיא משוכפלת על פני רשומות).
**מקור-סמכות:** סעיף "Cross-company agent sync" ב-[legal-ai/CLAUDE.md](../../CLAUDE.md) (14 agents = 7 × 2;
`agents.company_id NOT NULL`) + [sync...py:4-7,83-103](../../scripts/sync_agents_across_companies.py)
(שולף מערכי-סוכן נפרדים לכל `company_id`) + [HEARTBEAT.md §1](../../.claude/agents/HEARTBEAT.md).
(invariant פרויקטלי-תפעולי.)
**אכיפה:** אילוץ `company_id NOT NULL` בצד-Paperclip; הסקריפט מתאים סוכנים בין החברות לפי
`name` ולעולם לא יוצר רשומה משותפת ([sync...py:372,383-385](../../scripts/sync_agents_across_companies.py)
— "we never auto-create").
**הפרה ידועה:** —
---
## 5. מצב קיים מול יעד — פער אכיפה
ה-Sync הוא **ידני ולא-נאכף**. הסקריפט עצמו בנוי "אל-כשל" (dry-run כברירת-מחדל, גיבוי,
re-verify), אך **שום מנגנון לא מכריח** הרצה אחרי שינוי-תצורה ב-Master:
- **drift אם שוכחים.** שינוי `adapter_config`/`runtime_config`/budget/skills ב-CMP בלי הרצת
`--apply` משאיר את CMPA מאחור — שתי החברות מתפצלות בשקט, בניגוד ל-INV-MC1. **יעד:** טריגר/
בדיקת-בריאות תקופתית שמריצה `--verify` ומדווחת drift (היום ההרצה תלויה בזיכרון המפעיל).
- **מלכודת `adapter_type`-skip.** סוכן עם `adapter_type` שונה בין החברות נשמט מהסנכרון
([sync...py:387-389](../../scripts/sync_agents_across_companies.py)) — ה-`--verify` ידווח
`SKIPPING`, אך אם המפעיל לא יחיל את שינוי ה-adapter ידנית בשתי החברות, הסוכן יישאר drifted.
**יעד:** אזהרת-SKIPPING שמתבלטת ב-report + צ'קליסט-ידני (כבר מתועד ב-[CLAUDE.md](../../CLAUDE.md)).
---
## 6. הפניות-אחיות
- [00-constitution.md](00-constitution.md) — [G2](00-constitution.md#inv-g2-מקור-אמת-יחיד--אין-מסלולים-מקבילים-מתפצלים)
(מקור-אמת יחיד, אין מסלולים מקבילים מתפצלים) + כלל-ההנדסה "סימטריה" ([§6](00-constitution.md#6-כללי-הנדסה-מונעים-הישנות)).
- [X4-agents.md](X4-agents.md) — מפת 7 תפקידי-הסוכן שמשוכפלים על פני שתי החברות.
- [X3-integration-deploy.md](X3-integration-deploy.md) — Paperclip (wakeup, ניתוב comments) ו-deploy;
ה-wakeup-per-company משלים את הניתוב כאן.
- [scripts/sync_agents_across_companies.py](../../scripts/sync_agents_across_companies.py) — מימוש ה-Sync.
- [legal-ai/CLAUDE.md](../../CLAUDE.md) + [root CLAUDE.md](../../../CLAUDE.md) — סעיף
"Cross-company agent sync" + "External adapters — deepseek_local" (מלכודת ה-adapter_type).
- [.claude/agents/HEARTBEAT.md](../../.claude/agents/HEARTBEAT.md) — §1 (סינון-חברה) + §4ג (wake CEO לפי חברה).