fix(generate): הפקת מסמכים דרך issue-ילד משויך-ל-CEO — לא מבוטלת בהמתנה-ליו"ר (#227)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 33s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

באג: כפתורי "הפקת מסמכים" (טיוטת-ביניים / סיכום-מנהלים) העירו את ה-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) <noreply@anthropic.com>
This commit is contained in:
2026-07-06 11:16:46 +00:00
parent 17c23ab377
commit 3462cb7320
2 changed files with 79 additions and 20 deletions

View File

@@ -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,
}