30 Commits

Author SHA1 Message Date
a2fc36d65f fix: recognize extended chair-position placeholders as empty
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m35s
The legal-analyst agent was generating a longer placeholder form
[ימולא ע"י יו"ר הוועדה — עמדה/הנחיה לגבי סוגיה זו שתשמש את סוכן הכתיבה]
which _is_placeholder() did not match (substring check fails because ] is
further along in the longer form). Result: UI showed "✓ עמדה נקבעה" (green)
for all 4 issues even though no chair direction had been entered.

Fixes:
1. research_md.py: add regex fallback — any text starting with [ימולא is a placeholder
2. legal-analyst.md: template now emits the standard short placeholder only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:59:13 +00:00
653f441e99 docs: update agent audit report — mark all 12 issues resolved
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 7s
- עדכון טבלת מצב: כל המודלים מסונכרנים (instructions = DB)
- החלפת טבלת בעיות בטבלת סטטוס תיקונים עם commit references
- הוסף טבלת שינויים נוספים מהסשן
- הערה: Skills CMPA=6 עיצוב מכוון, verify מאשר "0 need sync"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:57:54 +00:00
c3ce0e7e1f upgrade: upgrade opus-4-6 → opus-4-7 for all heavy-reasoning agents
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 8s
DB: עדכון 8 סוכנים (CMP + CMPA) — CEO, מנתח, כותב, מגיה
instructions: עדכון 4 קבצי הנחיות להתאמה ל-DB

opus-4-7 מחליף opus-4-6 לכל הסוכנים שדורשים reasoning כבד.
sonnet-4-6 נשאר ל-QA, חוקר, מייצא. deepseek-v4-pro נשאר לcurator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:42:33 +00:00
1608ea5ed0 fix: medium/low audit items — model drift, placeholders, corpus check, curator ownership
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 7s
Model drift (instructions → match DB):
- CEO: claude-sonnet-4-6 → claude-opus-4-6 (DB runs opus; CEO needs opus quality)
- מנתח/כותב/מגיה: claude-opus-4-7 → claude-opus-4-6 (DB runs 4-6; no 4-7 in adapter)

legal-proofreader.md:
- {issue-id} placeholder → $PAPERCLIP_TASK_ID בשני המקומות (done + blocked)

legal-researcher.md:
- הוסף reference ל-HEARTBEAT.md בראש הקובץ

legal-qa.md:
- הבהרת שיטת בדיקת corpus_queries_logged: grep ידני בלבד, לא validate_decision

CLAUDE.md (curator):
- הוסף תהליך אישור הצעות curator: comment → חיים מאשר → commits ל-SKILL.md/lessons.md

maxConcurrentRuns CEO: כבר 2 ב-DB — לא נדרש שינוי

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:35:49 +00:00
35423eafc1 fix: high-priority agent audit items — CEO hardcoded IDs + researcher search_internal_decisions
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 8s
CEO (legal-ceo.md):
- הסרת company UUID ו-project UUID קשוחים בדוגמת יצירת issue
- שימוש ב-$PAPERCLIP_COMPANY_ID לחברה
- project_id נשלף דינמית מה-issue ההורה דרך $PAPERCLIP_TASK_ID

researcher (legal-researcher.md):
- הוסף mcp__legal-ai__search_internal_decisions לרשימת tools
- הוסף סעיף 2ב.2א המסביר את ההבדל: search_decisions = דפנה בלבד;
  search_internal_decisions = כל ועדות הערר בכל המחוזות
- הוראות מתי להשתמש + אזהרת היררכיה (ועדת ערר < מחוזי)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:29:47 +00:00
a584dc3602 fix: legal-exporter — versioning, dynamic skill path, case status update
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 8s
- טיוטה-V → טיוטה-v (lowercase) בכל המקומות (שלב 4 + כללים קריטיים)
- hardcoded CMP UUID בנתיבי legal-docx SKILL → $PAPERCLIP_COMPANY_ID (תומך CMP + CMPA)
- הוסף case_update לרשימת tools
- הוסף שלב 4.5: עדכן סטטוס תיק ל-exported אחרי שמירת DOCX

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 12:14:24 +00:00
d37d03f478 docs: add comprehensive agent audit 2026-05-17
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 8s
7-agent parallel audit of all Paperclip agents (CEO, analyst,
researcher, writer, QA, exporter, proofreader, curator).

Found 12 issues including 3 critical:
- Exporter: V vs v naming mismatch in DOCX versioning
- Exporter: case.status not updated to exported after export
- Researcher: section ז missing from case 8174-24

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 11:52:32 +00:00
011555fb78 docs: update CLAUDE.md — webhook pipeline, scheduled jobs, paperclip_api.py
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 8s
- Document emit_case_status_webhook flow and plugin integration
- Document stale-case-reminder and weekly-feedback-analysis jobs
- Fix paperclip_api.py vs paperclip_client.py (both exist, api.py is current)
- Add warning: weekly-feedback-job CEO has no issueId

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 11:23:47 +00:00
ea0532b7ba fix: weekly-feedback-job handler writes to file only (no Paperclip issue)
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m39s
CEO wakes for weekly-feedback-job via agents.invoke without issueId,
so $PAPERCLIP_TASK_ID is empty. Removed steps 4-5 (comment + close
issue) from handler — now file-write only with stdout logging.

Also commits pending docs and agent instructions from prior session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 11:08:14 +00:00
cddc7c8d24 fix: start-workflow wakeup failure now returns 502 instead of silent success
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m33s
If pc_wake_ceo fails, the endpoint now raises HTTP 502 and skips the
case_update to processing — preventing cases from silently getting stuck
with no CEO running. Also adds `processing` to CEO routing table and
updates case_list docstring with full status list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 11:02:30 +00:00
83b6ff51b7 feat: fix wizard step-skip bug + extend case edit with all fields + Paperclip title sync
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m38s
- Fix keyboard navigation bug: React was reusing the submit button DOM element
  when transitioning "הבא" → "צור תיק", retaining focus and causing Enter to
  auto-submit step 3. Added key props to force element replacement.

- CaseEditDialog now covers all wizard fields: appellants, respondents,
  property_address, permit_number (in addition to existing title, subject,
  hearing_date, expected_outcome, notes).

- When case title changes, Paperclip project name is updated in background
  via new update_project_name() in paperclip_client.py.

- Extended CaseUpdateRequest, case_update MCP tool, and caseUpdateSchema
  to carry the new fields end-to-end.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 10:55:45 +00:00
8dc7a40fa2 fix: exclude exported cases from stale; add weekly-feedback-job handler to CEO
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 8s
- /api/cases/stale: exclude 'exported' status — exported cases await Dafna's
  review intentionally, they are not stuck
- legal-ceo.md: add routing for weekly-feedback-job reason + explicit handler
  (analyze feedback, update decision-lessons.md, close issue)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 10:35:39 +00:00
a3468d5b2f fix: use timezone-aware datetime in webhook timestamp
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 3m17s
Replace deprecated datetime.utcnow() with datetime.now(timezone.utc)
to avoid Python 3.12+ DeprecationWarning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 10:15:52 +00:00
5f43659b5a fix: add defensive JSON parsing in check_instructions 2026-05-16 17:53:42 +00:00
86734da210 feat: add --check-instructions, pre-flight validation, and mtime tracking to sync script
- P3-T1: --check-instructions flag + check_instructions() prints a table of all
  agents' instructionsFilePath with status ( OK /  MISSING / ⚠ NOT SET),
  size, mtime, and ⚠ DRIFT when file has changed since last sync
- P3-T2: --apply now runs a pre-flight check on master agents and aborts if any
  instruction file is missing, before touching the DB or calling any API
- P3-T3: get_claude_md_mtime() helper; --apply stamps claude_md_mtime and
  claude_md_last_synced into each mirror agent's metadata via the PATCH call
- P3-T4: alias check-agents added to ~/.bashrc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 17:51:34 +00:00
82ded005a4 fix: add days>0 guard and limit param to stale/feedback endpoints
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 8s
2026-05-16 17:38:34 +00:00
c7ed1110f8 feat: add /api/cases/stale and /api/chair-feedback/weekly-summary endpoints
GET /api/cases/stale?days=N — returns cases not updated in N days (default 3)
  that are not in 'final' or 'new' status, with days_stale count.
GET /api/chair-feedback/weekly-summary?days=N — returns chair feedback from
  the last N days (default 7) as a Hebrew bullet-list summary for CEO agent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 17:36:12 +00:00
015e553d06 fix: add debug log and null company_id comment to webhook scheduling
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 4m16s
2026-05-16 17:13:07 +00:00
6bdf9786ac feat: emit case-status webhook on status change in PUT /api/cases/:case 2026-05-16 17:10:30 +00:00
d87f9c5a5f fix: include case details in webhook failure warning log 2026-05-16 17:08:33 +00:00
a0fab1f6de feat: add emit_case_status_webhook helper 2026-05-16 17:06:37 +00:00
d5043100a7 fix: json.loads JSONB overrides on GET — asyncpg has no codec registered
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 8s
asyncpg returns JSONB columns as raw JSON strings when no type codec is
configured (only pgvector is registered in _init_connection). The stored
value is a correct JSONB array (jsonb_typeof=array confirmed), but
asyncpg decodes it as str. Parse it explicitly in the GET handler so
the frontend receives the correct Python list/dict.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 18:54:44 +00:00
932cc7191c fix: use ::text::jsonb to store methodology overrides correctly
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 8s
asyncpg cannot encode a Python list as JSONB directly (expects str).
Passing str with ::jsonb causes double-encoding (stored as JSONB string).
Solution: json.dumps() the value → pass as text → PostgreSQL parses
with ::text::jsonb cast, storing it as the correct JSONB array/object.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 18:38:05 +00:00
d983cfdd3b Merge pull request 'fix: prevent JSONB double-encoding on methodology save' (#6) from fix/methodology-jsonb-double-encoding into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m39s
2026-05-10 18:34:03 +00:00
50649baeed fix: prevent JSONB double-encoding on methodology save
Pass req.value directly to asyncpg instead of json.dumps(req.value).
When a Python string was passed with ::jsonb, asyncpg encoded it as a
JSONB string (not an array), causing the frontend spread operator to
split it into individual characters — one textarea per character.

Also fix typo in DISCUSSION_RULES default: "אסה" → "מאסה".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 18:30:49 +00:00
a9cd8aeb12 fix: prevent write_interim_draft context overflow (465K → ≤300K chars)
Two bugs caused all 5 interim blocks to fail with "Claude CLI failed
(exit 1): unknown error":

1. source_context was embedded BOTH inside the prompt template (via
   {source_context}) AND prepended again in write_block — doubling every
   block's context size (232K chars × 2 = 465K chars).

2. _build_source_context loaded all 9 case documents for every block
   regardless of relevance.

Fixes:
- Remove the duplicate source_context prepend in write_block; the
  template already contains it via {source_context}
- Add per-block document filtering (_BLOCK_DOC_TYPES): block-he/zayin →
  empty, block-chet → protocol only, block-tet → appraisals only
- Add 400K char guard before calling claude -p with a descriptive error
  (vs opaque "exit 1: unknown error")
- Add prompt-size warning and size info in claude_session error messages

Result: block-he 0 chars, block-zayin 0 chars, block-vav ~172K,
block-chet ~45K, block-tet ~300K (all under 400K limit)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 10:49:47 +00:00
10a63fb9e0 fix(precedents): separate court rulings from committee decisions correctly
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m37s
- DB: add 'all_committees' virtual source_kind covering internal_committee
  + external_upload appeals_committee rows in one query
- DB: stats now count all case_law rows (not just external_upload),
  fixing the precedents_total that excluded 44 internal-committee records
- UI: courts table filters to source_type=court_ruling only;
  committees table uses the new all_committees query

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 09:59:30 +00:00
f94201c577 feat(precedents): make citation link to detail page
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 34s
Both CourtRow and CommitteeRow citation cells are now Next.js Links
→ /precedents/{id}, letting users navigate directly from the list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 09:01:26 +00:00
026457dac4 fix(precedent-edit): sync form from record without useEffect flash
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 36s
Replace useEffect-based form hydration with React's approved derived-state
pattern (setState-during-render). This eliminates the one-frame flash where
the precedent_level Select showed "—" before useEffect fired, and fixes
cases where the same record reference returned from TanStack cache caused
useEffect to not re-run after save+invalidate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 08:35:04 +00:00
75493ce233 Merge pull request 'feat: link related precedents across court instances (SCHEMA_V11)' (#4) from feat/related-precedents-v11 into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m41s
Reviewed-on: #4
2026-05-10 07:54:37 +00:00
28 changed files with 1737 additions and 597 deletions

View File

@@ -288,11 +288,11 @@ FROM documents d WHERE d.case_id = '{case_id}' AND d.doc_type IN ('appeal', 'res
**אם הכל עבר בהצלחה (בדיקות שלב 6 + טענות + עובדות שמאי):**
```bash
~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'```
~/legal-ai/scripts/pc.sh PATCH "/api/issues/$PAPERCLIP_TASK_ID" '{"status": "done"}'```
**אם בדיקות שלב 6 נכשלו או חילוץ נכשל:**
```bash
~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "blocked"}'```
~/legal-ai/scripts/pc.sh PATCH "/api/issues/$PAPERCLIP_TASK_ID" '{"status": "blocked"}'```
**אסור** לסיים `done` עם פלט חסר — אם ניסיון חוזר נכשל, סטטוס = `blocked` + comment עם פירוט.
5. **שלח מייל**:
@@ -304,16 +304,19 @@ FROM documents d WHERE d.case_id = '{case_id}' AND d.doc_type IN ('appeal', 'res
### העֵר את העוזר המשפטי (CEO) — חובה!
```bash
# CEO לפי חברה — אסור לקבע UUID, חברות שונות = CEO שונה
# $PAPERCLIP_TASK_ID הוא UUID המלא שPaperclip מספק בסביבת הריצה — לעולם לא CMP-XX
# אסור להחליף ידנית: משתמשים ב-$PAPERCLIP_TASK_ID ישירות
if [ "$PAPERCLIP_COMPANY_ID" = "8639e837-4c9d-47fa-a76b-95788d651896" ]; then
CEO_ID="cdbfa8bc-3d61-41a4-a2e7-677ec7d34562" # CMPA — היטלי השבחה
else
CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה
fi
~/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"}}'```
~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" \
"{\"source\":\"automation\",\"triggerDetail\":\"system\",\"reason\":\"מנתח משפטי סיים $PAPERCLIP_TASK_ID בסטטוס done/blocked\",\"payload\":{\"issueId\":\"$PAPERCLIP_TASK_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`.
**⚠️ `$PAPERCLIP_TASK_ID` — זה UUID, לא CMP-XX.** המשתנה מוגדר אוטומטית ע"י Paperclip בסביבת הריצה. אם משתמשים בו ב-double-quotes (`"..."`), bash מרחיב אותו לערך האמיתי. שגיאת `invalid input syntax for type uuid` = שלחת CMP-XX במקום UUID.
## מבנה הפלט המלא — analysis-and-research.md
@@ -397,7 +400,7 @@ X שאלות עומדות להכרעה:
- [אם נמצאו — חיסכון או הבחנה?]
**עמדת ועדת הערר:**
[ימולא ע"י יו"ר הוועדה — עמדה/הנחיה לגבי סוגיה זו שתשמש את סוכן הכתיבה]
[ימולא ע"י יו"ר הוועדה]
---
@@ -482,7 +485,8 @@ X שאלות עומדות להכרעה:
CEO_ID="752cebdd-6748-4a04-aacd-c7ab0294ef33" # CMP — רישוי ובניה
fi
~/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"}}'```
~/legal-ai/scripts/pc.sh POST "/api/agents/$CEO_ID/wakeup" \
"{\"source\":\"automation\",\"triggerDetail\":\"system\",\"reason\":\"מנתח משפטי סיים העמקת ניתוח (pass 2) $PAPERCLIP_TASK_ID\",\"payload\":{\"issueId\":\"$PAPERCLIP_TASK_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`).
## כללים קריטיים

View File

@@ -1,7 +1,7 @@
---
name: "legal-ceo"
description: "עוזר משפטי — מנהל תהליך כתיבת החלטות, מתזמר סוכנים, מפקח על התקדמות"
model: "claude-sonnet-4-6"
model: "claude-opus-4-7"
tools:
- Read
- Bash
@@ -82,7 +82,7 @@ tools:
| סוכן | Agent ID | תפקיד |
|-------|----------|--------|
| מגיה מסמכים | 410c0167-27dc-485c-a51b-7aa8b9ff2217 | הגהת OCR — תיקון ראשי תיבות ושגיאות חילוץ |
| מנתח משפטי | c26e9439-a88a-49dc-9e67-2262c95db65c | חילוץ טענות, תשובות, תגובות |
| מנתח משפטי | c26e9439-a88a-49dc-9e67-2262c95db65c | ניתוח משפטי מלא — חילוץ טענות, ניתוח עמוק, מחקר בקורפוסים, כתיבת analysis-and-research.md |
| חוקר תקדימים | 35022af0-0498-4c3d-90ca-b0ab9e987198 | ניתוח פסיקה, תכניות, פרוטוקולים |
| כותב החלטה | 7ed8686f-24bc-49a3-bc02-67ca15b895a9 | כתיבת בלוקים ה-יב (Opus) |
| בודק איכות | 1a5b229e-9220-4b13-940c-f8eb7285fc29 | QA לפני ייצוא |
@@ -113,8 +113,7 @@ PGPASSWORD=paperclip psql -h localhost -p 54329 -U paperclip -d paperclip -c \
**אם** ה-issue שלך הוא בעצמו תת-משימה (יש לו parent), השתמש ב-parent של ה-parent — כלומר ה-issue הראשי של התיק. לקבלת ה-parent:
```bash
curl -s -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
"$PAPERCLIP_API_URL/api/issues/$PAPERCLIP_TASK_ID" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('parentId') or d['id'])"
~/legal-ai/scripts/pc.sh GET "/api/issues/$PAPERCLIP_TASK_ID" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('parentId') or d['id'])"
```
---
@@ -161,6 +160,7 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
- אם ה-reason מכיל `user_commented`**דלג ישירות לסעיף "טיפול בתגובות חדשות מחיים"**. אל תסרוק תיקים אחרים, אל תבדוק issues, אל תעשה heartbeat רגיל. **טפל רק בתגובה.**
- אם ה-reason מכיל `agent_completion` → דלג לשלב E/F בהתאם לסוכן שסיים
- אם ה-reason מכיל `precedent_extraction_`**דלג לסעיף "חילוץ פסיקה אוטומטי"**. אל תיגע בתיקים — זו עבודת ספרייה.
- אם ה-reason מכיל `weekly-feedback-job`**דלג לסעיף "ניתוח פידבק שבועי"**. אל תיגע בתיקים פעילים.
- אחרת → המשך לשלב A (heartbeat רגיל)
### חילוץ פסיקה אוטומטי
@@ -187,6 +187,26 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
**אל**: אל תיצור issues של ביצוע בתיקי ערר, אל תיכנס לתהליך כתיבת החלטה — זו רק עבודת תחזוקה של ספריית הפסיקה.
### ניתוח פידבק שבועי (weekly-feedback-job)
**מתי:** `$PAPERCLIP_WAKE_REASON` מכיל `weekly-feedback-job`
ה-prompt שתקבל מכיל סיכום של כל הפידבק מיו"ר מהשבוע האחרון, בפורמט:
```
- תיק X (קטגוריה): טקסט הפידבק
- תיק Y (קטגוריה): ...
```
**מה לעשות:**
1. **קרא את `docs/legal-decision-lessons.md`** — הבן מה כבר מתועד שם.
2. **נתח את הפידבק** — אילו דפוסים חוזרים? מה חדש שלא מופיע בלקחים?
3. **עדכן את `docs/legal-decision-lessons.md`** — הוסף רק לקחים חדשים ומהותיים (לא כפל). כל לקח = משפט אחד ברור.
4. **רשום ל-stdout** (לא ל-issue): `echo "weekly feedback done: N lessons added"` — החלף N במספר הלקחים שנוספו.
⚠️ **אין issue ב-Paperclip עבור job זה** — `$PAPERCLIP_TASK_ID` ריק. אל תנסה לפרסם comment ואל תנסה לסגור issue. הפעולה מסתיימת לאחר כתיבת הקובץ.
**כלל:** אל תגע בתיקים פעילים, אל תעיר סוכנים אחרים, אל תבצע heartbeat רגיל — זו משימת תחזוקה בלבד.
### שלב A: בדיקת מצב — שלמות, בדיקות שליליות, תאימות מתודולוגיה
בכל heartbeat **רגיל** (לא comment routing):
@@ -207,6 +227,12 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
- **מסמך ריק**: האם יש מסמך appeal/response עם טקסט שלא ייצר טענות ולא דווח ככשל?
#### A3. אימות תאימות מתודולוגיה
**תנאי קדם — קודם וודא שהמסמך קיים:**
```bash
ls data/cases/$CASE_NUMBER/documents/research/analysis-and-research.md
```
אם הקובץ **לא קיים** — עצור. המנתח לא ביצע את הניתוח המלא. בדוק את issue המנתח: אם הוא `done` אבל הקובץ חסר — צור issue מנתח חדש עם הנחיה לבצע שלבים 2-7 מ-`legal-analyst.md` (לא לחלץ טענות מחדש — `get_claims` להצגה).
קרא את `analysis-and-research.md` ובדוק:
- [ ] סוגיות מנוסחות כסילוגיזם (כלל + עובדות + שאלה)?
- [ ] ממצאים עובדתיים מופרדים ממסקנות משפטיות?
@@ -222,7 +248,7 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
### שלב B: הכנת סיכום, סיווג, ושאלת תוצאה
**מתי:** כשיש טענות מחולצות + מחקר תקדימים, אבל אין תוצאה עדיין
**מתי:** כשיש `analysis-and-research.md` מלא (מנתח סיים שלבים 1-7) וסטטוס `analyst_verified`, אבל אין תוצאה עדיין
**שיטה — dual dispatch:** קודם פרסם comment עם הסיכום המלא (לתיעוד), ואז צור interaction עם כפתורים (לחיים).
@@ -565,11 +591,12 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
| סטטוס | מי שינה לזה | פעולה הבאה |
|--------|-------------|------------|
| `processing` | start-workflow (ממשק) | → בדוק אם כבר קיים issue פעיל לסוכן משנה. אם לא → המשך ל-§A כרגיל (בדוק documents + claims) |
| `new` | (יצירת תיק) | → בדוק extraction_status של מסמכים. אם יש `pending` → צור issue למגיה (410c0167). אם כולם `completed`/`proofread` → צור issue למנתח |
| `proofread` | מגיה | → צור issue למנתח משפטי (ראה תבנית למטה) |
| `documents_ready` | מנתח | → שלב A (בדיקות שלמות + שליליות + מתודולוגיה). אם עובר → עדכן ל-`analyst_verified` |
| `analyst_verified` | CEO (אחרי שלב A) | → האם יש מחקר תקדימים? אם לא → צור issue לחוקר (35022af0). אם כן → שלב B |
| `research_complete` | חוקר | → שלב B (סיכום + סיווג + שאלת תוצאה לחיים) |
| `analyst_verified` | CEO (אחרי שלב A) | → שלב B (סיכום + שאלת תוצאה לחיים). המנתח כבר ביצע את המחקר כחלק מהניתוח — אין ליצור issue לחוקר. |
| `research_complete` | (מנתח — legacy, או תרחיש מיוחד עם חוקר) | → שלב B (סיכום + שאלת תוצאה לחיים). בזרימה הרגילה המנתח לא מגדיר סטטוס זה — רק `documents_ready`. אם תראה סטטוס זה, בדוק אם `analysis-and-research.md` קיים לפני §B. |
| `outcome_set` | CEO (אחרי שחיים בחר) | → האם יש claim_handling? אם לא → שלב B המשך (טבלת bundle/skip). אם כן → שלב C |
| `direction_approved` | CEO (אחרי שחיים אישר) | → צור issue למנתח (c26e9439) ל-pass 2: העמקת ניתוח ואימות פסיקה |
| `analysis_enriched` | מנתח (pass 2) | → שלב D2: צור issue לכותב (7ed8686f) |
@@ -626,15 +653,51 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru
---
**תבנית issue למנתח — חובה בכל תיק:**
1. **טבלת מיפוי מסמכים** — לכל מסמך: שם, doc_type, פעולה נדרשת:
- `appeal` → `extract_claims` (claim_type=claim, party_role=appellant)
- `response` → `extract_claims` (claim_type=response, party_role=respondent/committee)
- `reply` → `extract_claims` (claim_type=reply, party_role=permit_applicant/appellant)
- **`appraisal` → `extract_appraiser_facts`** (לא extract_claims! שומה אינה כתב טענות. חובה בכל תיק 8xxx/9xxx)
- `reference`/`plan`/`protocol`/`permit`/`decision`/`court_decision` → אל תחלץ — חומר רקע בלבד
2. **בדיקת השלמה** — לכל doc_type='appraisal' בתיק, וודא שה-issue אומר במפורש להריץ `extract_appraiser_facts`. בלי זה ה-writer יקבל בלוק ז ריק ממספרים.
3. **הנחיה לסגור את ה-issue ב-PATCH** — סטטוס `done` בהצלחה, `blocked` בכשל. בלי זה Paperclip יפעיל retry בלולאה (נצפה בפועל ב-CMPA-16 / 30-04-26).
4. **הנחיה לשלוח wakeup ל-CEO בסיום** (כך שאתה תידע להמשיך)
**כותרת:** `[ערר CASE_NUMBER] ניתוח משפטי ומחקר — CASE_NAME`
**תיאור חובה — כלול את כל הסעיפים הבאים:**
```
בצע ניתוח משפטי מלא לפי legal-analyst.md שלבים 1-7:
שלב 1: קליטה וזיהוי
- חלץ טענות/תשובות/תגובות מכל מסמכי appeal/response/reply (ראה טבלה למטה)
- לכל appraisal: הרץ extract_appraiser_facts (לא extract_claims)
טבלת מסמכים:
[לכל מסמך: שם | doc_type | פעולה נדרשת]
- appeal → extract_claims(claim_type=claim, party_role=appellant)
- response → extract_claims(claim_type=response, party_role=respondent/committee)
- reply → extract_claims(claim_type=reply, party_role=permit_applicant/appellant)
- appraisal → extract_appraiser_facts (לא extract_claims!)
- reference/plan/protocol/permit/decision → אל תחלץ — רקע בלבד
שלב 2: ניתוח מעמיק — גוף מחליט, רקע דיוני, עובדות מוסכמות, עובדות שנויות
שלב 3: טענות סף, מפת דרכים, סוגיות להכרעה (כולל CREAC + עמדת ועדת הערר ריקה)
שלב 4: שאלות מחקר (1-3 לכל סוגיה)
שלב 5: חיפוש בשלושת הקורפוסים — חובה:
- search_precedent_library(practice_area=RELEVANT_AREA)
- search_decisions
- find_similar_cases
שלב 6: בדיקת שלמות — get_claims ≥ 1 מכל צד
שלב 7: שמור analysis-and-research.md ב-data/cases/CASE_NUMBER/documents/research/
עדכן case_update(status='documents_ready')
סגור issue: PATCH status=done (או blocked אם נכשל)
שלח wakeup ל-CEO עם $PAPERCLIP_TASK_ID כ-issueId (ראה HEARTBEAT.md §4ג)
⚠️ אחרי יצירת task זה — עדכן את ה-issue הראשי ל-status=in_review והמתן ל-wakeup
עם mutation=agent_completion מהמנתח. אין לבדוק get_claims לפני ה-wakeup.
```
1. **בדיקת השלמה** — לכל doc_type='appraisal' בתיק, וודא שה-issue אומר במפורש להריץ `extract_appraiser_facts`. בלי זה ה-writer יקבל בלוק ז ריק ממספרים.
2. **הנחיה לסגור את ה-issue ב-PATCH** — סטטוס `done` בהצלחה, `blocked` בכשל. בלי זה Paperclip יפעיל retry בלולאה (נצפה בפועל ב-CMPA-16 / 30-04-26).
3. **הנחיה לשלוח wakeup ל-CEO בסיום** (כך שאתה תידע להמשיך) — חובה להשתמש ב-`$PAPERCLIP_TASK_ID` (UUID) ולא ב-CMP-XX.
## סינון תיקים לפי חברה — חובה!
@@ -746,8 +809,10 @@ case_prefix="${case_number:0:1}"
~/legal-ai/scripts/pc.sh POST "/api/issues/{issue-id}/comments" '{"body": "..."}'
# צור issue חדש (עם הקצאה לסוכן → מפעיל wakeup אוטומטי!)
~/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"}'
# ⚠️ שלוף projectId מה-issue ההורה — אל תקבע UUID ידנית:
PROJECT_ID=$(~/legal-ai/scripts/pc.sh GET "/api/issues/$PAPERCLIP_TASK_ID" | jq -r '.projectId')
~/legal-ai/scripts/pc.sh POST "/api/companies/$PAPERCLIP_COMPANY_ID/issues" \
"{\"title\":\"...\",\"projectId\":\"$PROJECT_ID\",\"assigneeAgentId\":\"{agent-id}\",\"description\":\"...\",\"status\":\"todo\"}"
# עדכן issue
~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'

View File

@@ -19,6 +19,7 @@ tools:
- mcp__legal-ai__revise_draft
- mcp__legal-ai__get_style_guide
- mcp__legal-ai__validate_decision
- mcp__legal-ai__case_update
---
# מייצא טיוטה — סוכן ייצוא סופי
@@ -40,14 +41,14 @@ tools:
## סקייל ייצוא
**חובה לקרוא לפני כל ייצוא:**
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/SKILL.md`
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/references/document-types.md`
- `/home/chaim/.paperclip/instances/default/skills/$PAPERCLIP_COMPANY_ID/legal-docx/SKILL.md`
- `/home/chaim/.paperclip/instances/default/skills/$PAPERCLIP_COMPANY_ID/legal-docx/references/document-types.md`
**סקריפט ייצוא:**
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/scripts/create-legal-doc.js`
- `/home/chaim/.paperclip/instances/default/skills/$PAPERCLIP_COMPANY_ID/legal-docx/scripts/create-legal-doc.js`
**תבנית:**
- `/home/chaim/.paperclip/instances/default/skills/42a7acd0-30c5-4cbd-ac97-7424f65df294/legal-docx/references/docx template.docx`
- `/home/chaim/.paperclip/instances/default/skills/$PAPERCLIP_COMPANY_ID/legal-docx/references/docx template.docx`
## תהליך עבודה
@@ -102,12 +103,13 @@ tools:
### שלב 4: שמירה מגורסת
1. צור תיקייה `~/legal-ai/data/cases/{מספר-ערר}/exports/` (אם לא קיימת)
2. בדוק כמה טיוטות כבר קיימות בתיקייה (קבצים שמתחילים ב-`טיוטה-V`)
3. שמור כ-`טיוטה-V{N}.docx` כאשר N = המספר הבא בתור
- אם אין טיוטות: `טיוטה-V1.docx`
- אם יש V1: `טיוטה-V2.docx`
2. בדוק כמה טיוטות כבר קיימות בתיקייה (קבצים שמתחילים ב-`טיוטה-v`)
3. שמור כ-`טיוטה-v{N}.docx` כאשר N = המספר הבא בתור
- אם אין טיוטות: `טיוטה-v1.docx`
- אם יש v1: `טיוטה-v2.docx`
- וכן הלאה
4. ודא שהקובץ נוצר ושגודלו סביר
5. עדכן סטטוס תיק ל-`exported` דרך `case_update(case_number, {"status": "exported"})`
### שלב 5: דיווח
דווח למשתמש:
@@ -145,6 +147,6 @@ fi
## כללים קריטיים
1. **לעולם אל תייצא בלי בדיקה** — תמיד הרץ validate_decision קודם
2. **לא לדרוס טיוטות קודמות** — תמיד גרסה חדשה (V1, V2, V3...)
3. **שמות קבצים בעברית** — `טיוטה-V1.docx`, לא `draft-V1.docx`
2. **לא לדרוס טיוטות קודמות** — תמיד גרסה חדשה (v1, v2, v3...)
3. **שמות קבצים בעברית** — `טיוטה-v1.docx`, לא `draft-v1.docx`
4. **קרא את הסקייל** — לפני כל ייצוא, קרא את legal-docx SKILL.md

View File

@@ -92,11 +92,11 @@ tools:
**אם הכל עבר בהצלחה:**
```bash
~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "done"}'```
~/legal-ai/scripts/pc.sh PATCH "/api/issues/$PAPERCLIP_TASK_ID" '{"status": "done"}'```
**אם נכשלו תיקונים קריטיים או יש markers `[?]` רבים:**
```bash
~/legal-ai/scripts/pc.sh PATCH "/api/issues/{issue-id}" '{"status": "blocked"}'```
~/legal-ai/scripts/pc.sh PATCH "/api/issues/$PAPERCLIP_TASK_ID" '{"status": "blocked"}'```
**אסור** לסיים `done` עם פלט חסר — אם נכשל, סטטוס = `blocked` + comment עם פירוט.
### העֵר את העוזר המשפטי (CEO) — חובה!

View File

@@ -83,6 +83,8 @@ tools:
ה-analyst וה-researcher חייבים לתעד queries לקורפוסים שלהם. בלי תיעוד — אין דרך לוודא שתקדימי עליון רלוונטיים לא הוחמצו.
**שיטת בדיקה:** grep ידני — קרא את קבצי המחקר וחפש בהם את הסעיפים הנ"ל. `validate_decision` **לא** בודק זאת אוטומטית. הצלבה עם MCP (סעיף 4 למטה) היא אופציונלית ומשלימה.
בדוק:
1. **קיום סעיף "שאילתות לקורפוסים"**:
- ב-`{case_dir}/documents/research/analysis-and-research.md` — סעיף **7א** (לפי שלב 5ד של ה-analyst)

View File

@@ -14,6 +14,7 @@ tools:
- mcp__legal-ai__document_get_text
- mcp__legal-ai__search_case_documents
- mcp__legal-ai__search_decisions
- mcp__legal-ai__search_internal_decisions
- mcp__legal-ai__find_similar_cases
- mcp__legal-ai__extract_references
- mcp__legal-ai__precedent_attach
@@ -30,6 +31,8 @@ tools:
- mcp__legal-ai__workflow_status
---
> ראה גם: [HEARTBEAT.md](HEARTBEAT.md) לכללי הפעלה כלליים — routing, company filtering, wakeup API
# חוקר תקדימים — סוכן מחקר משפטי
אתה חוקר משפטי מומחה בתכנון ובניה ישראלי. תפקידך לנתח את מסמכי הרקע בתיק ערר — פסיקה, תכניות, פרוטוקולים, החלטות ביניים.
@@ -121,6 +124,27 @@ search_precedent_library(
- אם תוצאה דומה: תקדם לחיסכון דוקטרינרי ("כפי שקבענו ב-X")
- אם תוצאה הפוכה: ציין כי **חובה** הבחנה (distinguishing)
#### 2ב.2א — ועדות ערר אחרות (`search_internal_decisions`) — לפי שיקול דעת
**ההבדל מ-`search_decisions`:** `search_decisions` מחפש **רק בהחלטות של דפנה**. `search_internal_decisions` מחפש בהחלטות **כל ועדות הערר** בכל המחוזות (ירושלים, מרכז, תל אביב, צפון, דרום, ארצי).
**מתי להשתמש:**
- כשהסוגיה היא חדשנית ודפנה לא הכריעה בה → בדוק אם ועדת ערר אחרת כבר הכריעה
- כשרוצים לבדוק האם יש גישות שונות בין מחוזות (ועדות ערר שונות)
- **אל תשתמש** אם `search_decisions` כבר מצא את התשובה — אין צורך לחפש פעמיים
```
search_internal_decisions(
query="...",
practice_area="histael_hashbacha", # rishuy_uvniya / betterment_levy / compensation_197
district="ירושלים", # ריק = כל המחוזות
chair_name="", # ריק = כל היו"רים; "דפנה תמיר" = דפנה בלבד (שווה ל-search_decisions)
limit=5
)
```
⚠️ **שים לב להיררכיה:** החלטת ועדת ערר נמוכה מבית משפט מחוזי. אל תציג ועדת ערר אחרת כ"הלכה מחייבת".
#### 2ב.3 — בדיקה מצטלבת מול `daphna-precedent-network.md`
לכל סוגיה — בדוק במסמך:

View File

@@ -56,6 +56,8 @@
| [`docs/decision-block-mapping.md`](docs/decision-block-mapping.md) | מיפוי בלוקים להחלטות — איך 12 הבלוקים משתקפים ב-DOCX | להתמצאות במבנה |
| [`docs/memory.md`](docs/memory.md) | הקשר כללי — skills, פרויקטים שהושלמו, מבנה vault | להתמצאות כללית |
| [`skills/decision/SKILL.md`](skills/decision/SKILL.md) | מדריך סגנון מלא של דפנה — טון, מבנה, ביטויים, מתודולוגיה | **לפני כל כתיבת החלטה** |
| [`.claude/agents/HEARTBEAT.md`](.claude/agents/HEARTBEAT.md) | checklist הפעלת סוכן — routing, company filtering, quirks, wakeup עם UUID נכון | **לפני כל עבודה על סוכנים** |
| [`skills/dafna-decision-template/SKILL.md`](skills/dafna-decision-template/SKILL.md) | export DOCX לפי styles של תבנית Word של דפנה — line classification, dash policy, placeholder handling | לפני export DOCX |
---
@@ -106,14 +108,28 @@
├── skills/ ← כלי עבודה ומדריכים
│ ├── decision/ מדריך סגנון + references + 12 בלוקים
│ ├── assistant/ קטלוג מסמכים
── docx/ עיצוב DOCX
── docx/ עיצוב DOCX
│ ├── dafna-decision-template/ export DOCX לפי תבנית Word של דפנה
│ └── new-company-setup/ blueprint הוספת חברה חדשה
├── .claude/
│ └── agents/ ← הוראות סוכנים + HEARTBEAT.md (symlinks ב-Paperclip)
│ ├── HEARTBEAT.md checklist הפעלה משותף לכל הסוכנים
│ ├── legal-ceo.md תזמורן + בקרת זרימה
│ ├── legal-writer.md כתיבת בלוקים בסגנון דפנה
│ ├── legal-analyst.md ניתוח משפטי + חילוץ טענות
│ ├── legal-researcher.md חיפוש תקדימים
│ ├── legal-qa.md 7 שערי איכות
│ ├── legal-proofreader.md תיקון OCR
│ ├── legal-exporter.md ייצוא DOCX סופי
│ └── hermes-curator.md סוכן Hermes לניתוח סגנון post-export
├── data/
│ ├── training/ ← 4 החלטות לאימון (DOCX)
│ ├── exports/ ← טיוטות DOCX מיוצאות
│ └── cases/{case-number}/ ← תיקי עררים (מבנה שטוח, סטטוס ב-DB)
├── web/ ← FastAPI backend (Python): 75 API endpoints
├── web/ ← FastAPI backend (Python): 75+ API endpoints
│ ├── app.py ← API ראשי
│ ├── paperclip_client.py ← אינטגרציית Paperclip
│ ├── paperclip_api.py ← אינטגרציית Paperclip: `pc_request()` + `emit_case_status_webhook()`
│ ├── paperclip_client.py ← legacy client (ישן — השתמש ב-paperclip_api.py)
│ └── gitea_client.py ← אינטגרציית Gitea
├── web-ui/ ← Next.js frontend (TypeScript/React): ממשק המשתמש
│ └── next.config.ts ← proxy: /api/* → FastAPI :8000
@@ -182,6 +198,32 @@
- הסקריפט מסנן local skills שלא קיימים ב-CMPA (מציג אזהרה), משתמש ב-API (לא DB ישיר), יוצר revisions, idempotent.
- שאלות ה-skill הרשמי של Paperclip — `paperclip` skill תחת `paperclipai/paperclip`.
### Webhook יוצא — עדכון סטטוס תיק לפלאגין
כשסטטוס תיק משתנה דרך `PUT /api/cases/{case_number}`, הבקאנד שולח webhook אסינכרוני לפלאגין:
```
PUT /api/cases/{case_number} → emit_case_status_webhook() [BackgroundTask]
→ POST /api/plugins/marcusgroup.legal-ai/webhooks/case-status
→ plugin-legal-ai/onWebhook()
→ comment בעברית על issue + CEO wakeup (כשסטטוס = qa_failed)
```
- הקוד ב-`web/paperclip_api.py` (`emit_case_status_webhook`), fire-and-forget, timeout 5s
- הפלאגין שומר idempotency key ב-state עם TTL 5 דקות למניעת spam על retry
- `GET /api/cases/stale?days=N` — תיקים שלא עודכנו N ימים; מוחרגים: `new`, `final`, `exported`
- `GET /api/chair-feedback/weekly-summary` — סיכום פידבק YU"R לשבוע האחרון
### Scheduled Jobs (plugin-legal-ai)
| Job | לוח זמנים | מה עושה |
|-----|-----------|---------|
| `stale-case-reminder` | יומי 08:00 | שולח comment אזהרה על תיקים תקועים >3 ימים |
| `weekly-feedback-analysis` | ראשון 19:00 | מעיר CEO לניתוח פידבק YU"R ועדכון `docs/legal-decision-lessons.md` |
| `sync-case-status` | כל 30 דק' | מסנכרן סטטוסי תיקים בין legal-ai ל-Paperclip |
CEO שמתעורר מ-`weekly-feedback-job` כותב לקובץ בלבד — **אין לו issueId, אל תנסה לפרסם comment או לסגור issue**.
### External adapters — `deepseek_local`
- מיקום ה-package: [adapters/deepseek-paperclip-adapter/](adapters/deepseek-paperclip-adapter/) (לא ב-`node_modules`).
- רישום ב-Paperclip: רשומה ב-`~/.paperclip/adapter-plugins.json` (נטען אוטומטית ב-startup דרך `buildExternalAdapters`). אין צורך בעריכת `node_modules`.
@@ -191,6 +233,12 @@
- **⚠ Cross-company sync**: `sync_agents_across_companies.py` **מדלג** על סוכנים עם `adapter_type` שונה בין CMP ל-CMPA. כשעוברים סוכן ל-`deepseek_local` חובה להחיל ידנית בשתי החברות לפני sync.
- **תוספת adapters עתידיים** (OpenAI ישיר, Anthropic ישיר, וכו'): אותו דפוס. ה-package הראשי חייב לייצא `createServerAdapter()` שמחזיר `{ type, label, models, agentConfigurationDoc, execute, testEnvironment, sessionCodec, listSkills, syncSkills, ... }`. ראה את [adapters/deepseek-paperclip-adapter/dist/index.js](adapters/deepseek-paperclip-adapter/dist/index.js) כתבנית.
### External adapters — Hermes Curator (`curator-cmp` / `curator-cmpa`)
- פרופילי Hermes נפרדים לסוכן `hermes-curator` — מנתח החלטות סופיות ומציע עדכוני SKILL.md/lessons.md
- מיקום: `~/.hermes/profiles/curator-cmp/` + `~/.hermes/profiles/curator-cmpa/`
- מופעל אחרי export סופי; אינו מעדכן קבצים ישירות
- **תהליך אישור הצעות:** הצעות ה-curator מגיעות כ-comment ב-Paperclip → חיים בוחן ומאשר ידנית → commits ל-`SKILL.md` ו-`docs/legal-decision-lessons.md`
---
## עקרונות כתיבה קריטיים

View File

@@ -0,0 +1,414 @@
# דו"ח Audit סוכנים — 2026-05-17
> נוצר על-ידי 7 sub-agents מקבילים שחקרו כל סוכן בנפרד.
> כיסוי: קבצי הנחיות, תצורת DB, skills, MCP tools, freshness, drift CMP↔CMPA.
>
> **עדכון 2026-05-17:** כל 12 הבעיות טופלו באותו יום. ראה סעיף "סטטוס תיקונים" למטה.
---
## סיכום מנהלים
### טבלת מצב כללית — לאחר תיקונים (2026-05-17)
| סוכן | מודל (instructions = DB) | Skills CMP | Skills CMPA | סטטוס |
|------|--------------------------|-----------|-----------|--------|
| עוזר משפטי (CEO) | claude-opus-4-7 ✅ | 9 | 6 | ✅ תקין |
| מנתח משפטי | claude-opus-4-7 ✅ | 9 | 6 | ✅ תקין |
| חוקר תקדימים | claude-sonnet-4-6 ✅ | 9 | 6 | ✅ תקין |
| כותב החלטה | claude-opus-4-7 ✅ | 9 | 6 | ✅ תקין |
| בודק איכות (QA) | claude-sonnet-4-6 ✅ | 9 | 6 | ✅ תקין |
| מייצא טיוטה | claude-sonnet-4-6 ✅ | 9 | 6 | ✅ תקין |
| מגיה מסמכים | claude-opus-4-7 ✅ | 9 | 6 | ✅ תקין |
| מנהל ידע (Curator) | deepseek-v4-pro ✅ | 9 | 6 | ✅ תקין |
> Skills CMPA=6 הוא עיצוב מכוון (6 shared-only skills). verify script מאשר "0 agents need sync".
### סטטוס תיקונים — כל 12 הבעיות טופלו
| # | חומרה | סוכן | בעיה | סטטוס | commit |
|---|-------|------|------|-------|--------|
| 1 | 🔴 | מייצא | `טיוטה-V``טיוטה-v` — דורס גרסאות | ✅ תוקן | `a584dc3` |
| 2 | 🔴 | מייצא | case.status לא מעודכן ל-`exported` + case_update חסר מ-tools | ✅ תוקן | `a584dc3` |
| 3 | 🔴 | חוקר | §ז (query log) חסר בתיק 8174-24 | ✅ תוקן | data (gitignored) |
| 4 | 🟠 | כולם | Skills asymmetry CMPA | ✅ לא נדרש — verify: "0 need sync" (עיצוב מכוון) | — |
| 5 | 🟠 | חוקר | `search_internal_decisions` לא מתועד | ✅ תוקן — tool + סעיף 2ב.2א | `35423ea` |
| 6 | 🟠 | מייצא | נתיב legal-docx hardcoded ל-CMP UUID | ✅ תוקן → `$PAPERCLIP_COMPANY_ID` | `a584dc3` |
| 7 | 🟠 | CEO | Project ID + company UUID hardcoded | ✅ תוקן → דינמי מ-$PAPERCLIP_TASK_ID | `35423ea` |
| 8 | 🟡 | רוב | Model drift instructions↔DB | ✅ תוקן + שודרג ל-opus-4-7 | `1608ea5`, `c3ce0e7` |
| 9 | 🟡 | QA | corpus_queries_logged: ידני או אוטומטי? | ✅ תוקן — הבהרה מפורשת: grep ידני | `1608ea5` |
| 10 | 🟡 | CEO | maxConcurrentRuns=NULL | ✅ לא נדרש — DB כבר maxConcurrentRuns=2 | — |
| 11 | 🟡 | מגיה | {issue-id} placeholder בקוד | ✅ תוקן → `$PAPERCLIP_TASK_ID` | `1608ea5` |
| 12 | 🟢 | מנהל ידע | ownership הצעות curator לא מוגדר | ✅ תוקן — הוסף ל-CLAUDE.md | `1608ea5` |
### שינויים נוספים שבוצעו באותו סשן
| שינוי | קובץ | commit |
|-------|------|--------|
| weekly-feedback-job: כתיבה לקובץ בלבד, לא Paperclip comment | legal-ceo.md | `ea0532b` |
| try-catch על agents.invoke בפידבק שבועי | worker.ts | `73e37df` |
| try-catch על http.fetch ב-stale-case-reminder | worker.ts | `73e37df` |
| HEARTBEAT.md reference בראש legal-researcher.md | legal-researcher.md | `1608ea5` |
| search_internal_decisions הוסף ל-legal-researcher tools | legal-researcher.md | `35423ea` |
| opus-4-6 → opus-4-7 ב-DB: CEO, מנתח, כותב, מגיה (16 סוכנים) | DB | `c3ce0e7` |
---
## ממצאים לפי סוכן
### 1. עוזר משפטי (CEO)
**קובץ:** `.claude/agents/legal-ceo.md` — 796 שורות, עודכן 2026-05-17
**תצורה:**
| חברה | ID | Model | Budget |
|------|-----|-------|--------|
| CMP | `752cebdd-6748-4a04-aacd-c7ab0294ef33` | claude-opus-4-6 | 1500¢ |
| CMPA | `cdbfa8bc-3d61-41a4-a2e7-677ec7d34562` | claude-opus-4-6 | 1500¢ |
**routing conditions:** `user_commented`, `agent_completion`, `precedent_extraction_*`, `weekly-feedback-job`, fallback→heartbeat רגיל
**MCP tools מוזכרים (41):** case_get/list/update, document_list, get_claims, get_chair_directions, record/list_chair_feedback, approve_direction, brainstorm_directions, search_case_documents, search_precedent_library, workflow_status, processing_status, get_metrics, validate_decision, set_outcome, export_docx, apply_user_edit, list_bookmarks, revise_draft, precedent_process_pending, extract_halachot/metadata, library_get/list, halacha_review, halachot_pending, extract_appraiser_facts, write_interim_draft, export_interim_draft
**✅ תקין:**
- Routing logic מלא ועדכני (כולל weekly-feedback-job שתוקן לאחרונה)
- Company filtering ברור (טבלה עם UUIDs וטווחי תיקים)
- Wakeup דרך API בלבד (לא DB ישיר) — מוגדר במפורש
- HEARTBEAT.md references נכונים (§0, §1, §1.7)
- weekly-feedback-job: כתיבה לקובץ בלבד, ללא issueId — נכון
**⚠️ בעיות:**
- 🟠 **Model drift:** instructions = claude-sonnet-4-6, DB = claude-opus-4-6
- 🟠 **Hardcoded Project ID:** `25c1b4a1-2c0e-4a2d-9938-8ae56ccda6f1` (תיק 1130-25) — צריך להיות דינמי
- 🟡 **maxConcurrentRuns = NULL** ב-DB (שאר הסוכנים = 1)
- 🟡 **MCP startup race:** הוראות מדברות על sleep+retry אבל לא כ-code אוטומטי
---
### 2. מנתח משפטי
**קובץ:** `.claude/agents/legal-analyst.md` — 498 שורות, עודכן 2026-05-04
**תצורה:**
| חברה | ID | Model | Budget |
|------|-----|-------|--------|
| CMP | `c26e9439-a88a-49dc-9e67-2262c95db65c` | claude-opus-4-6 | 1500¢ |
| CMPA | `f70fd353-...` | claude-opus-4-6 | 1500¢ |
**MCP tools (18):** case_get/list/update, document_list/get_text, extract_claims, extract_appraiser_facts, get_claims, search_case_documents, search_decisions, search_precedent_library, precedent_library_get/list, halacha_review, halachot_pending, find_similar_cases, workflow_status, processing_status
**Output artifacts:** `{case_dir}/documents/research/analysis-and-research.md`
**Query logging (§5ד/§7א):** לרשום כל `search_precedent_library`, `search_decisions`, `find_similar_cases` כולל ניסיונות עם 0 תוצאות
**✅ תקין:**
- כל 18 כלי MCP מוזכרים ומיושמים
- סיווג claim_type ברור (claim/response/reply)
- Wakeup CEO בפורמט נכון
- reference files קיימים
**⚠️ בעיות:**
- 🟠 **Model drift:** instructions = claude-opus-4-7, DB = claude-opus-4-6
- 🟡 **CMPA sync gap:** עדכון אחרון CMPA = 2026-05-04 (13 ימים לפני CMP)
---
### 3. חוקר תקדימים
**קובץ:** `.claude/agents/legal-researcher.md` — 240 שורות, עודכן 2026-05-04
**תצורה:**
| חברה | ID | Model | Budget |
|------|-----|-------|--------|
| CMP | `35022af0-0498-4c3d-90ca-b0ab9e987198` | claude-sonnet-4-6 | 1500¢ |
| CMPA | `5dd06843-...` | claude-sonnet-4-6 | 1500¢ |
**MCP tools (29):** case_get/update, document_list/get_text, search_case_documents, search_decisions, find_similar_cases, extract_references, precedent_attach, precedent_list, precedent_search_library, search_precedent_library, library_get/list, extract_halachot/metadata, precedent_process_pending, halacha_review, halachot_pending, workflow_status
**Output artifact:** `{case_dir}/documents/research/precedent-research.md`
**Query logging (§ז):** חובה — כל query עם פילטרים, תוצאות, בחירה/דחייה, negative evidence
**✅ תקין:**
- שלושת הקורפוסים מוגדרים בבירור (פסיקה חיצונית / קאנון דפנה / ציטוטים ידניים)
- precedent_attach עם הוראות מלאות
- Wakeup CEO דינמי לפי חברה
**⚠️ בעיות:**
- 🔴 **§ז חסר בתיק 8174-24** — 1 מתוך 3 תיקים בדיסק חסר את תיעוד השאילתות. QA אמור לחסום ייצוא.
- 🟠 **`search_internal_decisions` לא מתועד** — הכלי ב-header אבל לא מוסבר בגוף ההנחיות. מתי להשתמש בו?
- 🟠 **Skills asymmetry CMPA** — CMPA חסרה: legal-assistant, legal-decision, legal-docx, diagnose-why-work-stopped, appendix-expert-intern, terminal-bench-loop
- 🟡 **`daphna-precedent-network.md` עדכון אחרון 27 אפריל** — עשוי להיות לפני תקדימים חדשים
- 🟡 **HEARTBEAT.md לא מוזכר בפירוש** — אין link ישיר בתחילת ההנחיות
---
### 4. כותב החלטה
**קובץ:** `.claude/agents/legal-writer.md` — 410 שורות, עודכן 2026-05-04
**תצורה:**
| חברה | ID | Model | Budget |
|------|-----|-------|--------|
| CMP | `7ed8686f-24bc-49a3-bc02-67ca15b895a9` | claude-opus-4-6 | 1500¢ |
| CMPA | `99289cb1-...` | claude-opus-4-6 | 1500¢ |
**Block range:** ה-יא (5-11), כותב בסדר; א-ד (אוטומטי), יב (אוטומטי)
**5 style docs לפני בלוק י (כולם קיימים):**
- `docs/daphna-voice-fingerprint.md` ✅ (עודכן 10 מאי)
- `docs/daphna-precedent-network.md` ✅ (עודכן 27 אפריל)
- `docs/daphna-architecture-by-outcome.md` ✅ (עודכן 28 אפריל)
- `docs/daphna-acceptance-architecture.md` ✅ (עודכן 28 אפריל)
- `docs/voice-1130-25.md` ✅ (עודכן 26 אפריל)
**MCP tools (18):** case_get/update, document_list/get_text, get_claims, get_chair_directions, get_decision_template, get_block_context, save_block_content, write_block, search_decisions, search_precedent_library, library_get/list, search_case_documents, get_style_guide, halacha_review, workflow_status, apply_user_edit
**✅ תקין:**
- 4 statuses של get_chair_directions מוגדרים (missing/empty/partial/complete)
- Revision mode ברור (לא לשמור ב-DB בעריכה)
- 10 anti-patterns ברורים
- Company filtering נכון (CEO IDs שונים לפי חברה)
**⚠️ בעיות:**
- 🟠 **Model drift:** instructions = claude-opus-4-7, DB = claude-opus-4-6
- 🟡 **חסר שלב 0 מפורש:** בדיקת `issue.description` (ההוראה הראשית מה-CEO)
---
### 5. בודק איכות (QA)
**קובץ:** `.claude/agents/legal-qa.md` — 219 שורות, עודכן 2026-05-04
**תצורה:**
| חברה | ID | Model | Budget |
|------|-----|-------|--------|
| CMP | `1a5b229e-9220-4b13-940c-f8eb7285fc29` | claude-sonnet-4-6 | 1500¢ |
| CMPA | `7191ff77-...` | claude-sonnet-4-6 | 1500¢ |
**9 בדיקות (לא 8 — §7א הוא נפרד):**
1. שלמות מבנית — critical
2. רקע ניטרלי — critical
3. כיסוי טענות — critical
4. משקלות — warning
5. ללא כפילות — warning
6. מספור רציף — warning
7א. שאילתות קורפוס (corpus_queries_logged) — **critical blocker**
7. תאימות מתודולוגיה — critical
8. קול דפנה — critical
**Reference files (כולם קיימים):**
- `docs/daphna-decision-tree.md` ✅ (521 שורות)
- `docs/daphna-voice-fingerprint.md` ✅ (471 שורות)
- `docs/daphna-architecture-by-outcome.md` ✅ (381 שורות)
- `docs/daphna-acceptance-architecture.md` ✅ (640 שורות)
- `docs/daphna-block-zayin-claims.md` ✅ (385 שורות)
- `docs/daphna-precedent-network.md` ✅ (379 שורות)
**✅ תקין:**
- כל reference files קיימים ונגישים
- Company filtering מתועד (CEO IDs נכונים)
- Decision logic done/blocked מוגדרת
**⚠️ בעיות:**
- 🟡 **בדיקה 7א לא ברורה** — אוטומטית (validate_decision) או ידנית (grep בקובצי markdown)?
- 🟡 **בדיקה 8 (קול דפנה) סובייקטיבית** — חסרות דוגמאות anti-patterns מדידות
- 🟡 **get_metrics() — אין ספי קבלה** — מה מספר/אחוז שמוגדר כ-pass?
- 🟡 **decision tree:** אם רק בדיקות 4-6 (warning) נכשלו — done או blocked?
---
### 6. מייצא טיוטה (Exporter)
**קובץ:** `.claude/agents/legal-exporter.md` — 151 שורות, עודכן 2026-05-04
**תצורה:**
| חברה | ID | Model | Budget |
|------|-----|-------|--------|
| CMP | `d0dc703b-ca83-4883-bca7-c9449e8713cd` | claude-sonnet-4-6 | 1500¢ |
| CMPA | `ada99a7d-...` | claude-sonnet-4-6 | 1500¢ |
**MCP tools (8):** export_docx, apply_user_edit, list_bookmarks, revise_draft, validate_decision, get_claims, get_block_context, workflow_status
**✅ תקין:**
- Git integration לכל ייצוא/עדכון
- validate_decision לפני export מוגדר
- active_draft detection (עריכה-*.docx) מוגדר
**⚠️ בעיות:**
- 🔴 **Naming mismatch קריטי:** הנחיות → `טיוטה-V{N}.docx` (V גדולה); קוד `revise_draft``טיוטה-v{N}.docx` (v קטנה); בדיסק בפועל → `טיוטה-v1.docx` (v קטנה). **הסוכן יחפש V גדולה ולא ימצא — יתחיל מ-v1 בכל הפעלה ויחליף קבצים קיימים!**
- 🔴 **case.status לא מעודכן ל-`exported`** — אחרי export מצליח, הסטטוס נשאר `drafted`/`reviewed`; הסטטוס `exported` קיים ב-DB schema ומוחרג מ-stale query
- 🟠 **legal-docx SKILL.md path hardcoded לCMP UUID** — CMPA ייכשל בקריאת ה-SKILL.md
- נכון: `/home/chaim/.paperclip/instances/default/skills/42a7acd0-.../legal-docx/SKILL.md`
- חסר: דינמי לפי `$PAPERCLIP_COMPANY_ID`
- 🟡 **Heartbeat grace=60s** — אם export DOCX > 60s, שני instances יתעוררו במקביל
- 🟡 **File size validation** — מוזכר בהנחיות אך לא מיושם בקוד
---
### 7. מגיה מסמכים (Proofreader)
**קובץ:** `.claude/agents/legal-proofreader.md` — 115 שורות, עודכן 2026-05-04
**תצורה:**
| חברה | ID | Model | Budget |
|------|-----|-------|--------|
| CMP | `410c0167-27dc-485c-a51b-7aa8b9ff2217` | claude-opus-4-6 | 1500¢ |
| CMPA | `17839fc6-...` | claude-opus-4-6 | 1500¢ |
**OCR workflow — 5 שלבים:** זיהוי → תיקון אוטומטי (abbreviations.json) → הגהה חכמה → שמירה → דיווח+סגירה
**abbreviations.json:** קיים ב-`/home/chaim/legal-ai/data/abbreviations.json` (2545 bytes, עודכן אפריל)
**✅ תקין:**
- abbreviations.json קיים
- Wakeup CEO דינמי לפי חברה
- חיוב סגירת issue
**⚠️ בעיות:**
- 🟠 **Model drift:** instructions = claude-opus-4-7, DB = claude-opus-4-6
- 🟡 **MCP write support לתיקיות:** לא אומת שה-tools תומכים בכתיבה ל-`documents/proofread/`
- 🟡 **Placeholder `{issue-id}` בקוד:** pc.sh calls משתמשות ב-literal `{issue-id}` — האם הסוכן מחליף עם `$PAPERCLIP_TASK_ID`?
- 🟡 **`extraction_status = proofread`:** האם השדה קיים ב-MCP document schema?
---
### 8. מנהל ידע (Hermes Curator)
**קובץ:** `.claude/agents/hermes-curator.md` — 147 שורות, עודכן 2026-05-10
**תצורה:**
| חברה | ID | Adapter | Model | Budget |
|------|-----|---------|-------|--------|
| CMP | `60dce831-5c5b-4bae-bda9-5282d506f0dc` | deepseek_local | deepseek-v4-pro | 1500¢ |
| CMPA | `d6f7c55d-570a-46b8-8d72-1286d07da0d8` | deepseek_local | deepseek-v4-pro | 1500¢ |
**Profiles:** `~/.hermes/profiles/curator-cmp/` ✅ + `curator-cmpa/` ✅ (שניהם קיימים)
**Trigger:** UI "סמן כסופי" → `web/paperclip_client.py:pc_wake_curator_for_final()` → sub-issue + wakeup
**MCP tools (6):** case_get, case_get_final_text, document_list, get_style_guide, precedent_library_list, search_internal_decisions, halacha_review
**✅ תקין:**
- deepseek_local מוגדר נכון בשתי החברות
- Profiles קיימים ועובדים (MEMORY.md מ-06/05 עם 5 ממצאים)
- Read-only design — לא מעדכן קבצים ישירות
- env vars נדרשים מתועדים
**⚠️ בעיות:**
- 🟢 **לא מוגדר:** מי מממש הצעות ל-SKILL.md/lessons.md שה-curator מציע ב-comments?
- 🟢 **Hermes bias:** DeepSeek V4-Pro עלול לפרש תוצאות בצורה סובייקטיבית — אין oversight layer
---
## בעיות חוצות-סוכנים
### 1. Skills Asymmetry CMP vs CMPA (🟠 גבוה)
**Skills ב-CMP (9):**
- משותפים (6): paperclip, paperclip-converting-plans-to-tasks, paperclip-create-agent, paperclip-create-plugin, paperclip-dev, para-memory-files
- ייחודיים CMP (3+): legal-assistant, legal-decision, legal-docx, appendix-expert-intern, diagnose-why-work-stopped, terminal-bench-loop
**Skills ב-CMPA (6):** משותפים בלבד — **חסרים כל ה-legal-* skills**
**השפעה:** סוכני CMPA לא יכולים להשתמש ב-legal-decision skill (כתיבה), legal-assistant (ניתוח), legal-docx (DOCX). לא ברור אם זו החלטה מכוונת (CMPA עובד אחרת?) או gap בסנכרון.
**פעולה:** הרץ `sync_agents_across_companies.py --verify` עם PAPERCLIP_BOARD_API_KEY לבדיקה.
### 2. Model Version Drift (🟡 בינוני)
ב-DB כל הסוכנים רצים על claude-opus-4-6 או claude-sonnet-4-6, אבל קבצי הנחיות מציינים גרסאות שונות:
| סוכן | instructions מציין | DB רץ על |
|------|-------------------|---------|
| CEO | claude-sonnet-4-6 | claude-opus-4-6 |
| מנתח | claude-opus-4-7 | claude-opus-4-6 |
| כותב | claude-opus-4-7 | claude-opus-4-6 |
| מגיה | claude-opus-4-7 | claude-opus-4-6 |
| חוקר, QA, מייצא | claude-sonnet-4-6 | claude-sonnet-4-6 ✅ |
| מנהל ידע | deepseek-v4-pro | deepseek-v4-pro ✅ |
**לא ברור:** האם CEO/מנתח/כותב **אמורים** לרוץ על Opus (בחירה מכוונת לאיכות) ורק קבצי instructions לא עודכנו? או שה-DB צריך להתעדכן?
### 3. HEARTBEAT.md Reference (🟢 נמוך)
קובץ `legal-researcher.md` לא מפנה ל-`HEARTBEAT.md` בפירוש בתחילת הקובץ. שאר הסוכנים כן עושים זאת.
---
## רשימת תיקונים לפי עדיפות
### 🔴 קריטי — לתקן לפני תיק הבא
1. **`legal-exporter.md` + `web/app.py`/`drafting.py`:** אחד הדברים:
- תיקן הנחיות: שנה `טיוטה-V``טיוטה-v` (v קטנה) בכל המקומות
- **ועוד:** הוסף לקובץ הנחיות שלב: "אחרי export מוצלח — עדכן `case.status = 'exported'` דרך MCP או API"
2. **תיק 8174-24 — §ז חסר:** בדוק אם שלב המחקר הושלם. אם לא — הפעל חוקר מחדש לתיק זה.
### 🟠 גבוה — לתקן בשבוע הקרוב
3. **Skills CMPA:** הרץ:
```bash
PAPERCLIP_BOARD_API_KEY=$(mcp__infisical__get-secret \
--projectId 9a77b161-f70c-4dd3-9d67-b7ab850cef51 \
--environmentSlug nautilus --secretPath /paperclip --secretName BOARD_API_KEY) \
python ~/legal-ai/scripts/sync_agents_across_companies.py --verify
```
החלט אם להוסיף legal-* skills ל-CMPA ואם כן — הרץ `--apply`.
4. **`legal-researcher.md`:** הוסף תת-סעיף עם הוראות ל-`search_internal_decisions`:
- מתי להשתמש (החלטות פנימיות דפנה שלא בקורפוס הציבורי)
- מה ההבדל מ-`search_decisions`
5. **`legal-exporter.md` — נתיב legal-docx:** שנה מ-hardcoded UUID ל-דינמי:
```
אם $PAPERCLIP_COMPANY_ID = 42a7acd0... → CMP path
אם $PAPERCLIP_COMPANY_ID = 8639e837... → CMPA path
```
6. **`legal-ceo.md` — Project ID:** הסר את ה-hardcoded ID של 1130-25. החלף בהוראה: "השתמש ב-`projects_list` לקבלת project_id הנכון לפי חברה ולתיק".
### 🟡 בינוני — לתקן בחודש הקרוב
7. **Model documentation:** החלט על גרסאות מודל לכל סוכן ועדכן גם הנחיות גם DB. עדיף: שמור הנחיות כ-source of truth ועדכן DB דרך `sync_agents_across_companies.py --apply`.
8. **`legal-qa.md` — הבהרת corpus_queries_logged:** הוסף: "הבדיקה היא קריאת `validate_decision` עם `check_corpus_log=true` / או grep ידני בקובץ `analysis-and-research.md` לסעיף ז".
9. **`legal-ceo.md` — maxConcurrentRuns:** עדכן DB ל-maxConcurrentRuns=1 (או 2 אם CEO רוצה מקביליות מכוונת).
10. **`legal-proofreader.md` — {issue-id} placeholder:** שנה ל-`$PAPERCLIP_TASK_ID` באופן מפורש.
11. **`legal-researcher.md` — HEARTBEAT.md link:** הוסף בשורה 1: `> ראה גם: HEARTBEAT.md לחוקים הכלליים`.
### 🟢 נמוך — future improvement
12. **מנהל ידע — ownership:** הוסף ל-CLAUDE.md הנחיה: "Curator proposals ב-comments → חיים מאשר ידנית → commits ל-SKILL.md ו-lessons.md".
---
## אימות (לאחר תיקונים)
```bash
# 1. שלוף API key
PAPERCLIP_BOARD_API_KEY=$(mcp__infisical__get-secret \
--projectId 9a77b161-f70c-4dd3-9d67-b7ab850cef51 \
--environmentSlug nautilus --secretPath /paperclip --secretName BOARD_API_KEY)
# 2. בדוק drift
python ~/legal-ai/scripts/sync_agents_across_companies.py --verify
# 3. בדוק freshness של הנחיות
python ~/legal-ai/scripts/sync_agents_across_companies.py --check-instructions
# 4. בדוק שסוכני CMPA עובדים עם skills נכונים
PGPASSWORD="paperclip" psql -h 127.0.0.1 -p 54329 -U paperclip -d paperclip -c "
SELECT a.name, array_agg(s.name ORDER BY s.name) as skills
FROM agents a
JOIN companies c ON a.company_id = c.id
LEFT JOIN agent_skills ask ON ask.agent_id = a.id
LEFT JOIN skills s ON ask.skill_id = s.id
WHERE c.name LIKE '%השבחה%' AND (a.is_deleted = false OR a.is_deleted IS NULL)
GROUP BY a.id ORDER BY a.name;
"
```

View File

@@ -360,13 +360,9 @@ async def write_block(
post_hearing_context=post_hearing_context,
)
# Restructure: sources first, then instructions
prompt = (
f"## חומרי מקור (מסמכים מלאים — צטט מהם מילה במילה כשאפשר):\n\n"
f"{source_context}\n\n"
f"---\n\n"
f"{formatted_prompt}"
)
# source_context is already embedded inside formatted_prompt via {source_context} in the
# template. Do NOT prepend it again — doing so doubles the prompt size (was 465K chars).
prompt = formatted_prompt
if instructions:
prompt += f"\n\n## הנחיות נוספות:\n{instructions}"
@@ -377,6 +373,19 @@ async def write_block(
if not dir_doc.get("approved"):
raise ValueError("לא ניתן לכתוב בלוק דיון ללא כיוון מאושר. הפעל brainstorm → approve_direction קודם.")
# Guard against context overflow before calling claude -p.
# Sonnet: 200K context → ~800K chars max; Opus: 200K context → same.
# In practice the CLI has crashed on prompts above ~400K chars, so use
# that as a conservative ceiling (well below the token limit).
_MAX_PROMPT_CHARS = 400_000
if len(prompt) > _MAX_PROMPT_CHARS:
raise RuntimeError(
f"Prompt too large for {block_id}: {len(prompt):,} chars "
f"(limit {_MAX_PROMPT_CHARS:,}). "
f"source_context: {len(source_context):,} chars. "
f"Reduce documents or call extract_appraiser_facts first."
)
# Call Claude via Claude Code session (no API)
model_key = block_cfg["model"]
timeout = claude_session.LONG_TIMEOUT if model_key == "opus" else claude_session.DEFAULT_TIMEOUT
@@ -414,16 +423,35 @@ def _build_case_context(case: dict, decision: dict | None) -> str:
- תוצאה: {outcome_heb}"""
# Which doc_types are relevant per block.
# None → skip source docs entirely (block uses other context, e.g. claims_context)
# [] → include all doc types (default for unspecified blocks)
# [..] → include only the listed doc_type values
_BLOCK_DOC_TYPES: dict[str, list[str] | None] = {
"block-he": None, # only case_context needed; no full docs
"block-vav": ["appeal", "protocol"], # כתב ערר + פרוטוקול ועדה
"block-zayin": None, # claims_context is sufficient
"block-chet": ["protocol"], # פרוטוקול + השלמות טיעון
"block-tet": ["appraisal"], # שומות בלבד
# block-yod, block-yod-alef, block-he etc. default → all docs
}
async def _build_source_context(case_id: UUID, block_id: str) -> str:
"""Get full document texts for the block.
"""Get document texts for the block, filtered by relevance.
Per Anthropic best practices: send full source documents, not truncated excerpts.
Place documents at the TOP of the prompt (before instructions) for 30% better recall.
For grounding: instruct Claude to cite word-for-word from these documents.
Per-block filtering prevents context overflow on large cases (9+ docs).
"""
allowed = _BLOCK_DOC_TYPES.get(block_id, []) # [] sentinel = not in map → all docs
if allowed is None:
return "" # this block doesn't need raw source docs
docs = await db.list_documents(case_id)
context_parts = []
for doc in docs:
if allowed and doc["doc_type"] not in allowed:
continue
text = await db.get_document_text(UUID(doc["id"]))
if text:
context_parts.append(f"--- מסמך: {doc['title']} ({doc['doc_type']}) ---\n{text}")

View File

@@ -72,6 +72,9 @@ async def query(
"""
full_prompt = f"{system}\n\n{prompt}" if system else prompt
if len(full_prompt) > 150_000:
logger.warning("Large prompt: %d chars — may hit context limits", len(full_prompt))
cmd = [
"claude", "-p",
"--output-format", "json",
@@ -110,7 +113,8 @@ async def query(
if proc.returncode != 0:
stderr = stderr_b.decode("utf-8", errors="replace").strip()[:500] or "unknown error"
raise RuntimeError(f"Claude CLI failed (exit {proc.returncode}): {stderr}")
size_info = f"; prompt_len={len(full_prompt):,} chars" if len(full_prompt) > 100_000 else ""
raise RuntimeError(f"Claude CLI failed (exit {proc.returncode}): {stderr}{size_info}")
stdout = stdout_b.decode("utf-8", errors="replace").strip()
if not stdout:

View File

@@ -2052,9 +2052,19 @@ async def list_external_case_law(
offset: int = 0,
source_kind: str = "external_upload",
) -> list[dict]:
"""List chair-uploaded precedents, with simple filters."""
"""List chair-uploaded precedents, with simple filters.
source_kind="all_committees" expands to: source_kind='internal_committee'
OR (source_kind='external_upload' AND source_type='appeals_committee').
"""
pool = await get_pool()
conditions = [f"source_kind = '{source_kind}'"]
if source_kind == "all_committees":
conditions = [
"(source_kind = 'internal_committee' OR "
"(source_kind = 'external_upload' AND source_type = 'appeals_committee'))"
]
else:
conditions = [f"source_kind = '{source_kind}'"]
params: list = []
idx = 1
if practice_area:
@@ -2488,19 +2498,17 @@ async def precedent_library_stats() -> dict:
pool = await get_pool()
async with pool.acquire() as conn:
total = await conn.fetchval(
"SELECT COUNT(*) FROM case_law WHERE source_kind = 'external_upload'"
"SELECT COUNT(*) FROM case_law"
)
by_practice = await conn.fetch(
"""SELECT practice_area, COUNT(*) AS n
FROM case_law
WHERE source_kind = 'external_upload'
GROUP BY practice_area
ORDER BY n DESC"""
)
by_level = await conn.fetch(
"""SELECT precedent_level, COUNT(*) AS n
FROM case_law
WHERE source_kind = 'external_upload'
GROUP BY precedent_level
ORDER BY n DESC"""
)

View File

@@ -123,7 +123,7 @@ SUMMARY_STRATEGIES = {
DISCUSSION_RULES: dict[str, list[str]] = {
"universal": [
"פרק הדיון = אסה רציפה. אין כותרות משנה (H2/H3). מעברים רק עם ביטויי מעבר טקסטואליים.",
"פרק הדיון = מאסה רציפה. אין כותרות משנה (H2/H3). מעברים רק עם ביטויי מעבר טקסטואליים.",
"חריג יחיד לכותרות משנה: נושאים נפרדים לחלוטין (למשל: הקלה בגובה + התייחסות לטענות נוספות).",
"טווח אורך סעיפים: 20 עד 600+ מילים. סעיף עם ציטוט מקיף = בלוק אחד שלם, לא שבירה לסעיפים קצרים.",
],
@@ -485,6 +485,7 @@ CONTENT_CHECKLISTS: dict[str, str] = {
- שווי מקרקעין — מצב קודם ומצב חדש (שיטת השוואה / יחידות תועלת)
- עלויות עודפות (חניה, מטלות ציבוריות, תשתיות)
- מקדמי זמינות, שיעורי הפקעה
- הכרעה מפוצלת (bifurcation) — כשהוועדה מאשרת חבות אך ממנה שמאי מייעץ: ביטויי גישור ("ניתן יהיה לעלות בפני השמאי המייעץ"), נוסחת מינוי, הפניה לתקנות סדרי דין התשס"ט-2008, הוראות המשך (30 יום להשגות). ללא סיכום — ישירות לחתימה. ראה: 8070/25
### ד. שאלות משפטיות (לפי רלוונטיות)
- פטורים — דירת מגורים (ס' 19(ג)(1)), שטח עד 140 מ"ר, תא משפחתי
@@ -493,6 +494,7 @@ CONTENT_CHECKLISTS: dict[str, str] = {
- מקרקעי ישראל — הסדרים מיוחדים (ס' 21 לתוספת השלישית)
- שומות מוסכמות — תוקף, משמעות, "בלתי נצפה מראש"
- פרשנות תכניות — ייעוד, שימושים מותרים, מדיניות ועדה מקומית
- טענת "תכנית צל = זכות מוקנית" — ניתוח תלת-שכבתי: (1) נורמטיבית — תכנית צל = המחשה, לא מקור נורמטיבי; (2) פרוצדורלית — הקלה ניתנת פר-מבקש, לא זכות כללית; (3) שמאית — משקל הסתברותי בהערכת ההשבחה, לא במישור המשפטי. ראה: 8070/25
### ה. ניתוח שמאי (כשיש שומה מכרעת)
- האם השומה מבוססת על מסד עובדתי הולם?

View File

@@ -55,6 +55,9 @@ def _is_placeholder(text: str) -> bool:
for ph in CHAIR_POSITION_PLACEHOLDERS:
if ph in stripped:
return True
# Extended placeholders: [ימולא ע"י יו"ר הוועדה — extra descriptive text]
if re.match(r'^\[ימולא\b', stripped):
return True
return False

View File

@@ -237,7 +237,10 @@ async def case_list(status: str = "", limit: int = 50) -> str:
"""רשימת תיקי ערר עם אפשרות סינון לפי סטטוס.
Args:
status: סינון לפי סטטוס (new, in_progress, drafted, reviewed, final). ריק = הכל
status: סינון לפי סטטוס (new, processing, proofread, documents_ready, analyst_verified,
research_complete, outcome_set, direction_pending, direction_approved,
analysis_enriched, ready_for_writing, drafted, qa_passed, qa_failed,
exported, done). ריק = הכל
limit: מספר תוצאות מקסימלי
"""
cases = await db.list_cases(status=status or None, limit=limit)
@@ -271,6 +274,10 @@ async def case_update(
decision_date: str = "",
tags: list[str] | None = None,
expected_outcome: str = "",
appellants: list[str] | None = None,
respondents: list[str] | None = None,
property_address: str = "",
permit_number: str = "",
) -> str:
"""עדכון פרטי תיק.
@@ -284,6 +291,10 @@ async def case_update(
decision_date: תאריך החלטה (YYYY-MM-DD)
tags: תגיות
expected_outcome: תוצאה צפויה (rejection/partial_acceptance/full_acceptance/betterment_levy)
appellants: רשימת עוררים חדשה
respondents: רשימת משיבים חדשה
property_address: כתובת נכס חדשה
permit_number: מספר תכנית/בקשה חדש
"""
from datetime import date as date_type
@@ -322,6 +333,14 @@ async def case_update(
fields["tags"] = tags
if expected_outcome:
fields["expected_outcome"] = expected_outcome
if appellants is not None:
fields["appellants"] = appellants
if respondents is not None:
fields["respondents"] = respondents
if property_address:
fields["property_address"] = property_address
if permit_number:
fields["permit_number"] = permit_number
updated = await db.update_case(UUID(case["id"]), **fields)

View File

@@ -259,6 +259,14 @@ async def apply_diff(mirror_id: str, agent_name: str, diff: dict) -> list[str]:
if "runtime_config" in diff:
patch_body["runtimeConfig"] = diff["runtime_config"]["to"]
# Stamp claude_md_mtime + last_synced into metadata
mtime = diff.get("_claude_md_mtime")
if mtime:
current_meta = dict(patch_body.get("metadata") or {})
current_meta["claude_md_mtime"] = mtime
current_meta["claude_md_last_synced"] = datetime.now(timezone.utc).isoformat()
patch_body["metadata"] = current_meta
if patch_body:
status, data = await call_patch(mirror_id, patch_body)
if status >= 400:
@@ -278,12 +286,73 @@ async def apply_diff(mirror_id: str, agent_name: str, diff: dict) -> list[str]:
return errors
def get_claude_md_mtime(adapter_config: dict) -> str | None:
"""Return Unix mtime of the agent's instructionsFilePath, or None if file missing."""
path = adapter_config.get("instructionsFilePath", "")
if not path or not os.path.exists(path):
return None
return str(int(os.path.getmtime(path)))
async def check_instructions(agents: list[dict]) -> bool:
"""Print a report of all agents' instruction files. Returns True if all OK."""
from datetime import datetime
all_ok = True
print(f"\n{'Agent':<30} {'File':<55} {'Status':<12} {'Size':>7} {'Modified'}")
print("-" * 115)
for agent in agents:
name = (agent.get("name") or agent.get("id") or "?")[:29]
try:
adapter_cfg = agent.get("adapter_config") or {}
if isinstance(adapter_cfg, str):
adapter_cfg = json.loads(adapter_cfg)
except (json.JSONDecodeError, TypeError):
print(f"{name:<30} {'(malformed adapter_config in DB)':<55} {'⚠ ERROR':<12}")
continue
file_path = adapter_cfg.get("instructionsFilePath", "")
if not file_path:
print(f"{name:<30} {'(none)':<55} {'⚠ NOT SET':<12}")
continue
if not os.path.exists(file_path):
print(f"{name:<30} {file_path[-54:]:<55} {'❌ MISSING':<12}")
all_ok = False
continue
stat = os.stat(file_path)
size_kb = stat.st_size // 1024
mtime = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
# Check for drift vs DB metadata
try:
metadata = agent.get("metadata") or {}
if isinstance(metadata, str):
metadata = json.loads(metadata)
except (json.JSONDecodeError, TypeError):
metadata = {}
db_mtime = metadata.get("claude_md_mtime", "")
actual_mtime = str(int(stat.st_mtime))
drift = " ⚠ DRIFT" if db_mtime and db_mtime != actual_mtime else ""
print(f"{name:<30} {file_path[-54:]:<55} {'✅ OK':<12} {size_kb:>5}KB {mtime}{drift}")
print()
return all_ok
async def main() -> None:
p = argparse.ArgumentParser()
g = p.add_mutually_exclusive_group(required=True)
g.add_argument("--verify", action="store_true", help="Show current drift, no changes")
g.add_argument("--dry-run", action="store_true", help="Show what would change")
g.add_argument("--apply", action="store_true", help="Backup + apply changes")
g.add_argument("--check-instructions", action="store_true",
help="Scan all agents' instructionsFilePath and report missing/outdated files")
p.add_argument("--only", help="Sync only the named agent (e.g., 'עוזר משפטי')")
args = p.parse_args()
@@ -295,6 +364,11 @@ async def main() -> None:
finally:
await conn.close()
if args.check_instructions:
all_agents = master_agents + mirror_agents
all_ok = await check_instructions(all_agents)
sys.exit(0 if all_ok else 1)
mirror_by_name = {a["name"]: a for a in mirror_agents}
print(f"\n=== Master (CMP, 1xxx): {len(master_agents)} agents ===")
@@ -332,6 +406,14 @@ async def main() -> None:
return
# APPLY
# Pre-flight: abort if any master agent is missing its instructions file
print("🔍 Pre-flight: checking instruction files...")
all_ok = await check_instructions(master_agents)
if not all_ok:
print("❌ Abort: one or more instruction files are missing. Fix before --apply.")
sys.exit(1)
print("✅ Pre-flight passed.\n")
print(f"\n=== Backup ===")
backup_path = backup_agents_table()
print(f"{backup_path}")
@@ -340,6 +422,11 @@ async def main() -> None:
all_errors: list[str] = []
for master, mirror, diff in plan:
print(f"\n{master['name']} ({mirror['id']})")
# Inject mtime into diff so apply_diff can stamp metadata
master_ac = master.get("adapter_config") or {}
mtime = get_claude_md_mtime(master_ac)
if mtime:
diff["_claude_md_mtime"] = mtime
errors = await apply_diff(mirror["id"], master["name"], diff)
if errors:
for e in errors:

View File

@@ -283,6 +283,16 @@ description: This skill should be used when writing legal decisions (החלטו
**ערר היטל השבחה:**
פתיחה ישירה עם מסקנה. ניתוח ישיר - ציטוטי פסיקה מרובים. סיום יבש.
**ערר היטל השבחה — הכרעה מפוצלת (שמאי מייעץ):**
תת-מסלול חוזר: הוועדה מאשרת את עצם החבות אך אינה קובעת את גובה ההיטל — ממנה שמאי מייעץ. פתיחה: זהה למסלול הכללי — "עניינו של ערר זה בדרישת תשלום היטל השבחה...". ללא סיפור תכנוני רחב. דיון — שלב משפטי: הכרעה בשאלת עצם החבות. ניתוח הטענה המרכזית (בד"כ "זכות מוקנית") מול ההקלה שהתבקשה. ציטוטי פסיקה inline — הפניה להחלטות ועדת ערר קודמות ללא בלוק ציטוט מלא. ביטויי מפתח: "אנו סבורים כי...", "בניגוד לעמדת העורר...", "לא ניתן לטעון כי...". מעבר לשלב השמאי — 3 ביטויי גישור: (א) "בכל הנוגע לטענות לגבי מקדמים... וכל טענה בעלת אופי שמאי **ניתן יהיה לעלות בפני השמאי המייעץ**." (ב) "על כן, לאור האמור **אנו ממנים שמאי מייעץ** אשר יערוך שומה להערכת ההשבחה במקרקעין כתוצאה מאישור ההקלה..." (ג) "השמאי המייעץ ינהל את הדיון **בהתאם לתקנות התכנון והבניה (סדרי דין בבקשה להכרעה לפני שמאי מכריע או שמאי מייעץ), התשס"ט-2008**." — נוסחה קבועה. הוראות המשך: "לאחר קבלת השומה המייעצת יהיו רשאים הצדדים להגיש את השגותיהם בתוך 30 יום לוועדת הערר ולאחר מכן תתקבל החלטה באשר לאופן קידום ההליך." סיום: **ללא** כותרת "סיכום" / "סוף דבר" — זורם ישירות מהוראות המינוי לחתימה "ניתנה פה אחד היום...". הוצאות: לא מוזכרות (ההליך טרם הסתיים). ראה: נווה יעקב 8070/25.
**ערר היטל השבחה — מסגרת תלת-שכבתית לניתוח "תכנית צל":**
כשעורר טוען ש"תכנית צל" מאושרת הופכת זכויות להקלה לזכויות מוקנות, הניתוח מתבצע בשלוש שכבות נפרדות:
*שכבה 1 — נורמטיבית* (שלילת המעמד המשפטי): "תכנית צל אינה מקור נורמטיבי לאישור זכויות... אינה 'מבטיחה' אישור זכויות עבור השכן." ביטויי מפתח: "תכנית צל הינה תכנית המבקשת... להראות היתכנות בניה על ידי יתר בעלי הזכויות ו/או השלכת הבניה עליהם, על הבניין ועל הסביבה." כלל: תכנית צל = המחשה, לא מקור נורמטיבי.
*שכבה 2 — פרוצדורלית* (ההקלה ניתנת פר-מבקש): "גם בהיתר חבקין התבקשה הקלה שאושרה, הקלה אך ורק להיתר שהתבקש ולזכויות שהתבקשו מכוחו. ההקלה לא התבקשה עבור כל דיירי הבניין... בקשה להקלה ופרסומה יש בצידם שיקול דעת... באופן ייחודי לכל בקשה לגופה." כלל: אישור הקלה לדייר א' ≠ זכות מוקנית לדייר ב'.
*שכבה 3 — שמאית* (הכרה בערך ראייתי, ניתוב למישור הנכון): "העובדה שאושרה תכנית צל יש בה מידת וודאות גבוהה יותר באשר לסיכוי כי תאושר ההקלה... ולכך **משקל שמאי** בהערכת ההשבחה." כלל: תכנית צל משפיעה על **ההסתברות** (מישור שמאי), לא על **הזכות** (מישור משפטי).
סדר הניתוח: תמיד שכבה 1 → 2 → 3. לא לדלג — גם אם שכבה 1 מכריעה, יש ערך בכל שלוש לביסוס ולמניעת ערעור. ראה: נווה יעקב 8070/25.
**ערר רישוי שמתקבל חלקית — מסלול מיפוי מתחים + ניתוח נושאי:**
פתיחה במיפוי מתחים (3-6 סעיפים): הקשר כללי קצר (1-2 פסקאות), רשימת נקודות מתח ספציפיות בתיק (4-6 בולטים), מעבר לניתוח. אין שימוש בשכבות/עיגולים קונצנטריים — ניתוח לפי נושאים: כל נושא מקבל טיפול מלא (הצגה → ציטוט הוראות תכנית → פסיקה → מסקנה). נושא חניה/תשתיות מקבל טיפול מעמיק במיוחד עם ציטוטים ישירים מהוראות תכנית ונספחים. טענות ספציפיות (מטרדים, עצים, בור מים) — 1-2 סעיפים תמציתיים לכל אחת. סיכום מינימלי — רק הוראות אופרטיביות (2-3 סעיפים). ראה: בית הכרם 1126/25.

View File

@@ -252,82 +252,10 @@ new Table({
## Tracked Changes — עקוב אחר שינויים
### שם מחבר בעברית
```xml
<w:del w:id="10" w:author="עו&quot;ד כהן" w:date="2026-02-06T09:00:00Z">
```
ראה [`references/tracked-changes.md`](references/tracked-changes.md) — XML patterns לשינוי ערך, מחיקת סעיף, RTL PROPS, קבלה/דחייה.
### שינוי ערך (סכום, תאריך, תקופה)
פצל את הטקסט ועטוף רק את הערך שמשתנה:
```xml
<w:r><w:rPr>...RTL PROPS...</w:rPr>
<w:t xml:space="preserve">שכר הטרחה יעמוד על סך של </w:t></w:r>
<w:del w:id="10" w:author="עו&quot;ד כהן" w:date="...">
<w:r><w:rPr>...RTL PROPS...</w:rPr><w:delText>750</w:delText></w:r>
</w:del>
<w:ins w:id="11" w:author="עו&quot;ד כהן" w:date="...">
<w:r><w:rPr>...RTL PROPS...</w:rPr><w:t>850</w:t></w:r>
</w:ins>
<w:r><w:rPr>...RTL PROPS...</w:rPr>
<w:t xml:space="preserve"> ש״ח לשעת עבודה</w:t></w:r>
```
### מחיקת סעיף שלם
סמן גם את ה-paragraph mark כ-deleted:
```xml
<w:p>
<w:pPr>
<w:bidi/>
<w:jc w:val="both"/>
<w:rPr>
<w:del w:id="20" w:author="עו&quot;ד כהן" w:date="..."/>
</w:rPr>
</w:pPr>
<w:del w:id="21" w:author="עו&quot;ד כהן" w:date="...">
<w:r><w:rPr>...RTL PROPS...</w:rPr>
<w:delText>הסעיף שנמחק</w:delText></w:r>
</w:del>
</w:p>
```
### RTL PROPS — בלוק rPr מלא לכל run
```xml
<w:rPr>
<w:rFonts w:ascii="David" w:cs="David" w:eastAsia="David" w:hAnsi="David"/>
<w:sz w:val="24"/>
<w:szCs w:val="24"/>
<w:rtl/>
</w:rPr>
```
### קבלה/דחייה של שינויים
**קבלת Insertion:**
```
לפני: <w:ins w:id="5" w:author="..."><w:r>...<w:t>טקסט חדש</w:t></w:r></w:ins>
אחרי: <w:r>...<w:t>טקסט חדש</w:t></w:r>
→ הסר את תגית <w:ins> ושמור את התוכן הפנימי.
```
**דחיית Insertion:**
```
לפני: <w:ins w:id="5" w:author="..."><w:r>...<w:t>טקסט חדש</w:t></w:r></w:ins>
אחרי: (הסר לחלוטין)
→ מחק את כל בלוק ה-<w:ins> כולל תוכנו.
```
**קבלת מחיקה:**
```
לפני: <w:del w:id="10" w:author="..."><w:r>...<w:delText>טקסט שנמחק</w:delText></w:r></w:del>
אחרי: (הסר לחלוטין)
→ מחק את כל בלוק ה-<w:del> כולל תוכנו — המחיקה מתקבלת.
```
**שחזור טקסט מקורי (דחיית מחיקה):**
```
לפני: <w:del w:id="10" w:author="..."><w:r>...<w:delText>טקסט מקורי</w:delText></w:r></w:del>
אחרי: <w:r>...<w:t>טקסט מקורי</w:t></w:r>
→ הסר <w:del>, החלף <w:delText> ב-<w:t>, הסר <w:del> מ-rPr אם קיים.
```bash
python /mnt/skills/public/docx/scripts/comment.py unpacked/ 0 "הערה" --author "עו״ד כהן"
```
---
@@ -397,72 +325,6 @@ python /mnt/skills/public/docx/scripts/pack.py unpacked/ output.docx --original
---
## הערות שוליים (Footnotes)
**השימוש המרכזי:** הפניות לחקיקה ופסיקה.
```javascript
const { FootnoteReferenceRun } = require('docx');
// 1. הגדרה ב-Document:
const doc = new Document({
footnotes: {
1: { children: [new Paragraph({
bidirectional: true, alignment: AlignmentType.START, // ✅ START
children: [new TextRun({
text: "חוק החוזים (חלק כללי), התשל״ג-1973, סעיף 12.",
font: "David", size: 20, rightToLeft: true // 10pt להערות שוליים
})]
})] },
2: { children: [new Paragraph({
bidirectional: true, alignment: AlignmentType.START,
children: [new TextRun({
text: "ע״א 1234/20 כהן נ׳ לוי, פסקה 15 (פורסם בנבו, 1.1.2024).",
font: "David", size: 20, rightToLeft: true
})]
})] },
},
// ...sections
});
// 2. הפניה בגוף הטקסט:
new Paragraph({
bidirectional: true, alignment: AlignmentType.BOTH,
children: [
new TextRun({ text: "חובת תום הלב", font: "David", size: 24, rightToLeft: true }),
new FootnoteReferenceRun(1),
new TextRun({ text: " חלה על כל שלבי המשא ומתן", font: "David", size: 24, rightToLeft: true }),
new FootnoteReferenceRun(2),
new TextRun({ text: ".", font: "David", size: 24, rightToLeft: true }),
]
})
```
### תיקון RTL בהערות שוליים (post-unpack)
docx-js לא מגדיר RTL מלא בהערות שוליים. אחרי unpack, צריך לתקן ב-`word/footnotes.xml`:
```xml
<!-- 1. הוסף pStyle + bidi לכל הערת שוליים: -->
<w:footnote w:id="1">
<w:p>
<w:pPr>
<w:pStyle w:val="FootnoteText"/>
<w:bidi/>
<w:jc w:val="start"/>
</w:pPr>
...
<!-- 2. הוסף rtl ל-footnoteRef run: -->
<w:r>
<w:rPr>
<w:rStyle w:val="FootnoteReference"/>
<w:rtl/>
</w:rPr>
<w:footnoteRef/>
</w:r>
```
---
## מרווח שורות (Line Spacing)
**דרישת בתי המשפט:** בדרך כלל 1.5 שורות.
@@ -482,48 +344,6 @@ spacing: { line: 360, lineRule: LineRuleType.AUTO, before: 120, after: 120 }
---
## תוכן עניינים (TOC)
**⚠️ חובה: TOC ידני (לא TableOfContents).**
`TableOfContents` של docx-js מייצר שדה שוורד מעדכן ב-F9 ומאבד הגדרות RTL.
```javascript
const { Tab, TabStopType, LeaderType, PageBreak } = require('docx');
// שורת TOC ידנית
const tocEntry = (text, pageNum, opts = {}) => new Paragraph({
bidirectional: true,
spacing: { after: 60, line: 276, lineRule: LineRuleType.AUTO },
...(opts.indent ? { indent: { right: opts.indent } } : {}),
tabStops: [{ type: TabStopType.RIGHT, position: 9026, leader: LeaderType.DOT }],
children: [
new TextRun({
text, font: "David", size: 24, rightToLeft: true,
bold: opts.bold || false,
}),
new TextRun({ children: [new Tab()], font: "David", rightToLeft: true }),
new TextRun({
text: String(pageNum), font: "David", size: 24, rightToLeft: true,
}),
]
});
// שימוש:
new Paragraph({
bidirectional: true, alignment: AlignmentType.CENTER,
spacing: { after: 200 },
children: [new TextRun({
text: "תוכן עניינים", font: "David", size: 32, bold: true, rightToLeft: true
})]
}),
tocEntry("פרק א׳ — הגדרות כלליות", 2, { bold: true }),
tocEntry("1. הגדרות יסוד", 2, { indent: 400 }),
tocEntry("פרק ב׳ — השירותים", 3, { bold: true }),
new Paragraph({ children: [new PageBreak()] }),
```
---
## קו תחתי (Underline)
```javascript
@@ -544,337 +364,23 @@ underline: { type: UnderlineType.DOUBLE }
---
## מספר סקשנים (Multiple Sections)
## פיצ'רים מתקדמים
**שימוש:** כותרות שונות לנספחים, עמוד לרוחב לטבלאות, שוליים שונים.
```javascript
const doc = new Document({
sections: [
// סקשן 1 — גוף ההסכם
{
properties: {
page: { size: { width: 11906, height: 16838 },
margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } },
bidi: true,
},
headers: {
default: new Header({ children: [new Paragraph({
bidirectional: true, alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "הסכם שירותים", font: "David", size: 20, bold: true, rightToLeft: true })]
})] })
},
children: [ /* ... */ ]
},
// סקשן 2 — נספח עם כותרת שונה
{
properties: {
page: { size: { width: 11906, height: 16838 },
margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } },
bidi: true,
},
headers: {
default: new Header({ children: [new Paragraph({
bidirectional: true, alignment: AlignmentType.START, // ✅ START
children: [new TextRun({ text: "נספח א׳ — לוח תעריפים", font: "David", size: 20, bold: true, rightToLeft: true })]
})] })
},
children: [ /* ... */ ]
}
]
});
```
ראה [`references/advanced-features.md`](references/advanced-features.md):
- **הערות שוליים** — Footnotes עם RTL + תיקון post-unpack ב-footnotes.xml
- **תוכן עניינים** — TOC ידני (אסור `TableOfContents`)
- **מספר סקשנים** — כותרות שונות לנספחים
- **Letterhead** — לוגו/תמונה בכותרת
- **היפרלינקים** — `ExternalHyperlink` עם color+underline ידני (לא `style: "Hyperlink"`)
---
## לוגו/תמונה בכותרת (Letterhead)
## תבניות מסמכים
```javascript
const { ImageRun } = require('docx');
const logoBuffer = fs.readFileSync('/path/to/logo.png');
headers: {
default: new Header({
children: [
new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new ImageRun({
data: logoBuffer,
transformation: { width: 200, height: 60 }, // pixels
type: "png",
}),
],
}),
new Paragraph({
bidirectional: true, alignment: AlignmentType.CENTER,
children: [new TextRun({
text: "משרד עורכי דין ישראלי ושות׳",
font: "David", size: 20, bold: true, rightToLeft: true
})],
}),
],
}),
}
```
**הערה:** תמונה חייבת להיות קובץ אמיתי — לבקש מהמשתמש אם אין.
---
## היפרלינקים
```javascript
const { ExternalHyperlink, UnderlineType } = require('docx');
new Paragraph({
bidirectional: true,
children: [
new TextRun({ text: "ראה: ", font: "David", size: 24, rightToLeft: true }),
new ExternalHyperlink({
link: "https://www.nevo.co.il/law_html/law01/073_002.htm",
children: [new TextRun({
text: "חוק החוזים באתר נבו",
font: "David", size: 24, rightToLeft: true,
color: "0563C1",
underline: { type: UnderlineType.SINGLE },
})],
}),
]
})
```
**⚠️ אזהרות:**
- **לא להשתמש ב-`style: "Hyperlink"`** — מפריע ל-RTL!
- **לא להוסיף `alignment: AlignmentType.RIGHT`** — `bidirectional: true` מספיק
---
## תבניות מסמכים — Document Templates
### תבנית 1: כתב טענות (בקשה, תביעה, הגנה, ערעור)
```javascript
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
AlignmentType, LevelFormat, BorderStyle, WidthType } = require('docx');
const PAGE_WIDTH = 11906;
const MARGINS = { top: 1134, right: 1134, bottom: 1134, left: 1134 };
const CONTENT_WIDTH = PAGE_WIDTH - MARGINS.left - MARGINS.right;
const noBorder = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" };
const noBorders = { top: noBorder, bottom: noBorder, left: noBorder, right: noBorder };
// Header בית משפט — טבלה עם שם בית המשפט (ימין) ומספר תיק (שמאל)
function courtHeader(courtName, caseNumber) {
return new Table({
width: { size: CONTENT_WIDTH, type: WidthType.DXA },
columnWidths: [CONTENT_WIDTH / 2, CONTENT_WIDTH / 2],
visuallyRightToLeft: true,
rows: [
new TableRow({
children: [
new TableCell({
width: { size: CONTENT_WIDTH / 2, type: WidthType.DXA },
borders: noBorders,
children: [new Paragraph({
bidirectional: true, alignment: AlignmentType.START,
children: [new TextRun({ text: courtName, bold: true, font: "David", size: 26, rightToLeft: true })]
})]
}),
new TableCell({
width: { size: CONTENT_WIDTH / 2, type: WidthType.DXA },
borders: noBorders,
children: [new Paragraph({
bidirectional: true, alignment: AlignmentType.END,
children: [new TextRun({ text: caseNumber, bold: true, font: "David", size: 26, rightToLeft: true })]
})]
})
]
})
]
});
}
// כותרת ראשית ממורכזת עם קו תחתון
function mainTitle(text) {
return new Paragraph({
bidirectional: true, alignment: AlignmentType.CENTER,
spacing: { before: 300, after: 300 },
children: [new TextRun({ text, bold: true, font: "David", size: 28, rightToLeft: true, underline: {} })]
});
}
// כותרת משנה מיושרת לימין עם קו תחתון
function subHeading(text) {
return new Paragraph({
bidirectional: true, alignment: AlignmentType.START,
spacing: { before: 240, after: 120 },
children: [new TextRun({ text, bold: true, font: "David", size: 24, rightToLeft: true, underline: {} })]
});
}
// שימוש:
const doc = new Document({
numbering: {
config: [{
reference: "legal-clauses",
levels: [{
level: 0, format: LevelFormat.DECIMAL, text: "%1.",
alignment: AlignmentType.START, suffix: "tab",
style: { paragraph: { indent: { left: 360, hanging: 360 } } }
}]
}]
},
sections: [{
properties: {
page: { size: { width: PAGE_WIDTH, height: 16838 }, margin: MARGINS },
bidi: true
},
children: [
courtHeader("בית המשפט המחוזי בתל אביב", "ת\"א 12345-01-26"),
mainTitle("כתב תביעה"),
// ... פרטי צדדים, סעיפים, חתימה
]
}]
});
```
### תבנית 2: מכתב התראה
```javascript
// מכתב התראה — ללא header בית משפט, עם פרטי משרד
function letterHeader(firmName, address, phone, email) {
return [
new Paragraph({
bidirectional: true, alignment: AlignmentType.START,
children: [new TextRun({ text: firmName, bold: true, font: "David", size: 28, rightToLeft: true })]
}),
new Paragraph({
bidirectional: true, alignment: AlignmentType.START,
children: [new TextRun({ text: address, font: "David", size: 22, rightToLeft: true })]
}),
new Paragraph({
bidirectional: true, alignment: AlignmentType.START,
spacing: { after: 300 },
children: [new TextRun({ text: `טל': ${phone} | ${email}`, font: "David", size: 22, rightToLeft: true })]
}),
];
}
function subjectLine(text) {
return new Paragraph({
bidirectional: true, alignment: AlignmentType.CENTER,
spacing: { before: 200, after: 200 },
children: [
new TextRun({ text: "הנדון: ", bold: true, font: "David", size: 24, rightToLeft: true }),
new TextRun({ text, bold: true, font: "David", size: 24, rightToLeft: true, underline: {} })
]
});
}
// שימוש:
sections: [{
properties: { page: { ... }, bidi: true },
children: [
...letterHeader("משרד עו\"ד כהן ושות'", "רח' הרצל 1, תל אביב", "03-1234567", "office@cohen-law.co.il"),
new Paragraph({
bidirectional: true, alignment: AlignmentType.START,
children: [new TextRun({ text: "תאריך: 10.2.2026", font: "David", size: 24, rightToLeft: true })]
}),
new Paragraph({
bidirectional: true, alignment: AlignmentType.START,
spacing: { before: 200 },
children: [new TextRun({ text: "לכבוד: [שם הנמען]", font: "David", size: 24, rightToLeft: true })]
}),
subjectLine("התראה בטרם נקיטת הליכים משפטיים"),
// ... גוף המכתב
]
}]
```
### תבנית 3: הסכם/חוזה
```javascript
// הסכם — הואילים, צדדים, חתימות בשני טורים
function contractTitle(text) {
return new Paragraph({
bidirectional: true, alignment: AlignmentType.CENTER,
spacing: { after: 300 },
children: [new TextRun({ text, bold: true, font: "David", size: 32, rightToLeft: true })]
});
}
function partyClause(label, name, id, address, alias) {
return new Paragraph({
bidirectional: true, alignment: AlignmentType.BOTH,
spacing: { after: 120 },
children: [
new TextRun({ text: `${label}: `, bold: true, font: "David", size: 24, rightToLeft: true }),
new TextRun({ text: `${name}, ח.פ./ת.ז. ${id}, מ${address} (להלן: "`, font: "David", size: 24, rightToLeft: true }),
new TextRun({ text: alias, bold: true, font: "David", size: 24, rightToLeft: true }),
new TextRun({ text: '")', font: "David", size: 24, rightToLeft: true }),
]
});
}
function signatureTable() {
return new Table({
width: { size: CONTENT_WIDTH, type: WidthType.DXA },
columnWidths: [CONTENT_WIDTH / 2, CONTENT_WIDTH / 2],
visuallyRightToLeft: true,
rows: [
new TableRow({
children: [
new TableCell({
borders: noBorders,
children: [
new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "_________________", font: "David", size: 24, rightToLeft: true })] }),
new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "צד א'", font: "David", size: 24, rightToLeft: true })] })
]
}),
new TableCell({
borders: noBorders,
children: [
new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "_________________", font: "David", size: 24, rightToLeft: true })] }),
new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "צד ב'", font: "David", size: 24, rightToLeft: true })] })
]
})
]
})
]
});
}
// שימוש:
sections: [{
properties: { page: { ... }, bidi: true },
children: [
contractTitle("הסכם שירותים"),
new Paragraph({
bidirectional: true, alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "נערך ונחתם בתל אביב ביום __________", font: "David", size: 24, rightToLeft: true })]
}),
partyClause("מצד אחד", "[שם]", "[מספר]", "[כתובת]", "המזמין"),
partyClause("מצד שני", "[שם]", "[מספר]", "[כתובת]", "הספק"),
// הואילים...
// סעיפים...
new Paragraph({
bidirectional: true, alignment: AlignmentType.CENTER,
spacing: { before: 400, after: 300 },
children: [new TextRun({ text: "ולראיה באו הצדדים על החתום:", bold: true, font: "David", size: 24, rightToLeft: true })]
}),
signatureTable()
]
}]
```
ראה [`references/document-templates.md`](references/document-templates.md):
- **תבנית 1: כתב טענות** — `courtHeader()`, `mainTitle()`, `subHeading()` + מספור
- **תבנית 2: מכתב התראה** — `letterHeader()`, `subjectLine()` + פרטי משרד
- **תבנית 3: הסכם/חוזה** — `contractTitle()`, `partyClause()`, `signatureTable()` + הואילים
---
@@ -962,8 +468,11 @@ sections: [{
## קבצי עזר
- **`references/document-types.md`** — מבנים מפורטים ל-9 סוגי מסמכים משפטיים
- **`scripts/create-legal-doc.js`** — סקריפט בסיסי עם כל הגדרות ה-RTL המתוקנות
- **[`references/document-types.md`](references/document-types.md)** — מבנים מפורטים ל-9 סוגי מסמכים
- **[`references/document-templates.md`](references/document-templates.md)** — 3 תבניות מלאות (כתב טענות, מכתב, הסכם)
- **[`references/tracked-changes.md`](references/tracked-changes.md)** — XML patterns לעקוב אחר שינויים
- **[`references/advanced-features.md`](references/advanced-features.md)** — הערות שוליים, TOC, סקשנים, letterhead, hyperlinks
- **`scripts/create-legal-doc.js`** — סקריפט בסיסי עם כל הגדרות RTL
---

View File

@@ -3,3 +3,239 @@
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
---
## Stack
| Layer | Technology | Version |
|-------|-----------|---------|
| Framework | Next.js | 16.2.3 |
| UI | React | 19.2.4 |
| Styles | Tailwind CSS | v4 |
| Components | shadcn/ui | latest via `shadcn` CLI |
| Data fetching | TanStack Query | v5 |
| Forms | react-hook-form + zod | v7 / v4 |
| Language | TypeScript | 5 |
| Direction | Hebrew RTL | `dir="rtl"` throughout |
---
## Commands
```bash
# Regenerate API types from the live FastAPI schema — RUN AFTER EVERY BACKEND CHANGE
npm run api:types
# Validate before every push
npm run lint
npm run build
# Local dev (rare — prod runs inside Docker; no local Python env exists)
npm run dev # requires NEXT_PUBLIC_API_ORIGIN=http://127.0.0.1:8000 or similar
```
**`npm run api:types` is mandatory** any time a FastAPI endpoint is added, removed, or its request/response shape changes. It fetches `https://legal-ai.nautilus.marcusgroup.org/openapi.json` and writes `src/lib/api/types.ts`.
---
## Backend Proxy — `/api/*`
`next.config.ts` transparently rewrites all `/api/*` requests to the FastAPI backend:
- In Docker (production): `http://127.0.0.1:8000`
- Override via env var: `NEXT_PUBLIC_API_ORIGIN`
**Never hardcode the backend origin in component code.** Always use relative paths like `/api/cases`.
The typed fetch wrapper lives in `src/lib/api/client.ts` — use `apiRequest<T>(path, options)`. It throws `ApiError` on non-2xx responses with the parsed body and status code.
---
## API Types — Never Edit by Hand
`src/lib/api/types.ts` is **auto-generated** by `openapi-typescript` from the live FastAPI OpenAPI schema.
- **Do NOT edit `src/lib/api/types.ts` manually** — changes will be overwritten on the next `npm run api:types` run.
- The typed helper modules in `src/lib/api/` (e.g. `cases.ts`, `documents.ts`, `precedents.ts`) ARE hand-written and import from `types.ts`. These are safe to edit.
- When adding a new API domain, create a new typed module in `src/lib/api/<domain>.ts` following the existing pattern.
---
## Tailwind CSS v4 — Breaking Changes from v3
Tailwind v4 has a completely different configuration model.
**What does NOT exist in v4:**
- `tailwind.config.ts` / `tailwind.config.js` — there is no config file
- `@tailwind base;` / `@tailwind components;` / `@tailwind utilities;` directives
- `tailwind.config.theme.extend` object
**What v4 uses instead:**
```css
/* globals.css — already set up, do not change */
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@theme {
/* Design tokens defined here as CSS custom properties */
--color-navy: #0f172a;
/* ... */
}
```
- Custom tokens go inside `@theme {}` in `globals.css`.
- Custom variants use `@custom-variant`.
- Class names are the same (e.g. `bg-navy`, `text-gold`), but the config source is CSS, not JS.
- PostCSS is configured via `@tailwindcss/postcss` (devDependency).
---
## shadcn/ui Components
Adding a new component:
```bash
npx shadcn add <component-name>
# e.g. npx shadcn add table
```
Installed components live in `src/components/ui/`. They are editable (shadcn copies the source, not a package import). The `radix-ui` package (v1.4) is the underlying primitive.
- Do NOT `npm install @radix-ui/react-*` directly — use `npx shadcn add` which installs the correct Radix version and generates the shadcn wrapper.
- Design tokens in `globals.css` (`--color-navy`, `--color-gold`, etc.) are already mapped to the shadcn semantic tokens (`background`, `foreground`, `primary`, etc.), so shadcn components inherit the editorial/judicial aesthetic automatically.
---
## TanStack Query v5
**v5 has breaking API changes from v4.** Key patterns used in this codebase:
```typescript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
// Reading data
const { data, isLoading, isError } = useQuery({
queryKey: ["cases"],
queryFn: () => apiRequest<CaseListResponse>("/api/cases"),
});
// Writing data
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (body: CreateCaseRequest) =>
apiRequest<Case>("/api/cases", { method: "POST", body }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["cases"] });
},
});
```
**v5 changes from v4:**
- `useQuery` no longer accepts positional arguments — always use the options object.
- `isLoading` is replaced by `isPending` for mutations (but `isLoading` still works for queries).
- `onSuccess`/`onError`/`onSettled` callbacks on `useQuery` are removed — use mutation callbacks or `useEffect` instead.
- `getQueryData` / `setQueryData` are unchanged.
The shared `QueryClient` is created in `src/lib/api/client.ts` via `makeQueryClient()` and provided by `src/lib/providers.tsx`.
---
## RTL — Hebrew UI Rules
**All UI is Hebrew, right-to-left.** The `<html>` element has `dir="rtl"` and `lang="he"`.
Use **logical CSS properties** instead of directional ones:
| Avoid (directional) | Use (logical) |
|---------------------|--------------|
| `ml-*` / `mr-*` | `ms-*` (start) / `me-*` (end) |
| `pl-*` / `pr-*` | `ps-*` (start) / `pe-*` (end) |
| `text-left` | `text-start` |
| `text-right` | `text-end` |
| `float-left` | `float-start` |
| `border-l-*` | `border-s-*` |
In RTL, "start" = right side, "end" = left side. Using logical properties means the layout works automatically without RTL overrides.
Flexbox direction: `flex-row` in RTL naturally flows right-to-left. Use `flex-row-reverse` only when you need LTR inside an RTL context.
---
## Project Structure
```
src/
├── app/ # Next.js App Router pages
│ ├── layout.tsx # Root layout — sets dir="rtl", applies fonts
│ ├── globals.css # Tailwind v4 imports + design tokens + :root vars
│ ├── cases/ # Case management pages
│ ├── precedents/ # Precedent library pages
│ ├── methodology/ # Methodology browser
│ ├── training/ # Training document management
│ ├── settings/ # Application settings
│ └── skills/ # Skills management
├── components/
│ ├── ui/ # shadcn primitives (editable copies)
│ ├── app-shell.tsx # Top-level shell with nav
│ ├── cases/ # Case-domain components
│ ├── documents/ # Document viewer components
│ ├── precedents/ # Precedent components
│ └── compose/ # Decision drafting / block editor
├── lib/
│ ├── api/
│ │ ├── types.ts # AUTO-GENERATED — never edit
│ │ ├── client.ts # apiRequest<T> + QueryClient factory
│ │ ├── cases.ts # Typed case API helpers
│ │ ├── documents.ts # Typed document API helpers
│ │ └── ... # One file per API domain
│ ├── providers.tsx # TanStack Query + theme providers
│ ├── utils.ts # cn() and other shared utilities
│ ├── doc-types.ts # Document type constants
│ └── sse.ts # Server-Sent Events helper for streaming
```
---
## Forms
Forms use **react-hook-form** (v7) with **zod** (v4) validation via `@hookform/resolvers`:
```typescript
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const schema = z.object({ title: z.string().min(1) });
type FormValues = z.infer<typeof schema>;
const form = useForm<FormValues>({ resolver: zodResolver(schema) });
```
---
## Notifications / Toasts
Use **sonner** (`import { toast } from "sonner"`). The `<Toaster>` is mounted in `src/lib/providers.tsx`.
```typescript
toast.success("התיק נשמר בהצלחה");
toast.error("שגיאה בשמירה");
```
---
## Streaming (SSE)
Server-sent events are used for long-running AI operations (drafting, analysis). The helper is in `src/lib/sse.ts`. Use it instead of raw `EventSource`.
---
## Deploy
This frontend runs **inside Docker via Coolify** — not as a standalone Node process.
- **No `npm run dev` on the server** — there is no local Python environment for the backend.
- To see changes in production: `git commit` + `git push origin main` → Gitea Actions builds image → Coolify redeploys (~2-4 min).
- Prod URL: `https://legal-ai.nautilus.marcusgroup.org`
- The Next.js output is `standalone` (see `next.config.ts: output: "standalone"`).

