diff --git a/.claude/agents/legal-writer.md b/.claude/agents/legal-writer.md index e0d3d54..a89b27c 100644 --- a/.claude/agents/legal-writer.md +++ b/.claude/agents/legal-writer.md @@ -13,6 +13,7 @@ tools: - mcp__legal-ai__document_list - mcp__legal-ai__document_get_text - mcp__legal-ai__get_claims + - mcp__legal-ai__get_chair_directions - mcp__legal-ai__get_decision_template - mcp__legal-ai__get_block_context - mcp__legal-ai__save_block_content @@ -71,8 +72,53 @@ tools: ### שלב 1: הכנה 1. קרא פרטי התיק (`case_get`) 2. קרא טענות מחולצות (`get_claims`) -3. קבל תבנית החלטה (`get_decision_template`) -4. קרא מדריך סגנון (`get_style_guide`) +3. **קרא את עמדות יו"ר הוועדה (`get_chair_directions`) — חובה!** +4. קבל תבנית החלטה (`get_decision_template`) +5. קרא מדריך סגנון (`get_style_guide`) + +### שלב 1ב: בדיקת עמדות יו"ר — חובה לפני כתיבה! + +ה-`get_chair_directions` מחזיר status: + +- **`missing`** — הקובץ `analysis-and-research.md` לא קיים. + ⛔ **עצור מייד.** הסוכן `legal-analyst` לא רץ עדיין על התיק. + דווח ל-Paperclip: "לא ניתן לכתוב טיוטה — ניתוח משפטי טרם בוצע. + יש להריץ את legal-analyst קודם." + +- **`empty`** — הקובץ קיים אבל דפנה לא מילאה אף עמדה. + ⛔ **עצור מייד.** דווח ל-Paperclip: "לא ניתן לכתוב טיוטה — + כל X הסוגיות ממתינות לעמדת יו"ר הוועדה. יש להיכנס לדף התיק + ב-UI (https://legal-ai.nautilus.marcusgroup.org/#/case/{case_number}) + ולמלא את השדה 'עמדת ועדת הערר' בכל סוגיה." + +- **`partial`** — חלק מהסוגיות מולאו, אחרות ריקות. + ⚠️ **עצור.** דווח למשתמשת שחסרות Y מתוך X עמדות. **רק** + אם המשתמשת מאשרת מפורשות להמשיך (למשל, כי היא רוצה טיוטה + חלקית), אפשר להמשיך — ולכתוב רק עבור הסוגיות שמולאו, ולציין + ב-comment את הסוגיות שלא טופלו. + +- **`complete`** — כל העמדות מולאו. ✅ **ניתן להמשיך.** + +### שלב 1ג: בניית direction_doc מעמדות היו"ר + +לפני כתיבת בלוק י (דיון), בנה direction_doc פנימי מהעמדות שקיבלת: + +```json +{ + "threshold_claims": [ + {"id": "threshold_1", "title": "...", "chair_ruling": "..."}, + ... + ], + "issues": [ + {"id": "issue_1", "title": "...", "chair_ruling": "..."}, + ... + ] +} +``` + +כל `chair_ruling` הוא הטקסט הגולמי שדפנה כתבה. הוא **מחייב אותך** — +אסור לך לסתור את דעתה של דפנה, רק לנסח אותה בצורה משפטית מקצועית +בסגנון שלה. ### שלב 2: כתיבה בלוק-אחרי-בלוק לכל בלוק (ה עד יא): @@ -104,3 +150,28 @@ case_update(case_number, status="drafted") - השתמש בציטוטים ארוכים (200-600 מילים) מפסיקה - אל תחזור על עובדות מבלוק ו - אל תשתמש בכותרות משנה (למעט נושאים נפרדים לחלוטין) + +### חובה: שימוש בעמדות יו"ר מ-`get_chair_directions` + +עבור **כל טענת סף** ו**כל סוגיה** ב-direction_doc שבנית בשלב 1ג: + +1. **פתח את הדיון במסקנה של דפנה** — למשל "**טענת הסף הראשונה נדחית**" + או "**בסוגיה זו אנו מקבלים את עמדת העוררים**", **על בסיס** מה + שדפנה כתבה ב-`chair_ruling`. +2. **נסח את הנימוק** בסגנון דפנה — השתמש בביטויי מעבר מ-`get_style_guide` + ("נחדד", "ודוק", "יחד עם זאת", "מכאן כי"), פסיקה שמוזכרת + ב-`internal_precedents` של הסוגיה, וחקיקה מ-`relevant_legislation`. +3. **עקוב אחר הטון של דפנה** — אם היא כתבה "יש לדחות זאת מכל וכל" + אל תנסח מתון ("ייתכן שהוועדה תמצא לנכון..."). אם היא כתבה + "נראה לי שיש מקום לקבל בחלקה" אל תנסח חד ("הערר מתקבל במלואו"). +4. **אסור לסתור את דעתה של דפנה.** אם היא כתבה דעה שמנוגדת לעמדתך — + דעתה קובעת. אתה מנסח את הטיעון המשפטי בעד **עמדתה**. +5. **ציון שאלות המחקר** — בכל סוגיה, השתמש ב-`legal_questions` + שחולצו ב-analysis-and-research.md כמבנה לניתוח (שאלה עקרונית + תחילה, ואז יישום קונקרטי). + +## בלוק יא — סיכום + +- חזור על המסקנות של דפנה מה-`chair_ruling` של כל סוגיה בקצרה +- ציין את התוצאה הסופית (ערר מתקבל/נדחה/מתקבל בחלקו) בהתאם לעמדות +- הוסף את פסקת "ניתנה פה אחד" עם תאריך עברי ולועזי diff --git a/mcp-server/src/legal_mcp/server.py b/mcp-server/src/legal_mcp/server.py index 968aa78..54dc127 100644 --- a/mcp-server/src/legal_mcp/server.py +++ b/mcp-server/src/legal_mcp/server.py @@ -217,6 +217,15 @@ async def draft_section( return await drafting.draft_section(case_number, section, instructions) +@mcp.tool() +async def get_chair_directions(case_number: str) -> str: + """שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר כ-direction_doc לכותב. + קורא מ-analysis-and-research.md שמולא ע"י דפנה דרך ה-UI. + מחזיר סטטוס (missing/empty/partial/complete) + עמדות מובנות. + """ + return await drafting.get_chair_directions(case_number) + + @mcp.tool() async def get_decision_template(case_number: str) -> str: """תבנית מבנית להחלטה מלאה עם פרטי התיק.""" diff --git a/mcp-server/src/legal_mcp/services/research_md.py b/mcp-server/src/legal_mcp/services/research_md.py index d95635e..18f8e07 100644 --- a/mcp-server/src/legal_mcp/services/research_md.py +++ b/mcp-server/src/legal_mcp/services/research_md.py @@ -353,3 +353,84 @@ def update_chair_position( "preview": preview, "timestamp": datetime.now().isoformat(), } + + +# ── Chair directions extraction (for downstream agents) ───────── + + +def extract_chair_directions(file_path: Path) -> dict[str, Any]: + """Extract only the chair positions from analysis-and-research.md. + + Returns a compact dict that the legal-writer agent can use as direction: + + { + "case_number": "1033-25", + "file_path": "...", + "file_exists": True, + "total_items": 9, + "filled_count": 3, + "empty_count": 6, + "status": "partial", # "empty" | "partial" | "complete" + "threshold_claims": [ + {"id": "threshold_1", "number": 1, "title": "...", "direction": "..."}, + ... + ], + "issues": [ + {"id": "issue_1", "number": 1, "title": "...", "direction": "..."}, + ... + ] + } + + Used by legal-writer to convert chair positions into direction docs + before generating blocks of the decision. + """ + if not file_path.exists(): + return { + "file_exists": False, + "status": "missing", + "error": "analysis-and-research.md not found", + "threshold_claims": [], + "issues": [], + "total_items": 0, + "filled_count": 0, + "empty_count": 0, + } + + parsed = parse(file_path) + + def reduce_item(item: dict) -> dict: + return { + "id": item["id"], + "number": item["number"], + "title": item["title"], + "direction": item.get("chair_position", "") or "", + } + + threshold = [reduce_item(t) for t in parsed.get("threshold_claims", [])] + issues = [reduce_item(i) for i in parsed.get("issues", [])] + + all_items = threshold + issues + total = len(all_items) + filled = sum(1 for x in all_items if x["direction"].strip()) + empty = total - filled + + if total == 0: + status = "missing" + elif filled == 0: + status = "empty" + elif filled == total: + status = "complete" + else: + status = "partial" + + return { + "file_exists": True, + "file_path": str(file_path), + "case_number": parsed.get("header", {}).get("case_number", ""), + "status": status, + "total_items": total, + "filled_count": filled, + "empty_count": empty, + "threshold_claims": threshold, + "issues": issues, + } diff --git a/mcp-server/src/legal_mcp/tools/drafting.py b/mcp-server/src/legal_mcp/tools/drafting.py index 74707f9..2433550 100644 --- a/mcp-server/src/legal_mcp/tools/drafting.py +++ b/mcp-server/src/legal_mcp/tools/drafting.py @@ -3,9 +3,11 @@ from __future__ import annotations import json +from pathlib import Path from uuid import UUID -from legal_mcp.services import db, embeddings +from legal_mcp import config +from legal_mcp.services import db, embeddings, research_md from legal_mcp.services.lessons import ( CITATION_GUIDANCE, DECISION_TEMPLATES, @@ -279,6 +281,32 @@ async def draft_section( return json.dumps(context, ensure_ascii=False, indent=2) +async def get_chair_directions(case_number: str) -> str: + """שליפת עמדות יו"ר הוועדה (דפנה) על סוגיות הערר, לצורך יצירת direction_doc + לכותב. קורא מ-analysis-and-research.md (שנוצר ע"י legal-analyst ומולא ע"י + דפנה דרך ה-UI). + + מחזיר JSON עם סטטוס, כמה סוגיות מולאו וכמה עדיין ריקות, ורשימה של עמדות + מובנות — ניתן להזריק ישירות כ-direction_doc לבלוק י (דיון) ולבלוק יא (סיכום). + + סטטוסים: + missing — הקובץ לא קיים + empty — הקובץ קיים אבל כל העמדות ריקות (טרם נקבעה דעה) + partial — חלק מהעמדות מולאו + complete — כל העמדות מולאו + + אם המצב הוא `empty` או `missing` — הכותב צריך לעצור ולבקש מדפנה למלא + את הקובץ דרך ה-UI לפני המשך הכתיבה. + + Args: + case_number: מספר תיק הערר + """ + case_dir = config.find_case_dir(case_number) + file_path = case_dir / "documents" / "research" / "analysis-and-research.md" + result = research_md.extract_chair_directions(file_path) + return json.dumps(result, ensure_ascii=False, indent=2) + + async def get_decision_template(case_number: str) -> str: """קבלת תבנית מבנית להחלטה מלאה עם פרטי התיק, מותאמת לסוג התוצאה הצפויה.