Compare commits
5 Commits
4d227a4999
...
worktree-a
| Author | SHA1 | Date | |
|---|---|---|---|
| 253f8ff97b | |||
| 044aecb034 | |||
| 1c7464c2ce | |||
| 8bcf05df26 | |||
| cb4aa703ec |
@@ -234,7 +234,7 @@ search_precedent_library(
|
||||
|
||||
### 5ד. תיעוד מחייב — סעיף "שאילתות לקורפוסים" ב-`analysis-and-research.md`
|
||||
|
||||
ב-artifact הסופי, חובה להופיע סעיף חדש בשם **"7א. שאילתות לקורפוסים — log מלא"**, עם הפורמט הבא:
|
||||
ב-artifact הסופי, חובה להופיע סעיף חדש בשם **"7א. שאילתות לקורפוסים — log מלא"**, עם הפורמט הבא. (זהו סעיף 7א שמופיע גם בתבנית-הפלט המלאה שבהמשך — **אותו סעיף אחד**, לא תבנית נפרדת.)
|
||||
|
||||
```markdown
|
||||
## 7א. שאילתות לקורפוסים — log מלא
|
||||
@@ -323,7 +323,7 @@ FROM documents d WHERE d.case_id = '{case_id}' AND d.doc_type IN ('appeal', 'res
|
||||
|
||||
### העֵר את העוזר המשפטי (CEO) — חובה!
|
||||
|
||||
wakeup ל-CEO עם `payload.issueId=$PAPERCLIP_TASK_ID` ו-`reason="מנתח משפטי סיים $PAPERCLIP_TASK_ID בסטטוס done/blocked"` — הפרוטוקול המלא (CEO לפי חברה, אזהרות) במקור היחיד [HEARTBEAT.md](HEARTBEAT.md) §4ג. **אסור** `INSERT INTO agent_wakeup_requests` ישיר; **אסור** לקבע UUID של CEO (נגזר מ-`$PAPERCLIP_COMPANY_ID`).
|
||||
wakeup ל-CEO עם `payload.issueId=$PAPERCLIP_TASK_ID` ו-`reason="מנתח משפטי סיים $PAPERCLIP_TASK_ID בסטטוס done/blocked"` — הפרוטוקול המלא (CEO לפי חברה, אזהרות) במקור היחיד [HEARTBEAT.md](HEARTBEAT.md) §4ג.
|
||||
**⚠️ `$PAPERCLIP_TASK_ID` — זה UUID, לא CMP-XX.** מוגדר אוטומטית ע"י Paperclip; ב-double-quotes bash מרחיב לערך האמיתי. שגיאת `invalid input syntax for type uuid` = שלחת CMP-XX במקום UUID.
|
||||
|
||||
## מבנה הפלט המלא — analysis-and-research.md
|
||||
|
||||
@@ -850,7 +850,8 @@ ls data/cases/$CASE_NUMBER/documents/research/analysis-and-research.md
|
||||
...
|
||||
|
||||
### 4. chair directions
|
||||
- העתק מלא של עמדות הוועדה מ-analysis-and-research.md (או הפנייה: "קרא get_chair_directions")
|
||||
- העתק מלא של עמדות הוועדה מ-analysis-and-research.md (או הפנייה: "קרא get_chair_directions").
|
||||
- **עטוף את ההעתק המילולי בתגית `<chair_directions>…</chair_directions>`** — כך הכותב מבחין בין הוראות-היו"ר המילוליות לבין הערות ה-CEO, ואינו דורס אותן. בתוך התגית: טקסט מילולי בלבד, בלי פרפרזה.
|
||||
|
||||
### 5. הנחיות סגנון
|
||||
- ניטרליות: בלוק ו = עובדות בלבד, בלי ציטוטים מצדדים
|
||||
|
||||
@@ -96,7 +96,7 @@ tools:
|
||||
"id": "r1",
|
||||
"type": "insert_after", // או insert_before, replace, delete
|
||||
"anchor_bookmark": "block-yod", // מ-list_bookmarks
|
||||
"content": "וכך נפסק בעניין פלוני. בבג\"ץ 1234/21 קבע השופט...",
|
||||
"content": "<תוכן ההוספה — טקסט עברי נקי, בלי ציטוט מומצא>",
|
||||
"style": "body", // או heading, quote
|
||||
"reason": "הוספת פסק הלכה שחסר לפי בקשת יו\"ר"
|
||||
}
|
||||
@@ -128,7 +128,7 @@ tools:
|
||||
|
||||
בלי סגירת-issue, Paperclip מזהה "in_progress בלי execution חיה" ומפעיל auto-retry בלולאה (נצפה ב-CMPA-17, 30/04/26 — 4 איטרציות מיותרות עד הריגה ידנית).
|
||||
|
||||
**הפרוטוקול המלא — מקור יחיד: [HEARTBEAT.md](HEARTBEAT.md) §4ב (סטטוס) + §4ג (wake CEO לפי חברה).** בקצרה: PATCH סטטוס `done` (הצלחה) או `blocked` (כשל/פלט-חסר), ואז wakeup ל-CEO עם `payload.issueId` ו-`reason="מייצא טיוטה סיים [issue-id] בסטטוס [done/blocked]"`. **אסור** `done` עם פלט חסר; **אסור** `INSERT INTO agent_wakeup_requests` ישיר; **אסור** לקבע UUID של CEO (נגזר מ-`$PAPERCLIP_COMPANY_ID`).
|
||||
**הפרוטוקול המלא — מקור יחיד: [HEARTBEAT.md](HEARTBEAT.md) §4ב (סטטוס) + §4ג (wake CEO).** בקצרה: PATCH סטטוס `done`/`blocked`, ואז wakeup ל-CEO עם `payload.issueId` ו-`reason="מייצא טיוטה סיים [issue-id] בסטטוס [done/blocked]"`.
|
||||
|
||||
## כללים קריטיים
|
||||
|
||||
|
||||
@@ -96,5 +96,4 @@ tools:
|
||||
|
||||
בלי סגירת-issue, Paperclip מזהה "in_progress בלי execution חיה" ומפעיל auto-retry בלולאה (נצפה ב-CMPA-17, 30/04/26 — 4 איטרציות מיותרות עד הריגה ידנית).
|
||||
|
||||
**הפרוטוקול המלא — מקור יחיד: [HEARTBEAT.md](HEARTBEAT.md) §4ב (סטטוס) + §4ג (wake CEO לפי חברה).** בקצרה: PATCH סטטוס `done` (הצלחה) או `blocked` (כשל / markers `[?]` רבים), ואז wakeup ל-CEO עם `payload.issueId` ו-`reason="מגיה סיים [issue-id] בסטטוס [done/blocked]"`. **אסור** `done` עם פלט חסר; **אסור** `INSERT INTO agent_wakeup_requests` ישיר; **אסור** לקבע UUID של CEO (נגזר מ-`$PAPERCLIP_COMPANY_ID`).
|
||||
**⚠️ אסור לקבע UUID של CEO** — UUID שונה לכל חברה. תמיד דרך `$PAPERCLIP_COMPANY_ID`. wakeup לחברה אחרת נדחה: `Agent key cannot access another company`.
|
||||
**הפרוטוקול המלא — מקור יחיד: [HEARTBEAT.md](HEARTBEAT.md) §4ב (סטטוס) + §4ג (wake CEO).** בקצרה: PATCH סטטוס `done`/`blocked` (`blocked` אם markers `[?]` רבים), ואז wakeup ל-CEO עם `payload.issueId` ו-`reason="מגיה סיים [issue-id] בסטטוס [done/blocked]"`.
|
||||
|
||||
@@ -56,6 +56,8 @@ tools:
|
||||
|
||||
## 7 בדיקות
|
||||
|
||||
> **סף-הקבלה המשותף — `docs/qa-acceptance-rubric.md`:** 7 הבדיקות למטה הן האכיפה המפורטת שלו (מקור-אמת אחד; הכותב מכוון לאותו rubric). בדיווח — מפה כל `fail` לחוסם/אזהרה שם.
|
||||
|
||||
### 1. שלמות מבנית (structural_integrity)
|
||||
- כל בלוקי חובה קיימים (ה עד יא)
|
||||
- מספור רציף ללא קפיצות
|
||||
@@ -232,14 +234,24 @@ new → processing → documents_ready → analyst_verified → research_complet
|
||||
6. האם יש אנלוגיה לתקדים ללא הסבר מדיניות (למה הדמיון רלוונטי)?
|
||||
|
||||
### שלב 3: דיווח — חובה!
|
||||
פרסם comment ב-Paperclip עם:
|
||||
- תוצאת כל בדיקה (pass/fail)
|
||||
- רשימת שגיאות מפורטת (אם יש)
|
||||
- האם מותר לייצא (כל הקריטיים pass?)
|
||||
- עדכן סטטוס ל-qa_review (אם נכשל) או drafted (אם עבר)
|
||||
פרסם comment ב-Paperclip **במבנה קבוע** (markdown — נקרא ע"י היו"ר וע"י ה-CEO), כדי שהפסיקה תהיה עקבית וניתנת-לפרסינג. מלא שורה לכל אחת מ-7 הבדיקות:
|
||||
|
||||
```markdown
|
||||
## דוח בדיקת איכות — ערר {case_number}
|
||||
|
||||
| בדיקה | תוצאה | חומרה | ממצא (אם fail) |
|
||||
|-------|:-----:|:-----:|-----------------|
|
||||
| {שם הבדיקה} | pass / fail | critical / major / minor | {פירוט קצר} |
|
||||
| ... שורה לכל 7 הבדיקות ... | | | |
|
||||
|
||||
**מותר לייצא:** כן / לא ← "לא" אם ולו בדיקה **critical** אחת ב-fail
|
||||
**סטטוס חדש:** drafted (עבר) / qa_review (נכשל)
|
||||
```
|
||||
|
||||
- עדכן סטטוס בהתאם: `drafted` (אם עבר) או `qa_review` (אם נכשל).
|
||||
|
||||
### סגור את ה-issue של עצמך + העֵר CEO — חובה!
|
||||
|
||||
בלי סגירת-issue, Paperclip מזהה "in_progress בלי execution חיה" ומפעיל auto-retry בלולאה (נצפה ב-CMPA-17, 30/04/26 — 4 איטרציות מיותרות עד הריגה ידנית).
|
||||
|
||||
**הפרוטוקול המלא — מקור יחיד: [HEARTBEAT.md](HEARTBEAT.md) §4ב (סטטוס) + §4ג (wake CEO לפי חברה).** בקצרה: PATCH סטטוס `done` (הצלחה) או `blocked` (כשל/פלט-חסר), ואז wakeup ל-CEO עם `payload.issueId` ו-`reason="בודק איכות סיים [issue-id] בסטטוס [done/blocked]"`. **אסור** `done` עם פלט חסר; **אסור** `INSERT INTO agent_wakeup_requests` ישיר; **אסור** לקבע UUID של CEO (נגזר מ-`$PAPERCLIP_COMPANY_ID`).
|
||||
**הפרוטוקול המלא — מקור יחיד: [HEARTBEAT.md](HEARTBEAT.md) §4ב (סטטוס) + §4ג (wake CEO).** בקצרה: PATCH סטטוס `done`/`blocked`, ואז wakeup ל-CEO עם `payload.issueId` ו-`reason="בודק איכות סיים [issue-id] בסטטוס [done/blocked]"`.
|
||||
|
||||
@@ -410,7 +410,7 @@ python3 /home/chaim/legal-ai/scripts/notify.py \
|
||||
|
||||
בלי סגירת-issue, Paperclip מזהה "in_progress בלי execution חיה" ומפעיל auto-retry בלולאה (נצפה ב-CMPA-17, 30/04/26 — 4 איטרציות מיותרות עד הריגה ידנית).
|
||||
|
||||
**הפרוטוקול המלא — מקור יחיד: [HEARTBEAT.md](HEARTBEAT.md) §4ב (סטטוס) + §4ג (wake CEO לפי חברה).** בקצרה: PATCH סטטוס `done` (הצלחה) או `blocked` (כשל/פלט-חסר), ואז wakeup ל-CEO עם `payload.issueId` ו-`reason="חוקר תקדימים סיים [issue-id] בסטטוס [done/blocked]"`. **אסור** `done` עם פלט חסר; **אסור** `INSERT INTO agent_wakeup_requests` ישיר; **אסור** לקבע UUID של CEO (נגזר מ-`$PAPERCLIP_COMPANY_ID`).
|
||||
**הפרוטוקול המלא — מקור יחיד: [HEARTBEAT.md](HEARTBEAT.md) §4ב (סטטוס) + §4ג (wake CEO).** בקצרה: PATCH סטטוס `done`/`blocked`, ואז wakeup ל-CEO עם `payload.issueId` ו-`reason="חוקר תקדימים סיים [issue-id] בסטטוס [done/blocked]"`.
|
||||
|
||||
## כללים
|
||||
- **דיוק** — ציין מספרי סעיפים, תאריכים, שמות שופטים
|
||||
|
||||
@@ -53,6 +53,8 @@ tools:
|
||||
|
||||
## לפני שאתה מתחיל — קרא!
|
||||
|
||||
> **המטרה שלך — `docs/qa-acceptance-rubric.md`:** זהו סף-הקבלה שה-QA יאכוף. כתוב כדי לעבור אותו מהפעם הראשונה, ולפני מסירה ל-CEO עבור על החוסמים ואשר שכל אחד מתקיים.
|
||||
|
||||
### חובה לפני כל כתיבה — נקודת ההתחלה:
|
||||
0. **עץ ההחלטה: `docs/daphna-decision-tree.md`** — **כלי הפעולה היומיומי**. מאחד את כל המסמכים לתהליך אנליטי קצר: מהי הראיה הניצחת? איזה ארכיטקטורה? איזה מוד פתיחה? איזה אורך? **תמיד להתחיל כאן** — המסמך מצביע איזה מסמך אחר לקרוא לפי השאלה.
|
||||
|
||||
@@ -122,9 +124,11 @@ tools:
|
||||
4. החזר את הטקסט ל-CEO (בקומנט או כ-return value) — **לא** שומר ב-DB
|
||||
5. CEO יקרא ל-`revise_draft` עם הטקסט שלך
|
||||
|
||||
**דוגמה לפלט מצופה:**
|
||||
**דוגמה לפלט מצופה** (עטופה בתגית כדי שלא תתבלבל עם הוראה):
|
||||
|
||||
> בבג"ץ 1234/21 [פלוני נ' הוועדה המחוזית] קבע בית המשפט העליון כי הוועדה המקומית מחויבת לשקול שיקולי Y גם בהיעדר התנגדות מפורשת. הלכה זו חלה ישירות על ענייננו: הוועדה המקומית לא בחנה את Y, ודי בכך כדי להחזיר את הדיון לוועדה.
|
||||
<example_output>
|
||||
בבג"ץ 1234/21 [פלוני נ' הוועדה המחוזית] קבע בית המשפט העליון כי הוועדה המקומית מחויבת לשקול שיקולי Y גם בהיעדר התנגדות מפורשת. הלכה זו חלה ישירות על ענייננו: הוועדה המקומית לא בחנה את Y, ודי בכך כדי להחזיר את הדיון לוועדה.
|
||||
</example_output>
|
||||
|
||||
---
|
||||
|
||||
@@ -218,18 +222,13 @@ case_update(case_number, status="drafted")
|
||||
|
||||
בלי סגירת-issue, Paperclip מזהה "in_progress בלי execution חיה" ומפעיל auto-retry בלולאה (נצפה ב-CMPA-17, 30/04/26 — 4 איטרציות מיותרות עד הריגה ידנית).
|
||||
|
||||
**הפרוטוקול המלא — מקור יחיד: [HEARTBEAT.md](HEARTBEAT.md) §4ב (סטטוס) + §4ג (wake CEO לפי חברה).** בקצרה: PATCH סטטוס `done` (הצלחה) או `blocked` (כשל/פלט-חסר), ואז wakeup ל-CEO עם `payload.issueId` ו-`reason="כותב החלטה סיים [issue-id] בסטטוס [done/blocked]"`. **אסור** `done` עם פלט חסר; **אסור** `INSERT INTO agent_wakeup_requests` ישיר; **אסור** לקבע UUID של CEO (נגזר מ-`$PAPERCLIP_COMPANY_ID`).
|
||||
**הפרוטוקול המלא — מקור יחיד: [HEARTBEAT.md](HEARTBEAT.md) §4ב (סטטוס) + §4ג (wake CEO).** בקצרה: PATCH סטטוס `done`/`blocked`, ואז wakeup ל-CEO עם `payload.issueId` ו-`reason="כותב החלטה סיים [issue-id] בסטטוס [done/blocked]"`.
|
||||
|
||||
**אם לא תעדכן סטטוס ל-drafted — בודק האיכות לא יוכל לרוץ!**
|
||||
|
||||
## בלוק י — דיון (הבלוק החשוב ביותר)
|
||||
|
||||
**קריאת חובה לפני כתיבה (5 מסמכים)**:
|
||||
1. `docs/daphna-voice-fingerprint.md` — קבועים, פעלי "אנחנו", אנטי-דפוסים
|
||||
2. `docs/daphna-precedent-network.md` — לכל סוגיה משפטית, איזה תקדם
|
||||
3. `docs/daphna-architecture-by-outcome.md` — מבנה לפי תוצאה + עץ החלטה
|
||||
4. `docs/daphna-acceptance-architecture.md` — **חובה אם תוצאה צפויה: קבלה (לא חלקית).** חמש תבניות מובחנות
|
||||
5. `docs/voice-1130-25.md` — דוגמה עמוקה
|
||||
> קרא קודם את **"חובה לפני בלוק י (חמישיית הקול)"** שלמעלה — חמשת מסמכי-הקול.
|
||||
|
||||
**עץ החלטה לבחירת ארכיטקטורה**:
|
||||
1. מה התוצאה?
|
||||
@@ -373,6 +372,8 @@ case_update(case_number, status="drafted")
|
||||
|
||||
### חובה: שימוש בעמדות יו"ר מ-`get_chair_directions`
|
||||
|
||||
> **מקור העמדות:** בדרך-כלל מ-`get_chair_directions` (שלב 1ג). אם ה-CEO **העתיק** עמדות-יו"ר לתוך ה-issue שלך, הן מגיעות עטופות ב-`<chair_directions>…</chair_directions>` — טקסט מילולי זה **מחייב אותך בדיוק כמו** `chair_ruling` מהכלי: אל תדרוס, אל תפרפרז, נסח בעדו.
|
||||
|
||||
עבור **כל טענת סף** ו**כל סוגיה** ב-direction_doc שבנית בשלב 1ג:
|
||||
|
||||
1. **פתח את הדיון במסקנה של דפנה** — למשל "**טענת הסף הראשונה נדחית**"
|
||||
|
||||
57
docs/qa-acceptance-rubric.md
Normal file
57
docs/qa-acceptance-rubric.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Rubric קבלה של החלטה — מקור-אמת אחד
|
||||
|
||||
מסמך זה הוא **הגדרת ה"מוכן"** של טיוטת החלטה: סף-הקבלה שהכותב **מכוון אליו מראש**
|
||||
וה-QA **אוכף** כשער. שני הסוכנים מפנים לכאן — אין לשכפל את הקריטריונים בקבצי-הסוכן.
|
||||
|
||||
**זהו סף-הקבלה, לא הוראת-הכתיבה.** ה"איך" המלא חי במסמכי-הקול המופנים בכל שורה
|
||||
(`daphna-voice-fingerprint.md`, `daphna-block-zayin-claims.md`,
|
||||
`daphna-architecture-by-outcome.md`, `daphna-acceptance-architecture.md`,
|
||||
`decision-methodology.md`, `anti-hallucination-gate.md`). כאן — רק המבחן.
|
||||
|
||||
> **לכותב:** לפני מסירה ל-CEO, עבור על החוסמים למטה ואשר שכל אחד מתקיים. טיוטה
|
||||
> שלא עוברת חוסם אחד — תוחזר ב-`needs_revision`. כתוב כדי לעבור מהפעם הראשונה.
|
||||
>
|
||||
> **ל-QA:** אלה הקריטריונים ש-7 הבדיקות שלך אוכפות. `fail` על חוסם = `qa_review`;
|
||||
> `fail` על אזהרה = דיווח לחיים, לא חוסם.
|
||||
|
||||
---
|
||||
|
||||
## חוסמי ייצוא (critical — כל אחד מכשיל את הטיוטה)
|
||||
|
||||
1. **שלמות מבנית** — בלוקים ה–יא קיימים; מספור רציף ללא קפיצות/כפילות; "להלן" בשימוש ראשון.
|
||||
2. **רקע ניטרלי (בלוק ו)** — עובדות בלבד; **אפס** ציטוטים מצדדים; **אפס** מילות-שיפוט
|
||||
("חריג", "בעייתי", "מגוחך", "פגום", "שערורייתי").
|
||||
3. **כיסוי טענות** — כל טענה מהותית מבלוק ז קיבלה מענה בבלוק י (ישיר / קיבוץ / ציון שנבחנה).
|
||||
טענות `[skip]` לא נספרות; `[bundle]` נבדקות כקבוצה. טענה מהותית לא-מסומנת שלא נענתה = כשל.
|
||||
4. **מתודולוגיה** — לכל סוגיה מבנה סילוגיסטי (כלל+עובדות+מסקנה); ממצא עובדתי מופרד ממסקנה
|
||||
משפטית; טענת הצד המפסיד הוצגה בחוזקתה (Steel-Man); איזון מפורש כשנדרש; בלי "נוסחאות ריקות";
|
||||
ציטוטים בסנדוויץ' (הקדמה→ציטוט→ניתוח).
|
||||
5. **קול דפנה** —
|
||||
- בלוק ז: כותרת **"תמצית טענות הצדדים"**; כותרת-משנה לכל צד; בלי רשימה ממוספרת בפסקה;
|
||||
בלי מילות-הערכה; בלי גילוי-מסקנה; קול פעיל ("העורר טוען").
|
||||
- בלוק י: כותרת **"דיון והכרעה"**; קול "אנחנו" תפקידי ("מצאנו", לא "הוועדה מוצאת");
|
||||
דפוס **"אכן... אולם"** לטענות שנדחות; בלי מספור-פסקאות סדרתי; ציטוטי פסיקה במלואם (4–15 שורות);
|
||||
"למעלה מן הצורך" לטיעונים מרכזיים; בלי תוצאת הכל-או-לא-כלום כשיש טענות מהותיות משני הצדדים.
|
||||
- 8xxx עם הכרעה שמאית → ציטוט **בר"מ 3644/13** קיים.
|
||||
6. **עיגון-מקור (INV-AH)** — כל אזכור פסיקה/חוק/הלכה/מספר מעוגן-מקור עם ציטוט; ציטוט חיצוני
|
||||
תואם להלכה מאושרת בקורפוס (`search_precedent_library`). אזכור לא-מעוגן = הסר או הורד ל-`[ספקולציה]`.
|
||||
7. **צירוף פסיקה ל-DB** — כל ציטוט בבלוק י (חיצוני/internal) רשום ב-`case_precedents` דרך
|
||||
`precedent_attach`. ציטוט שקיים רק בטקסט ולא ב-DB = חוסם (ה-exporter ו-Hermes קוראים מה-DB).
|
||||
8. **log שאילתות-קורפוס** — `analysis-and-research.md` §7א ו-`precedent-research.md` §ז קיימים,
|
||||
עם מינימום queries לקורפוס הסמכותי (`analyst ≥ טענות-סף + סוגיות`; `researcher ≥ סוגיות`)
|
||||
ותיעוד negative-evidence (גם 0-results).
|
||||
|
||||
## אזהרות (warning — מדווח לחיים, לא חוסם)
|
||||
- **משקלות בטווח:** ו 15–40% · ז 20–40% · י 32–50% · יא 2–9%.
|
||||
- **בלי כפילות:** בלוק י מפנה לבלוקים קודמים ("כאמור"), לא חוזר על עובדות/טענות.
|
||||
- **מראה-מקום מלא בציטוטים:** מספר-תיק מלא + ערכאה + תאריך/`פורסם בנבו` (+ `page_reference` לציטוט ארוך).
|
||||
|
||||
## אם תוצאה = קבלה (מ-`daphna-acceptance-architecture.md`)
|
||||
- התבנית הנבחרת (A/B/C/D/E) מתאימה לסיבת-הקבלה, ופורמט-הסיום תואם לתבנית
|
||||
(A: "מתבטלת" + הודאת-צד-נגדי; B: "תיקבע לדיון"; C: "בכפוף לתיקונים" + הכרה בוועדה;
|
||||
D: "דרישת התשלום בטלה"; E: "השומה תושב לתיקון").
|
||||
|
||||
---
|
||||
|
||||
> **מקור-אמת:** חומרת-הבדיקות המחייבת חיה ב-`.claude/agents/legal-qa.md` ("7 בדיקות" + טבלת-חומרה)
|
||||
> וב-`docs/spec/05-qa-review.md`. Rubric זה הוא התמצית המשותפת — כשמשתנה שער QA, עדכן שם ואת התמצית כאן.
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
useSendComment,
|
||||
useSubmitInteraction,
|
||||
useDismissInteraction,
|
||||
useSetIssueStatus,
|
||||
} from "@/lib/api/agents";
|
||||
import type {
|
||||
Interaction,
|
||||
@@ -772,14 +773,34 @@ function IssueGroup({
|
||||
defaultOpen: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
const setStatus = useSetIssueStatus(caseNumber);
|
||||
const closed = issue.status === "done" || issue.status === "cancelled";
|
||||
// Show the parent run this issue hangs under (e.g. a "הרצה חדשה" nested under
|
||||
// the live CEO root) so the hierarchy is visible, not flattened.
|
||||
const parentIdentifier = issue.parent_id
|
||||
? issueMap.get(issue.parent_id)
|
||||
: undefined;
|
||||
|
||||
const close = (status: "done" | "cancelled") =>
|
||||
setStatus.mutate(
|
||||
{ issue_id: issue.id, status },
|
||||
{
|
||||
onSuccess: () =>
|
||||
toast.success(
|
||||
status === "done" ? "המשימה סומנה כהושלמה" : "המשימה בוטלה",
|
||||
),
|
||||
onError: () => toast.error("שגיאה בעדכון סטטוס המשימה"),
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="border-b border-rule-soft last:border-b-0">
|
||||
<div className="border-b-2 border-rule last:border-b-0">
|
||||
<div className="w-full flex items-center gap-2 px-2 py-2.5 hover:bg-sand-soft/50">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
aria-expanded={open}
|
||||
className="w-full flex items-center gap-2 px-2 py-2.5 hover:bg-sand-soft/50 text-start"
|
||||
className="flex-1 flex items-center gap-2 text-start min-w-0"
|
||||
>
|
||||
<Badge
|
||||
variant={closed ? "secondary" : "default"}
|
||||
@@ -790,13 +811,58 @@ function IssueGroup({
|
||||
<span className="text-[0.78rem] font-semibold text-navy truncate">
|
||||
{shortIssueTitle(issue.title)}
|
||||
</span>
|
||||
{parentIdentifier && (
|
||||
<span
|
||||
className="inline-flex items-center gap-0.5 text-[0.6rem] font-bold text-gold-deep bg-gold/10 border border-rule rounded-full px-1.5 py-0.5 shrink-0 whitespace-nowrap"
|
||||
title={`תת-משימה של ${parentIdentifier}`}
|
||||
>
|
||||
↳ {parentIdentifier}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[0.7rem] text-ink-muted whitespace-nowrap">
|
||||
{issueStatusLabel(issue.status)} · {items.length}
|
||||
</span>
|
||||
</button>
|
||||
{!closed && (
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => close("done")}
|
||||
disabled={setStatus.isPending}
|
||||
className="h-7 px-2 text-[0.7rem] text-success border-success/40 hover:bg-success-bg"
|
||||
title="סמן את המשימה כהושלמה"
|
||||
>
|
||||
{setStatus.isPending ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle2 className="w-3.5 h-3.5 me-1" />
|
||||
)}
|
||||
הושלם
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => close("cancelled")}
|
||||
disabled={setStatus.isPending}
|
||||
className="h-7 px-2 text-[0.7rem] text-ink-muted hover:text-danger"
|
||||
title="בטל את המשימה"
|
||||
>
|
||||
בטל
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
aria-label={open ? "כווץ" : "הרחב"}
|
||||
className="shrink-0"
|
||||
>
|
||||
<ChevronDown
|
||||
className={`w-4 h-4 ms-auto text-ink-faint shrink-0 transition-transform ${open ? "rotate-180" : ""}`}
|
||||
className={`w-4 h-4 text-ink-faint transition-transform ${open ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{open && (
|
||||
<div className="pb-1">
|
||||
{items.map((item) =>
|
||||
@@ -878,6 +944,13 @@ function TargetSelector({
|
||||
const defaultIssue = useMemo(() => pickDefaultTarget(issues), [issues]);
|
||||
const activeIssues = issues.filter((i) => isOpenStatus(i.status));
|
||||
const closedIssues = issues.filter((i) => !isOpenStatus(i.status));
|
||||
// id → identifier, to label a run with the parent it hangs under (↳ CMP-189).
|
||||
const idToIdentifier = useMemo(
|
||||
() => new Map(issues.map((i) => [i.id, i.identifier])),
|
||||
[issues],
|
||||
);
|
||||
const parentTag = (i: PaperclipIssue) =>
|
||||
i.parent_id ? idToIdentifier.get(i.parent_id) : undefined;
|
||||
|
||||
// The issue the pill currently represents (explicit pick, or the auto default).
|
||||
const effectiveIssue =
|
||||
@@ -925,7 +998,31 @@ function TargetSelector({
|
||||
onClick={() => setOpen(false)}
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="absolute z-20 top-full mt-1 start-0 w-[360px] max-w-[88vw] bg-white border border-rule rounded-xl shadow-lg p-1.5">
|
||||
<div className="absolute z-20 top-full mt-1 start-0 w-[360px] max-w-[88vw] max-h-[62vh] overflow-y-auto overscroll-contain bg-white border border-rule rounded-xl shadow-lg p-1.5">
|
||||
{/* "פתח הרצה חדשה" is the FIRST, most-prominent option so it's always
|
||||
visible without scrolling to the bottom of a long task list. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange({ kind: "new_run" });
|
||||
setOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-2.5 px-2 py-2 rounded-lg text-start bg-gold/10 border border-rule hover:border-gold/60"
|
||||
>
|
||||
<span className="w-6 h-6 rounded-md bg-navy text-white flex items-center justify-center shrink-0">
|
||||
<Plus className="w-4 h-4" />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-xs font-semibold text-navy">
|
||||
פתח הרצה חדשה
|
||||
</span>
|
||||
<span className="block text-[10px] text-ink-muted">
|
||||
ה-CEO ייצור משימה חדשה ויטפל בהוראה מאפס
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
<div className="h-px bg-rule-soft my-1.5 mx-1" />
|
||||
|
||||
<div className="px-2 py-1 text-[10px] font-bold text-ink-faint uppercase tracking-wide">
|
||||
משימות פעילות
|
||||
</div>
|
||||
@@ -933,6 +1030,7 @@ function TargetSelector({
|
||||
const selected =
|
||||
(target.kind === "issue" && target.id === i.id) ||
|
||||
(target.kind === "auto" && defaultIssue?.id === i.id);
|
||||
const parent = parentTag(i);
|
||||
return (
|
||||
<button
|
||||
key={i.id}
|
||||
@@ -941,12 +1039,20 @@ function TargetSelector({
|
||||
onChange({ kind: "issue", id: i.id });
|
||||
setOpen(false);
|
||||
}}
|
||||
className={`w-full flex items-center gap-2 px-2 py-2 rounded-lg text-start hover:bg-sand-soft ${selected ? "bg-gold/10" : ""}`}
|
||||
className={`w-full flex items-center gap-2 px-2 py-2 rounded-lg text-start hover:bg-sand-soft ${parent ? "ps-6" : ""} ${selected ? "bg-gold/10" : ""}`}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 shrink-0" />
|
||||
<Badge variant="outline" className="text-[10px] font-mono shrink-0">
|
||||
{i.identifier}
|
||||
</Badge>
|
||||
{parent && (
|
||||
<span
|
||||
className="text-[9px] font-bold text-gold-deep bg-gold/10 border border-rule rounded-full px-1.5 py-0.5 shrink-0 whitespace-nowrap"
|
||||
title={`תת-משימה של ${parent}`}
|
||||
>
|
||||
↳ {parent}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-ink-soft truncate flex-1">
|
||||
{shortIssueTitle(i.title)}
|
||||
</span>
|
||||
@@ -968,16 +1074,26 @@ function TargetSelector({
|
||||
<div className="px-2 py-1 text-[10px] font-bold text-ink-faint uppercase tracking-wide">
|
||||
משימות סגורות · שליחה אליהן לא תעיר סוכן
|
||||
</div>
|
||||
{closedIssues.map((i) => (
|
||||
{closedIssues.map((i) => {
|
||||
const parent = parentTag(i);
|
||||
return (
|
||||
<div
|
||||
key={i.id}
|
||||
className="w-full flex items-center gap-2 px-2 py-2 rounded-lg opacity-60 cursor-not-allowed"
|
||||
className={`w-full flex items-center gap-2 px-2 py-2 rounded-lg opacity-60 cursor-not-allowed ${parent ? "ps-6" : ""}`}
|
||||
title="משימה סגורה — שליחה אליה לא תעיר סוכן"
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-ink-faint shrink-0" />
|
||||
<Badge variant="outline" className="text-[10px] font-mono shrink-0">
|
||||
{i.identifier}
|
||||
</Badge>
|
||||
{parent && (
|
||||
<span
|
||||
className="text-[9px] font-bold text-gold-deep bg-gold/10 border border-rule rounded-full px-1.5 py-0.5 shrink-0 whitespace-nowrap"
|
||||
title={`תת-משימה של ${parent}`}
|
||||
>
|
||||
↳ {parent}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs text-ink-faint truncate flex-1">
|
||||
{shortIssueTitle(i.title)}
|
||||
</span>
|
||||
@@ -987,31 +1103,10 @@ function TargetSelector({
|
||||
{issueStatusLabel(i.status)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="h-px bg-rule-soft my-1.5 mx-1" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onChange({ kind: "new_run" });
|
||||
setOpen(false);
|
||||
}}
|
||||
className="w-full flex items-center gap-2.5 px-2 py-2 rounded-lg text-start hover:bg-sand-soft"
|
||||
>
|
||||
<span className="w-6 h-6 rounded-md bg-navy text-white flex items-center justify-center shrink-0">
|
||||
<Plus className="w-4 h-4" />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
<span className="block text-xs font-semibold text-navy">
|
||||
פתח הרצה חדשה
|
||||
</span>
|
||||
<span className="block text-[10px] text-ink-muted">
|
||||
ה-CEO ייצור משימה חדשה ויטפל בהוראה מאפס
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -208,6 +208,25 @@ export function useDismissInteraction(caseNumber: string | undefined) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Manually close a board issue to done/cancelled — the chair tidying the agents
|
||||
* board (e.g. superseded runs left in in_review). Backend does a loop-safe
|
||||
* direct close and issues no wakeup; only the two terminal statuses are allowed. */
|
||||
export function useSetIssueStatus(caseNumber: string | undefined) {
|
||||
const qc = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (vars: { issue_id: string; status: "done" | "cancelled" }) =>
|
||||
apiRequest<{ ok: boolean; id: string; identifier: string; status: string }>(
|
||||
`/api/cases/${caseNumber}/agents/issue-status`,
|
||||
{ method: "POST", body: vars },
|
||||
),
|
||||
onSuccess: () => {
|
||||
if (caseNumber) {
|
||||
qc.invalidateQueries({ queryKey: agentKeys.activity(caseNumber) });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export type AgentResetResult = {
|
||||
ok: boolean;
|
||||
reassigned_issues: { id: string; identifier: string }[];
|
||||
|
||||
@@ -68,6 +68,7 @@ from web.paperclip_client import (
|
||||
reap_stale_interactions as _reap_stale_interactions,
|
||||
reset_agent_session as _reset_agent_session,
|
||||
reset_case_agents as _reset_case_agents,
|
||||
set_issue_status as _set_issue_status,
|
||||
wake_analyst_for_appraiser_facts as _wake_analyst_for_appraiser_facts,
|
||||
wake_analyst_for_argument_aggregation as _wake_analyst_for_argument_aggregation,
|
||||
wake_analyst_for_protocol_analysis as _wake_analyst_for_protocol_analysis,
|
||||
@@ -127,6 +128,12 @@ pc_cancel_interaction = instrument(
|
||||
pc_escalate_issue = instrument(
|
||||
"agent.escalated", keys=("issue_id", "severity", "company_id", "reason"),
|
||||
)(_escalate_issue)
|
||||
# Manual chair close of a board issue (done/cancelled) — the loop-safe primitive
|
||||
# behind the agents-board "סמן כהושלם / בטל" actions. Countable per case in the
|
||||
# same telemetry stream as the escalations/wakeups it sits beside.
|
||||
pc_set_issue_status = instrument(
|
||||
"issue.status_set", keys=("issue_id", "status", "company_id"),
|
||||
)(_set_issue_status)
|
||||
pc_reap_stale_interactions = instrument(
|
||||
"interaction.reaped", result_keys=("cancelled",),
|
||||
)(_reap_stale_interactions)
|
||||
@@ -192,6 +199,7 @@ __all__ = [
|
||||
"pc_cancel_interaction",
|
||||
"pc_reap_stale_interactions",
|
||||
"pc_escalate_issue",
|
||||
"pc_set_issue_status",
|
||||
# agent-run observability + control (live view + smart management)
|
||||
"pc_get_agent_health",
|
||||
"pc_get_recent_escalations",
|
||||
|
||||
24
web/app.py
24
web/app.py
@@ -82,6 +82,7 @@ from web.agent_platform_port import (
|
||||
pc_reset_agent_session,
|
||||
pc_reset_case_agents,
|
||||
pc_respond_to_interaction,
|
||||
pc_set_issue_status,
|
||||
pc_restore_project,
|
||||
pc_wake_analyst_for_appraiser_facts,
|
||||
pc_wake_analyst_for_argument_aggregation,
|
||||
@@ -4851,6 +4852,29 @@ async def api_escalate_issue(case_number: str, req: EscalateRequest):
|
||||
return result
|
||||
|
||||
|
||||
class IssueStatusRequest(BaseModel):
|
||||
issue_id: str
|
||||
status: Literal["done", "cancelled"]
|
||||
|
||||
|
||||
@app.post("/api/cases/{case_number}/agents/issue-status")
|
||||
async def api_set_issue_status(case_number: str, req: IssueStatusRequest):
|
||||
"""Manually close a board issue to done/cancelled from the chair UI.
|
||||
|
||||
Lets the chair tidy the agents board by hand — superseded "הרצה חדשה" runs
|
||||
left in ``in_review`` had no manual close control and piled up. Only the two
|
||||
terminal statuses are accepted; the loop-safe direct-DB close lives behind the
|
||||
Port (``pc_set_issue_status``) and issues no wakeup.
|
||||
"""
|
||||
issues = await pc_get_case_issues(case_number)
|
||||
if not any(i["id"] == req.issue_id for i in issues):
|
||||
raise HTTPException(404, f"Issue {req.issue_id} לא שייך לתיק {case_number}")
|
||||
result = await pc_set_issue_status(req.issue_id, req.status)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(400, result.get("error", "עדכון הסטטוס נכשל"))
|
||||
return result
|
||||
|
||||
|
||||
# ── Settings: MCP Server Configuration ────────────────────────────
|
||||
#
|
||||
# Source of truth for legal-ai env vars is Coolify (see memory:
|
||||
|
||||
@@ -988,6 +988,70 @@ async def escalate_issue(
|
||||
}
|
||||
|
||||
|
||||
async def set_issue_status(
|
||||
issue_id: str, status: str, company_id: str = "",
|
||||
) -> dict:
|
||||
"""Manually close a case issue to ``done`` or ``cancelled`` from the chair UI.
|
||||
|
||||
The chair needs to tidy the agents board by hand: superseded "הרצה חדשה" runs
|
||||
that were replaced by a later run pile up in ``in_review`` with no way to close
|
||||
them — there was no manual control, so they had to be closed out-of-band. This
|
||||
is the loop-safe primitive behind the board's "סמן כהושלם / בטל" actions.
|
||||
|
||||
Only the two **terminal** statuses are accepted (``CLOSED_ISSUE_STATUSES``) —
|
||||
this tidies the board, it does not drive the workflow (agents own the
|
||||
todo/in_progress/in_review transitions). Direct-DB in one transaction,
|
||||
mirroring :func:`escalate_issue`: bypasses Paperclip's disposition resolver
|
||||
(whose multi-PATCH/``issue.released`` behaviour *causes* the recovery loops),
|
||||
sets the matching close timestamp, clears any agent assignee so no recovery
|
||||
sweep re-acts on it, and records an ``author_type='system'`` audit note (inert
|
||||
w.r.t. the user-comment routing sweep, so it will not wake an agent). No wakeup.
|
||||
"""
|
||||
if status not in CLOSED_ISSUE_STATUSES:
|
||||
return {
|
||||
"ok": False,
|
||||
"error": f"invalid status {status!r}; expected one of {tuple(CLOSED_ISSUE_STATUSES)}",
|
||||
}
|
||||
|
||||
# status is validated against the frozenset above, so the interpolated column
|
||||
# name is one of exactly two literals — no injection surface.
|
||||
ts_col = "completed_at" if status == "done" else "cancelled_at"
|
||||
note = (
|
||||
"✓ סומן ידנית כהושלם ע\"י היו\"ר"
|
||||
if status == "done"
|
||||
else "✕ בוטל ידנית ע\"י היו\"ר"
|
||||
)
|
||||
conn = await asyncpg.connect(PAPERCLIP_DB_URL)
|
||||
try:
|
||||
async with conn.transaction():
|
||||
row = await conn.fetchrow(
|
||||
f"""UPDATE issues
|
||||
SET status=$1, {ts_col}=now(),
|
||||
assignee_agent_id=null, updated_at=now()
|
||||
WHERE id=$2::uuid
|
||||
RETURNING id, identifier, company_id""",
|
||||
status, issue_id,
|
||||
)
|
||||
if not row:
|
||||
return {"ok": False, "error": f"issue {issue_id} not found"}
|
||||
cid = company_id or str(row["company_id"])
|
||||
await conn.execute(
|
||||
"""INSERT INTO issue_comments (id, company_id, issue_id, body, author_type)
|
||||
VALUES ($1::uuid, $2::uuid, $3::uuid, $4, 'system')""",
|
||||
str(uuid.uuid4()), cid, issue_id, note,
|
||||
)
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
logger.info("Chair set issue %s → %s", issue_id, status)
|
||||
return {
|
||||
"ok": True,
|
||||
"id": str(row["id"]),
|
||||
"identifier": row["identifier"],
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
async def respond_to_interaction(
|
||||
issue_id: str, interaction_id: str, payload: dict,
|
||||
) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user