feat(agents-tab): הוראה למעלה + פעילות מקופלת + בורר-יעד עם "הרצה חדשה"
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

טאב הסוכנים בדף-התיק — מימוש מוקאפ X17 מאושר (18i3), פותר את התלונה החוזרת
שהיו"ר נחת בתחתית העמוד, שדה-ההוראה למטה, והטיוטה החדשה נבלעה בגלילה.

Frontend (web-ui/agent-activity-feed.tsx):
- **שדה-ההוראה עלה לראש הטאב** (Composer), מעל הפעילות — הכי זמין, בלי גלילה.
  בוטל ה-auto-scroll-to-bottom (endRef/useEffect הוסרו).
- **כל הפעילות מקופלת כברירת-מחדל** — קבוצות-המשימה וגם "ממתין לתשובתך"
  (defaultOpen=false); היו"ר פותח כדי להסתכל. סדר החדש-למעלה (תג "החדש למעלה").
- **בורר-יעד** (TargetSelector): מציג לאיזו משימה ההוראה תלך, בוחר משימה פעילה,
  מסמן משימות סגורות כמנוטרלות ("שליחה אליהן לא תעיר סוכן"), אזהרת יעד-סגור
  עם כפתור-שליחה מנוטרל, ואופציית **"פתח הרצה חדשה"**. מחשב את יעד-ברירת-המחדל
  בצד-לקוח במראָה ל-pick_default_comment_target.