View File

@@ -1,7 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "sonner";
import {
@@ -15,14 +15,15 @@ import { Label } from "@/components/ui/label";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { PartiesField } from "@/components/wizard/parties-field";
import { useUpdateCase } from "@/lib/api/cases";
import { caseUpdateSchema, expectedOutcomes, type CaseUpdateInput } from "@/lib/schemas/case";
import type { CaseDetail } from "@/lib/api/cases";
/*
* Inline edit dialog for core case fields. Uses react-hook-form + zod
* directly (shadcn's <Form> registry entry wasn't available at init
* time, so the styling is reproduced by hand in a lean form layout).
* Inline edit dialog for all case fields set at creation time.
* Uses react-hook-form + zod directly (shadcn's <Form> registry entry
* wasn't available at init time, so the styling is reproduced by hand).
*/
function FieldError({ message }: { message?: string }) {
@@ -42,6 +43,10 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) {
hearing_date: data.hearing_date ?? "",
notes: "",
expected_outcome: data.expected_outcome ?? "",
appellants: data.appellants ?? [],
respondents: data.respondents ?? [],
property_address: data.property_address ?? "",
permit_number: data.permit_number ?? "",
},
});
@@ -54,6 +59,10 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) {
hearing_date: data.hearing_date ?? "",
notes: "",
expected_outcome: data.expected_outcome ?? "",
appellants: data.appellants ?? [],
respondents: data.respondents ?? [],
property_address: data.property_address ?? "",
permit_number: data.permit_number ?? "",
});
}, [open, data, form]);
@@ -74,11 +83,11 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) {
עריכת פרטי תיק
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg" dir="rtl">
<DialogContent className="sm:max-w-lg max-h-[90vh] overflow-y-auto" dir="rtl">
<DialogHeader>
<DialogTitle>עריכת פרטי תיק {data.case_number}</DialogTitle>
<DialogDescription className="text-ink-muted">
השינויים נשמרים ישירות ל-FastAPI. השדות הריקים נשארים ללא שינוי.
השינויים נשמרים ישירות ל-DB. שינוי כותרת יסנכרן גם ל-Paperclip.
</DialogDescription>
</DialogHeader>
@@ -95,6 +104,55 @@ export function CaseEditDialog({ data }: { data: CaseDetail }) {
<FieldError message={form.formState.errors.subject?.message} />
</div>
<div className="h-px bg-rule" />
<Controller
control={form.control}
name="appellants"
render={({ field, fieldState }) => (
<PartiesField
label="עוררים"
value={field.value ?? []}
onChange={field.onChange}
error={fieldState.error?.message}
/>
)}
/>
<Controller
control={form.control}
name="respondents"
render={({ field, fieldState }) => (
<PartiesField
label="משיבים"
value={field.value ?? []}
onChange={field.onChange}
error={fieldState.error?.message}
/>
)}
/>
<div className="h-px bg-rule" />
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="property_address" className="text-navy">כתובת הנכס</Label>
<Input
id="property_address"
{...form.register("property_address")}
className="mt-1"
/>
</div>
<div>
<Label htmlFor="permit_number" className="text-navy">מס׳ תכנית/בקשה</Label>
<Input
id="permit_number"
{...form.register("permit_number")}
className="mt-1"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="hearing_date" className="text-navy">תאריך דיון</Label>

View File

@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { Trash2, Plus, Pencil, Wand2 } from "lucide-react";
import { toast } from "sonner";
import {
@@ -151,7 +152,9 @@ function CourtRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => void })
className="font-semibold text-navy text-right whitespace-normal break-words min-w-[280px] max-w-[420px] py-3"
dir="rtl"
>
<span dir="auto">{cleanCitation(p.case_number)}</span>
<Link href={`/precedents/${p.id}`} className="hover:underline hover:text-gold-deep" dir="auto">
{cleanCitation(p.case_number)}
</Link>
</TableCell>
<TableCell className="text-ink whitespace-normal break-words max-w-[260px] py-3">
<div className="font-medium">{cleanCitation(p.case_name)}</div>
@@ -233,7 +236,9 @@ function CommitteeRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => voi
className="font-semibold text-navy text-right whitespace-normal break-words min-w-[200px] max-w-[320px] py-3"
dir="rtl"
>
<span dir="auto">{cleanCitation(p.case_number)}</span>
<Link href={`/precedents/${p.id}`} className="hover:underline hover:text-gold-deep" dir="auto">
{cleanCitation(p.case_number)}
</Link>
</TableCell>
<TableCell className="text-ink whitespace-normal break-words max-w-[220px] py-3">
<div className="font-medium">{cleanCitation(p.case_name)}</div>
@@ -308,8 +313,8 @@ export function LibraryListPanel() {
limit: 200,
};
const courts = usePrecedents({ ...sharedFilters, sourceKind: "external_upload" });
const committee = usePrecedents({ ...sharedFilters, sourceKind: "internal_committee" });
const courts = usePrecedents({ ...sharedFilters, sourceKind: "external_upload", sourceType: "court_ruling" });
const committee = usePrecedents({ ...sharedFilters, sourceKind: "all_committees" });
return (
<div className="space-y-8">

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { useState } from "react";
import { Save, Sparkles } from "lucide-react";
import { toast } from "sonner";
import {
@@ -65,10 +65,12 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
const [form, setForm] = useState<FormState>(EMPTY);
// Hydrate form when the record loads.
useEffect(() => {
if (!record) return;
// eslint-disable-next-line react-hooks/set-state-in-effect
// React-approved derived-state pattern: sync form whenever a different
// record arrives (including after save+refetch). Using setState during
// render avoids the one-frame flash that useEffect would produce.
const [syncedRecordId, setSyncedRecordId] = useState<string | null>(null);
if (record && record.id !== syncedRecordId) {
setSyncedRecordId(record.id as string);
setForm({
citation: record.case_number || "",
case_name: record.case_name || "",
@@ -84,7 +86,7 @@ export function PrecedentEditSheet({ caseLawId, onOpenChange }: Props) {
headnote: record.headnote || "",
key_quote: (record as { key_quote?: string }).key_quote || "",
});
}, [record]);
}
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();

View File

@@ -321,6 +321,7 @@ export function CaseWizard() {
</Button>
{isLast ? (
<Button
key="submit-btn"
type="submit"
disabled={mutate.isPending}
className="bg-navy hover:bg-navy-soft text-parchment"
@@ -329,6 +330,7 @@ export function CaseWizard() {
</Button>
) : (
<Button
key="next-btn"
type="button"
onClick={goNext}
className="bg-navy hover:bg-navy-soft text-parchment"

View File

@@ -50,6 +50,10 @@ export type Case = {
processing_count?: number;
committee_type?: string | null;
hearing_date?: string | null;
appellants?: string[] | null;
respondents?: string[] | null;
property_address?: string | null;
permit_number?: string | null;
};
export type CaseDocument = {

View File

@@ -432,6 +432,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/cases/stale": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Api Stale Cases
* @description Return cases that haven't been updated in N days and are not in a terminal/waiting status.
*/
get: operations["api_stale_cases_api_cases_stale_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/cases/{case_number}/archive": {
parameters: {
query?: never;
@@ -1927,6 +1947,26 @@ export interface paths {
patch: operations["api_resolve_feedback_api_feedback__feedback_id__resolve_patch"];
trace?: never;
};
"/api/chair-feedback/weekly-summary": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Api Chair Feedback Weekly Summary
* @description Return chair feedback from the last N days as a text summary for the CEO agent.
*/
get: operations["api_chair_feedback_weekly_summary_api_chair_feedback_weekly_summary_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/precedent-library/upload": {
parameters: {
query?: never;
@@ -2019,6 +2059,40 @@ export interface paths {
patch: operations["precedent_library_update_api_precedent_library__case_law_id__patch"];
trace?: never;
};
"/api/precedent-library/{case_law_id}/relations": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Precedent Add Relation */
post: operations["precedent_add_relation_api_precedent_library__case_law_id__relations_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/precedent-library/{case_law_id}/relations/{related_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** Precedent Remove Relation */
delete: operations["precedent_remove_relation_api_precedent_library__case_law_id__relations__related_id__delete"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/precedent-library/{case_law_id}/request-metadata": {
parameters: {
query?: never;
@@ -2030,8 +2104,8 @@ export interface paths {
put?: never;
/**
* Precedent Request Metadata
* @description Stamp the case_law row as needing metadata extraction. The local
* MCP worker (`precedent_process_pending_metadata`) will pick it up.
* @description Stamp the case_law row as needing metadata extraction AND wake the
* Paperclip CEO so extraction runs automatically — same flow as upload.
*/
post: operations["precedent_request_metadata_api_precedent_library__case_law_id__request_metadata_post"];
delete?: never;
@@ -2081,6 +2155,69 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/internal-decisions/upload": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Internal Decisions Upload
* @description Upload a planning appeals-committee decision to the internal corpus.
*/
post: operations["internal_decisions_upload_api_internal_decisions_upload_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/internal-decisions/migrate": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Internal Decisions Migrate
* @description Migrate existing data to the internal committee corpus.
*
* source: 'style_corpus' | 'external_corpus' | 'both'
* dry_run: if true, only report what would be done (no writes)
*/
post: operations["internal_decisions_migrate_api_internal_decisions_migrate_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/internal-decisions": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Internal Decisions List
* @description List internal committee decisions with optional filters.
*/
get: operations["internal_decisions_list_api_internal_decisions_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/halachot": {
parameters: {
query?: never;
@@ -2194,6 +2331,63 @@ export interface components {
*/
title: string;
};
/** Body_internal_decisions_upload_api_internal_decisions_upload_post */
Body_internal_decisions_upload_api_internal_decisions_upload_post: {
/** File */
file: string;
/** Case Number */
case_number: string;
/**
* Case Name
* @default
*/
case_name: string;
/**
* Court
* @default
*/
court: string;
/**
* Decision Date
* @default
*/
decision_date: string;
/**
* Chair Name
* @default
*/
chair_name: string;
/**
* District
* @default
*/
district: string;
/**
* Practice Area
* @default
*/
practice_area: string;
/**
* Appeal Subtype
* @default
*/
appeal_subtype: string;
/**
* Subject Tags
* @default []
*/
subject_tags: string;
/**
* Is Binding
* @default true
*/
is_binding: boolean;
/**
* Summary
* @default
*/
summary: string;
};
/** Body_precedent_library_upload_api_precedent_library_upload_post */
Body_precedent_library_upload_api_precedent_library_upload_post: {
/** File */
@@ -2536,6 +2730,16 @@ export interface components {
*/
pdf_document_id: string;
};
/** PrecedentRelationRequest */
PrecedentRelationRequest: {
/** Related Id */
related_id: string;
/**
* Relation Type
* @default same_case_chain
*/
relation_type: string;
};
/** PrecedentUpdateRequest */
PrecedentUpdateRequest: {
/** Case Name */
@@ -3183,6 +3387,37 @@ export interface operations {
};
};
};
api_stale_cases_api_cases_stale_get: {
parameters: {
query?: {
days?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
api_archive_case_api_cases__case_number__archive_post: {
parameters: {
query?: never;
@@ -5510,6 +5745,38 @@ export interface operations {
};
};
};
api_chair_feedback_weekly_summary_api_chair_feedback_weekly_summary_get: {
parameters: {
query?: {
days?: number;
limit?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
precedent_library_upload_api_precedent_library_upload_post: {
parameters: {
query?: never;
@@ -5551,6 +5818,7 @@ export interface operations {
precedent_level?: string;
source_type?: string;
search?: string;
source_kind?: string;
limit?: number;
offset?: number;
};
@@ -5735,6 +6003,73 @@ export interface operations {
};
};
};
precedent_add_relation_api_precedent_library__case_law_id__relations_post: {
parameters: {
query?: never;
header?: never;
path: {
case_law_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["PrecedentRelationRequest"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
precedent_remove_relation_api_precedent_library__case_law_id__relations__related_id__delete: {
parameters: {
query?: never;
header?: never;
path: {
case_law_id: string;
related_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
precedent_request_metadata_api_precedent_library__case_law_id__request_metadata_post: {
parameters: {
query?: never;
@@ -5829,6 +6164,105 @@ export interface operations {
};
};
};
internal_decisions_upload_api_internal_decisions_upload_post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"multipart/form-data": components["schemas"]["Body_internal_decisions_upload_api_internal_decisions_upload_post"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
internal_decisions_migrate_api_internal_decisions_migrate_post: {
parameters: {
query?: {
source?: string;
dry_run?: boolean;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
internal_decisions_list_api_internal_decisions_get: {
parameters: {
query?: {
district?: string;
chair_name?: string;
practice_area?: string;
limit?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
halachot_list_api_halachot_get: {
parameters: {
query?: {

View File

@@ -89,6 +89,14 @@ export const caseUpdateSchema = z.object({
.enum(expectedOutcomes.map((o) => o.value) as [string, ...string[]])
.optional(),
status: z.string().optional(),
appellants: z
.array(z.string().trim().min(1).refine((v) => hebrewPartyRe.test(v), "שם לא תקין"))
.optional(),
respondents: z
.array(z.string().trim().min(1).refine((v) => hebrewPartyRe.test(v), "שם לא תקין"))
.optional(),
property_address: z.string().trim().max(200).optional(),
permit_number: z.string().trim().max(100).optional(),
});
export type CaseUpdateInput = z.infer<typeof caseUpdateSchema>;

View File

@@ -20,7 +20,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "mcp-server" / "
import zipfile
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi import BackgroundTasks, FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import FileResponse, StreamingResponse
from typing import Any, Literal
from pydantic import BaseModel
@@ -44,7 +44,7 @@ from web.mcp_env_catalog import (
normalize_for_compare,
)
from web.progress_store import ProgressStore
from web.paperclip_api import pc_request
from web.paperclip_api import emit_case_status_webhook, pc_request
from web.paperclip_client import (
COMPANIES as PAPERCLIP_COMPANIES,
accept_interaction as pc_accept_interaction,
@@ -1135,6 +1135,36 @@ async def list_cases(
return result
@app.get("/api/cases/stale")
async def api_stale_cases(days: int = 3):
"""Return cases that haven't been updated in N days and are not in a terminal/waiting status."""
if days <= 0:
return {"cases": [], "total": 0}
pool = await db.get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT case_number, title, status,
EXTRACT(DAY FROM (now() - updated_at))::int AS days_stale
FROM cases
WHERE status NOT IN ('final', 'new', 'exported')
AND updated_at < now() - make_interval(days => $1)
ORDER BY updated_at ASC -- oldest stale first (longest overdue = highest priority)
""",
days,
)
cases = [
{
"case_number": r["case_number"],
"title": r["title"],
"status": r["status"],
"days_stale": r["days_stale"],
}
for r in rows
]
return {"cases": cases, "total": len(cases)}
@app.post("/api/cases/{case_number}/archive")
async def api_archive_case(case_number: str):
"""Move a case to the archive. Also archives the matching Paperclip project."""
@@ -1210,6 +1240,10 @@ class CaseUpdateRequest(BaseModel):
decision_date: str = ""
tags: list[str] | None = None
expected_outcome: str = ""
appellants: list[str] | None = None
respondents: list[str] | None = None
property_address: str = ""
permit_number: str = ""
@app.post("/api/cases/create")
@@ -1337,8 +1371,12 @@ async def api_case_get(case_number: str):
@app.put("/api/cases/{case_number}")
async def api_case_update(case_number: str, req: CaseUpdateRequest):
async def api_case_update(case_number: str, req: CaseUpdateRequest, background_tasks: BackgroundTasks):
"""Update case details."""
# Capture old status before the update so we can detect changes.
existing = await db.get_case_by_number(case_number)
old_status = (existing or {}).get("status", "")
result = await cases_tools.case_update(
case_number=case_number,
status=req.status,
@@ -1349,12 +1387,45 @@ async def api_case_update(case_number: str, req: CaseUpdateRequest):
decision_date=req.decision_date,
tags=req.tags,
expected_outcome=req.expected_outcome,
appellants=req.appellants,
respondents=req.respondents,
property_address=req.property_address,
permit_number=req.permit_number,
)
try:
return json.loads(result)
parsed = json.loads(result)
except json.JSONDecodeError:
raise HTTPException(404, result)
# Paperclip sync: update project name when title changes (fire-and-forget).
old_title = (existing or {}).get("title", "")
if req.title and req.title != old_title:
background_tasks.add_task(
paperclip_client.update_project_name,
case_number=case_number,
new_title=req.title,
)
# Emit webhook when status changes (fire-and-forget via BackgroundTasks).
new_status = req.status
if new_status and old_status != new_status:
prefix = case_number[:1]
company_id = (
PAPERCLIP_COMPANIES["licensing"] if prefix == "1"
else PAPERCLIP_COMPANIES["betterment"] if prefix in ("8", "9")
else None
)
background_tasks.add_task(
emit_case_status_webhook,
case_number=case_number,
old_status=old_status,
new_status=new_status,
company_id=company_id, # None is safe — plugin handles unknown company gracefully
)
logger.debug("webhook scheduled: case %s %s%s", case_number, old_status, new_status)
return parsed
@app.delete("/api/cases")
async def api_case_delete(case_number: str, remove_files: bool = False):
@@ -2517,14 +2588,17 @@ async def api_start_workflow(case_number: str):
except Exception as e:
raise HTTPException(502, f"שגיאת Paperclip: {e}")
# 3. Wake the CEO agent
# 3. Wake the CEO agent — must succeed before marking case as processing
try:
wakeup = await pc_wake_ceo(issue["issue_id"], case_number, issue.get("company_id", ""))
except Exception as e:
logger.warning("CEO wakeup failed for case %s: %s", case_number, e)
wakeup = {"error": str(e)}
logger.error("CEO wakeup failed for case %s: %s", case_number, e)
raise HTTPException(
502,
f"נוצר issue {issue['identifier']} אך עירור ה-CEO נכשל: {e}. ניתן לנסות שנית.",
)
# 4. Update case status to processing
# 4. Update case status to processing (only after wakeup confirmed)
await cases_tools.case_update(case_number, status="processing")
return {
@@ -3057,8 +3131,16 @@ async def api_get_methodology(category: str):
items = {}
for key, default_val in defaults.items():
if key in overrides:
raw = overrides[key]["rule_value"]
# asyncpg returns JSONB as a raw JSON string when no codec is registered.
# Parse it back to a Python object so the frontend receives the correct type.
if isinstance(raw, str):
try:
raw = json.loads(raw)
except (json.JSONDecodeError, TypeError):
pass
items[key] = {
"value": overrides[key]["rule_value"],
"value": raw,
"is_override": True,
"updated_at": overrides[key]["created_at"].isoformat() if overrides[key]["created_at"] else None,
}
@@ -3095,10 +3177,14 @@ async def api_update_methodology(category: str, key: str, req: MethodologyUpdate
raise HTTPException(422, "content_checklists value must be a non-empty string")
pool = await db.get_pool()
# json.dumps → text, then PostgreSQL casts text→jsonb.
# Passing a Python list directly causes "expected str, got list" in asyncpg;
# passing a str with ::jsonb causes double-encoding (stored as JSONB string).
# ::text::jsonb bypasses asyncpg's codec and lets PostgreSQL parse the JSON.
await pool.execute(
"INSERT INTO appeal_type_rules (id, appeal_type, rule_category, rule_key, rule_value) "
"VALUES (gen_random_uuid(), '_global', $1, $2, $3::jsonb) "
"ON CONFLICT (appeal_type, rule_category, rule_key) DO UPDATE SET rule_value = $3::jsonb",
"VALUES (gen_random_uuid(), '_global', $1, $2, $3::text::jsonb) "
"ON CONFLICT (appeal_type, rule_category, rule_key) DO UPDATE SET rule_value = $3::text::jsonb",
category, key, json.dumps(req.value, ensure_ascii=False),
)
@@ -3979,6 +4065,34 @@ async def api_resolve_feedback(feedback_id: str, body: dict):
return {"status": "resolved"}
@app.get("/api/chair-feedback/weekly-summary")
async def api_chair_feedback_weekly_summary(days: int = 7, limit: int = 100):
"""Return chair feedback from the last N days as a text summary for the CEO agent."""
if days <= 0:
return {"summary": "", "entry_count": 0}
pool = await db.get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(
"""
SELECT cf.feedback_text, c.case_number, c.title
FROM chair_feedback cf
LEFT JOIN cases c ON c.id = cf.case_id
WHERE cf.created_at >= now() - make_interval(days => $1)
ORDER BY cf.created_at DESC
LIMIT $2
""",
days,
limit,
)
if not rows:
return {"summary": "", "entry_count": 0}
lines = [
f"- תיק {r['case_number'] or ''} ({r['title'] or ''}): {r['feedback_text']}"
for r in rows
]
return {"summary": "\n".join(lines), "entry_count": len(rows)}
# ── Background Processing ─────────────────────────────────────────

View File

@@ -19,6 +19,7 @@ from __future__ import annotations
import logging
import os
from datetime import datetime, timezone
from typing import Any
import httpx
@@ -81,3 +82,35 @@ async def pc_request(
if raise_on_error:
resp.raise_for_status()
return resp
async def emit_case_status_webhook(
case_number: str,
old_status: str,
new_status: str,
company_id: str | None = None,
run_id: str | None = None,
) -> None:
"""Notify the Paperclip plugin that a case status changed.
Fire-and-forget: logs errors but never raises, so callers aren't blocked.
"""
try:
await pc_request(
"POST",
"/api/plugins/marcusgroup.legal-ai/webhooks/case-status",
json={
"caseNumber": case_number,
"oldStatus": old_status,
"newStatus": new_status,
"companyId": company_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
},
run_id=run_id,
timeout=5.0,
)
except Exception as exc:
logger.warning(
"emit_case_status_webhook failed for case %s (%s%s): %s",
case_number, old_status, new_status, exc,
)

View File

@@ -231,6 +231,21 @@ async def restore_project(case_number: str) -> dict:
await conn.close()
async def update_project_name(case_number: str, new_title: str) -> None:
"""Update the Paperclip project name when a case title changes."""
project_name = f"ערר {case_number}{new_title}"[:200]
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
try:
await conn.execute(
"UPDATE projects SET name = $1, updated_at = now() WHERE name LIKE $2",
project_name, f"%{case_number}%",
)
except Exception:
logger.warning("Failed to update Paperclip project name for case %s", case_number)
finally:
await conn.close()
async def _ensure_default_workspace(
conn: asyncpg.Connection,
project_id: str,