From 3462cb7320a87c75b90ae2e34d2dc70020d46c39 Mon Sep 17 00:00:00 2001 From: Chaim Date: Mon, 6 Jul 2026 11:16:46 +0000 Subject: [PATCH] =?UTF-8?q?fix(generate):=20=D7=94=D7=A4=D7=A7=D7=AA=20?= =?UTF-8?q?=D7=9E=D7=A1=D7=9E=D7=9B=D7=99=D7=9D=20=D7=93=D7=A8=D7=9A=20iss?= =?UTF-8?q?ue-=D7=99=D7=9C=D7=93=20=D7=9E=D7=A9=D7=95=D7=99=D7=9A-=D7=9C-C?= =?UTF-8?q?EO=20=E2=80=94=20=D7=9C=D7=90=20=D7=9E=D7=91=D7=95=D7=98=D7=9C?= =?UTF-8?q?=D7=AA=20=D7=91=D7=94=D7=9E=D7=AA=D7=A0=D7=94-=D7=9C=D7=99?= =?UTF-8?q?=D7=95"=D7=A8=20(#227)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit באג: כפתורי "הפקת מסמכים" (טיוטת-ביניים / סיכום-מנהלים) העירו את ה-CEO *על ה-issue הראשי*. כשהתיק ב-in_review משויך-לחיים (המצב הרגיל של תיק ממתין-ליו"ר), Paperclip ביטל את הריצה מיד (issue_assignee_changed) — הכלום הופק, וה-endpoint החזיר 202 בכל מקרה, כך שהכשל היה שקוף. תיקון: wake_ceo_for_action יוצר issue-ילד משויך-ל-CEO תחת ה-issue הראשי ומעיר על הילד — בדיוק כדפוס wake_analyst_for_argument_aggregation שעובד. ה-CEO בעל הילד → הריצה לא מבוטלת. ההוראה מוכלת בתיאור-הילד (הרץ את הכלי, comment, סגור done) ולא תלויה בפענוח payload.action. legal-ceo.md מעודכן שההערה מגיעה על issue-ילד ייעודי שיש לסגור בסיום. מגע-Paperclip רק בשער-הפלטפורמה (G12). זהו החצי הראשון (ב) של #227; חצי שני (ג) = סימון-סטטוס ב-UI. py_compile ירוק. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/agents/legal-ceo.md | 2 +- web/paperclip_client.py | 97 +++++++++++++++++++++++++++++-------- 2 files changed, 79 insertions(+), 20 deletions(-) diff --git a/.claude/agents/legal-ceo.md b/.claude/agents/legal-ceo.md index 33dcd4c..7e0e875 100644 --- a/.claude/agents/legal-ceo.md +++ b/.claude/agents/legal-ceo.md @@ -226,7 +226,7 @@ Paperclip חוסם אוטומטית כל issue ב-`in_progress` שאין לו ru ### שלב 0: בדוק למה התעוררת **לפני כל דבר אחר** — בדוק את סיבת ההתעוררות (`$PAPERCLIP_WAKE_REASON`): -- **פעולות סטרוקטורליות (כפתורי-UI, בלי פענוח-טקסט):** אם `$PAPERCLIP_WAKE_PAYLOAD_JSON` מכיל שדה `action` → נתב דטרמיניסטית לפי הערך: `action == "interim_draft"` → **שלב H** (טיוטת ביניים); `action == "party_claims_summary"` → **שלב H2** (סיכום מנהלים). אלו side-quests — אל תסרוק תיקים אחרים, טפל רק בתיק שב-payload (`case_number`/`issueId`). +- **פעולות סטרוקטורליות (כפתורי-UI, בלי פענוח-טקסט):** אם `$PAPERCLIP_WAKE_PAYLOAD_JSON` מכיל שדה `action` → נתב דטרמיניסטית לפי הערך: `action == "interim_draft"` → **שלב H** (טיוטת ביניים); `action == "party_claims_summary"` → **שלב H2** (סיכום מנהלים). אלו side-quests — אל תסרוק תיקים אחרים, טפל רק בתיק שב-payload (`case_number`/`issueId`). ה-`issueId` שב-payload הוא **issue-ילד ייעודי שמשויך אליך** (נוצר ע"י המערכת כדי שהריצה לא תבוטל כשה-issue הראשי ממתין-ליו"ר; #227) — הרץ את הפעולה, פרסם comment עם התוצאה, ו**סגור את ה-issue-ילד הזה כ-done** בסיום. אל תיגע ב-issue הראשי של התיק. - אם ה-reason מכיל `user_commented` **ו**-`$PAPERCLIP_WAKE_PAYLOAD_JSON` כולל `issueId` → **דלג ישירות לסעיף "טיפול בתגובות חדשות מחיים"** עם ה-issue הזה. אל תסרוק תיקים אחרים. **טפל רק בתגובה.** - אם ה-reason מכיל `agent_completion` → דלג לשלב E/F בהתאם לסוכן שסיים - אם ה-reason מכיל `precedent_extraction_` → **דלג לסעיף "חילוץ פסיקה אוטומטי"**. אל תיגע בתיקים — זו עבודת ספרייה. diff --git a/web/paperclip_client.py b/web/paperclip_client.py index a471751..55cef15 100644 --- a/web/paperclip_client.py +++ b/web/paperclip_client.py @@ -1258,25 +1258,58 @@ async def wake_ceo_agent(issue_id: str, case_number: str, company_id: str = "") return result +def _ceo_action_brief(action: str, case_number: str) -> tuple[str, str]: + """(child-issue title, description) for a deterministic CEO generation action. + + The instruction is self-contained in the description (like + ``wake_analyst_for_argument_aggregation``) so the run does not depend on the + CEO parsing ``payload.action`` — it just runs the named tool and closes the + child. Both actions run host-side (claude_session → local claude CLI). + """ + if action == "party_claims_summary": + title = f"[ערר {case_number}] הפקת סיכום-מנהלים לטענות הצדדים" + description = ( + f"חיים ביקש הפקת סיכום-מנהלים לטענות הצדדים בתיק {case_number}.\n\n" + f"הרץ `mcp__legal-ai__summarize_party_claims(case_number=\"{case_number}\")`, " + f"כתוב comment קצר בעברית עם תמצית התוצאה, וסגור issue זה כ-done. " + f"אם חסרים נתונים (אין טענות מחולצות) — דווח ב-comment מה חסר וסגור כ-blocked." + ) + return title, description + # default: interim_draft + title = f"[ערר {case_number}] הפקת טיוטת טענות-הצדדים (טיוטת-ביניים)" + description = ( + f"חיים ביקש הפקת טיוטת טענות-הצדדים (טיוטת-ביניים) בתיק {case_number}.\n\n" + f"הרץ `mcp__legal-ai__write_interim_draft(case_number=\"{case_number}\")` ואז " + f"`mcp__legal-ai__export_interim_draft(case_number=\"{case_number}\")`, כתוב comment " + f"קצר בעברית עם שם הקובץ שנוצר, וסגור issue זה כ-done. אם חסרים נתונים — דווח " + f"ב-comment וסגור כ-blocked." + ) + return title, description + + async def wake_ceo_for_action( case_number: str, action: str, company_id: str = "", ) -> dict: - """Wake the CEO with a deterministic *structured action* on the case's MAIN issue. + """Wake the CEO to run a deterministic generation action (TaskMaster #214). - Drives the UI "הפקת מסמכים" buttons (TaskMaster #214): the CEO reads - ``payload.action`` and routes deterministically to its שלב H / שלב H2 - side-quests (legal-ceo.md §"פעולות סטרוקטורליות") — no free-text parsing. + Drives the UI "הפקת מסמכים" buttons: + action="party_claims_summary" → summarize_party_claims (שלב H2) + action="interim_draft" → write_interim_draft + export_interim_draft (שלב H) - action="party_claims_summary" → שלב H2 (summarize_party_claims) - action="interim_draft" → שלב H (write_interim_draft + export_interim_draft) + A **child issue assigned to the CEO** is created under the case's main issue + and the wakeup targets that child — NOT the main issue directly (#227). The + main issue is usually ``in_review`` assigned to the chair (human) while a case + waits; waking the CEO *on* a human-owned issue makes Paperclip cancel the run + immediately (``issue_assignee_changed``) — the exact silent failure this + fixes. The CEO owns the child, so its run is never cancelled. Same delegation + shape as ``wake_analyst_for_argument_aggregation``. - These are side-quests on the EXISTING ``[ערר {case_number}]`` issue — no child - issue is created (the CEO must not change ``cases.status`` or spawn sub-agents). - Wakeup goes through the Paperclip API (``POST /api/agents/{id}/wakeup``), never a - direct DB insert (CLAUDE.md hard rule). Returns - ``{"status": "ok"|"skipped", ...}``. + Wakeup goes through the Paperclip API (``POST /api/agents/{id}/wakeup``), never + a direct DB insert (CLAUDE.md hard rule). Returns + ``{"status": "ok", "issue_id"(=child), "ceo_id", ...}`` or + ``{"status": "skipped", "reason": ...}``. """ if not PAPERCLIP_BOARD_API_KEY: logger.warning("PAPERCLIP_BOARD_API_KEY not set — skipping CEO action wakeup") @@ -1287,17 +1320,42 @@ async def wake_ceo_for_action( logger.warning("No Paperclip issues found for case %s — skipping CEO action", case_number) return {"status": "skipped", "reason": "no_issue"} - # The main case issue — prefer the in-progress one, else the canonical - # "[ערר {case_number}]" issue, else the oldest. (Same selection spirit as - # wake_curator_for_final, but we never spawn a child — it's a side-quest.) main_issue = ( next((i for i in issues if i.get("status") == "in_progress"), None) or next((i for i in issues if f"[ערר {case_number}]" in (i.get("title") or "")), None) or issues[0] ) main_issue_id = main_issue["id"] - ceo_id = CEO_AGENTS.get(company_id, CEO_AGENT_ID) + + # Child issue assigned to the CEO — so the queued run is owned by the CEO and + # not cancelled by the human assignee on the parent (#227). + title, description = _ceo_action_brief(action, case_number) + child_resp = await pc_request( + "POST", + f"/api/issues/{main_issue_id}/children", + json={ + "title": title, + "description": description, + "status": "in_progress", + "priority": "medium", + "assigneeAgentId": ceo_id, + }, + raise_on_error=True, + ) + sub_issue = child_resp.json() + sub_issue_id = sub_issue["id"] + + # Tag plugin_state so the case page surfaces this sub-issue too. + try: + conn = await asyncpg.connect(PAPERCLIP_DB_URL) + try: + await _link_case_to_issue(conn, sub_issue_id, case_number) + finally: + await conn.close() + except Exception as e: + logger.warning("plugin_state link failed for sub_issue=%s: %s", sub_issue_id, e) + wake_resp = await pc_request( "POST", f"/api/agents/{ceo_id}/wakeup", @@ -1306,7 +1364,7 @@ async def wake_ceo_for_action( "triggerDetail": "manual", "reason": f"generate_{action}_{case_number}", "payload": { - "issueId": main_issue_id, + "issueId": sub_issue_id, "action": action, "case_number": case_number, }, @@ -1314,13 +1372,14 @@ async def wake_ceo_for_action( raise_on_error=True, ) logger.info( - "CEO action wakeup for case %s: action=%s issue=%s ceo=%s status=%s", - case_number, action, main_issue_id, ceo_id, wake_resp.status_code, + "CEO action wakeup for case %s: action=%s child_issue=%s ceo=%s status=%s", + case_number, action, sub_issue_id, ceo_id, wake_resp.status_code, ) return { "status": "ok", "action": action, - "issue_id": main_issue_id, + "issue_id": sub_issue_id, + "main_issue_id": main_issue_id, "ceo_id": ceo_id, }