Backend:
- `open_ceo_run` (paperclip_client) — פותח הרצת-CEO טרייה דרך פרימיטיב ה-CEO-child
  (#227): יוצר issue-ילד בבעלות CEO ומעיר עליו, ועוקף את issue_assignee_changed
  על issue בבעלות-אדם (הבאג של #228). נחשף דרך ה-Port כ-`pc_open_ceo_run`.
- `/agents/comment` מקבל `new_run:bool`; `PaperclipIssue` מחזיר `parent_id`
  (לבורר בצד-לקוח).

Invariants: G1 (נרמול-במקור), G2 (אין מסלול-ניתוב מקביל), G12 (מגע-Paperclip דרך ה-Port).
מקדם TaskMaster #228 (מנתב הרצה-חדשה דרך פרימיטיב ה-CEO-child).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 15:48:26 +00:00
parent 0a5be942ba
commit 9e7d98a47a
5 changed files with 457 additions and 80 deletions

View File

@@ -72,6 +72,7 @@ from web.paperclip_client import (
wake_analyst_for_protocol_analysis as _wake_analyst_for_protocol_analysis,
wake_ceo_agent as _wake_ceo,
wake_ceo_for_action as _wake_ceo_for_action,
open_ceo_run as _open_ceo_run,
wake_ceo_for_feedback_fold as _wake_ceo_for_feedback_fold,
wake_curator_for_final as _wake_curator_for_final,
wake_for_precedent_extraction as _wake_for_precedent_extraction,
@@ -89,6 +90,9 @@ pc_wake_ceo = instrument("agent.wakeup", role="ceo")(_wake_ceo)
pc_wake_ceo_for_action = instrument(
"agent.wakeup", role="ceo", keys=("case_number", "company_id", "action"),
)(_wake_ceo_for_action)
pc_open_ceo_run = instrument(
"agent.wakeup", role="ceo", keys=("case_number", "company_id"),
)(_open_ceo_run)
pc_wake_ceo_for_feedback_fold = instrument(
"agent.wakeup", role="ceo", keys=("feedback_id", "category", "block_id"),
)(_wake_ceo_for_feedback_fold)
@@ -165,6 +169,7 @@ __all__ = [
"pc_get_agents",
"pc_wake_ceo",
"pc_wake_ceo_for_action",
"pc_open_ceo_run",
"pc_wake_ceo_for_feedback_fold",
"pc_wake_curator_for_final",
"pc_wake_for_precedent_extraction",

View File

@@ -90,6 +90,7 @@ from web.agent_platform_port import (
rename_case_project,
pc_wake_ceo,
pc_wake_ceo_for_action,
pc_open_ceo_run,
pc_wake_ceo_for_feedback_fold,
pc_wake_curator_for_final,
pc_wake_for_precedent_extraction,
@@ -4604,15 +4605,38 @@ async def api_case_agents(case_number: str):
class AgentCommentRequest(BaseModel):
body: str
issue_id: str | None = None
# "פתח הרצה חדשה" — open a fresh CEO-owned run for this instruction instead of
# appending to an existing issue (agents-tab target selector). Overrides issue_id.
new_run: bool = False
@app.post("/api/cases/{case_number}/agents/comment")
async def api_post_agent_comment(case_number: str, req: AgentCommentRequest):
"""Post a comment on a Paperclip issue linked to a case.
"""Post a chair instruction to a Paperclip issue linked to a case.
If ``issue_id`` is omitted, routing prefers the live CEO main issue and
never a closed (done/cancelled) issue — see ``_pick_default_comment_target``.
Routing (agents-tab target selector):
- ``new_run=True`` → open a fresh CEO-owned run (child issue assigned to the
CEO + wakeup), sidestepping the ``issue_assignee_changed`` cancellation on
a human-owned parent. Overrides ``issue_id``.
- ``issue_id`` given → post to that issue (explicit chair choice).
- neither → prefer the live CEO main issue, never a closed one
(``pick_default_comment_target``).
"""
if req.new_run:
result = await pc_open_ceo_run(case_number, req.body)
if result.get("status") != "ok":
raise HTTPException(
502,
f"פתיחת הרצה חדשה נכשלה: {result.get('reason', 'unknown')}",
)
return {
"comment_id": None,
"issue_id": result["issue_id"],
"issue_identifier": result.get("identifier", ""),
"issue_status": "in_progress",
"new_run": True,
}
issues = await pc_get_case_issues(case_number)
if not issues:
raise HTTPException(404, f"לא נמצא פרויקט Paperclip לתיק {case_number}")

View File

@@ -1739,6 +1739,93 @@ async def wake_ceo_for_action(
}
async def open_ceo_run(
case_number: str,
instruction: str,
company_id: str = "",
) -> dict:
"""Open a FRESH CEO-owned run carrying a free-form chair instruction.
Drives the "פתח הרצה חדשה" option of the agents-tab target selector: instead
of appending the instruction to an existing (possibly human-owned or closed)
issue, a **child issue assigned to the CEO** is created under the case's main
issue and the CEO is woken on it. This is the same CEO-child primitive as
:func:`wake_ceo_for_action` (#227) — it sidesteps the ``issue_assignee_changed``
cancellation that fires when an agent is woken on a human-owned parent (the
exact silent failure the chair hit on 1027-04-26; see TaskMaster #228).
Wakeup goes through the Paperclip API, never a direct DB insert. Returns
``{"status":"ok", "issue_id"(=child), "identifier", ...}`` or
``{"status":"skipped", "reason": ...}``.
"""
if not PAPERCLIP_BOARD_API_KEY:
logger.warning("PAPERCLIP_BOARD_API_KEY not set — skipping CEO new-run")
return {"status": "skipped", "reason": "no_api_key"}
issues = await get_case_issues(case_number)
if not issues:
logger.warning("No Paperclip issues for case %s — skipping CEO new-run", case_number)
return {"status": "skipped", "reason": "no_issue"}
# Parent the new run under the live main issue (never a closed child).
main_issue = pick_default_comment_target(issues)
main_issue_id = main_issue["id"]
if not company_id:
company_id = main_issue.get("company_id", "")
ceo_id = CEO_AGENTS.get(company_id, CEO_AGENT_ID)
title = f'[ערר {case_number}] הוראת יו"ר — הרצה חדשה'
description = (
'הוראת יו"ר שנשלחה כ"הרצה חדשה" מטאב הסוכנים. קרא את מצב התיק, טפל בהוראה '
"בסגנון דפנה, ומסור disposition תקין בסיום.\n\n---\n\n" + instruction
)
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"]
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 new-run sub_issue=%s: %s", sub_issue_id, e)
await pc_request(
"POST",
f"/api/agents/{ceo_id}/wakeup",
json={
"source": "on_demand",
"triggerDetail": "manual",
"reason": f"chair_new_run_{case_number}",
"payload": {"issueId": sub_issue_id, "case_number": case_number},
},
raise_on_error=True,
)
logger.info(
"CEO new-run for case %s: child_issue=%s ceo=%s", case_number, sub_issue_id, ceo_id,
)
return {
"status": "ok",
"issue_id": sub_issue_id,
"identifier": sub_issue.get("identifier", ""),
"main_issue_id": main_issue_id,
"ceo_id": ceo_id,
}
# Substring of the generation child-issue title per action (see _ceo_action_brief).
GENERATION_TITLE_SUBSTR = {
"party_claims_summary": "סיכום-מנהלים",