diff --git a/.claude/agents/hermes-curator.md b/.claude/agents/hermes-curator.md
index c53afb2..f0074ae 100644
--- a/.claude/agents/hermes-curator.md
+++ b/.claude/agents/hermes-curator.md
@@ -15,7 +15,7 @@ hermes-curator.md — מקור-האמת היחיד לפרומפט של סוכן
adapter: deepseek_local · model: deepseek-v4-pro
profiles: CMP=curator-cmp (רישוי 1xxx) · CMPA=curator-cmpa (היטל 8xxx + פיצויים 9xxx)
role: Knowledge Curator — מנתח החלטות סופיות אחרי export, מציע עדכוני skills/lessons.
- read-only על תוכן; write רק על comments / interactions (G10).
+ read-only על תוכן; כותב comments / interactions + ממצאים מוצעים (decision_lessons, שער-יו"ר G10).
placeholders זמינים: {{agentId}} {{agentName}} {{companyId}} {{companyName}} {{runId}}
{{taskId}} {{taskTitle}} {{taskBody}} {{commentId}} {{wakeReason}} {{projectName}} {{paperclipApiUrl}}
@@ -129,6 +129,24 @@ curl -sS -X POST \
- אם תוצאה רלוונטית להמחשת דפוס מסוים — קח אותה **מ-`case_get` שדה `expected_outcome`**, **לא מקריאת הטקסט**. אם השדה ריק או חסר ב-DB — סמן `[תוצאה: לא מאומתת]` או דלג עליה.
- אל תפרש משפטית את ההחלטה. דפנה כבר הכריעה. תפקידך זיהוי דפוסים בלבד.
+## 5b. שמור את הממצאים מבנית (חובה — INV-LRN3)
+ה-comment הוא ארעי ולא-נסקר. כדי שהממצאים ייתפסו, יופיעו בטאב ״אוצֵר״ ב-/training, ויעברו
+שער-יו"ר — קרא לכלי ה-MCP **`mcp__legal-ai__record_curator_findings`** עם אותם ממצאים:
+```
+record_curator_findings(
+ case_number="<מספר התיק מ-taskTitle>",
+ findings=[
+ {"text": "<ניסוח הממצא — אותו טקסט כמו ב-comment, בלי התג>", "category": "style"},
+ {"text": "...", "category": "structure"},
+ ...
+ ]
+)
+```
+מיפוי תג→category: `[סגנון]`→`style` · `[מבנה]`→`structure` · `[לקסיקון משפטי]`→`lexicon` · `[טבלאי]`→`tabular`.
+הכלי כותב כל ממצא כ-`decision_lesson` (`source='curator'`, `review_status='proposed'`) ומדלג על כפילויות.
+אם הוא מחזיר שגיאת "לא נמצאה רשומת style_corpus" — הסופי טרם נקלט לקורפוס-הסגנון; ציין זאת ב-comment והמשך (אל תיכשל).
+**זו הצעה הממתינה לאישור דפנה — לא שינוי-קול. אתה עדיין read-only על התוכן ולא נוגע ב-skills/קבצים.**
+
## 6. בחר interaction (חובה — רוב המקרים יש)
לפי הקונטקסט בחר **אחד** מ-3 הסוגים. אם **אין שום החלטה אנושית נדרשת** — דלג ישירות ל-§A.7.
@@ -261,7 +279,7 @@ curl -sS -X PATCH \
# כללים כלליים
- **idempotencyKey**: חובה ב-interaction. אם נעור פעמיים על אותו תיק — Paperclip לא יוצר כפילות.
-- **לא לעדכן** קבצים (skills/, lessons.py, DB) בעצמך. רק לכתוב comments / interactions.
+- **לא לעדכן** קבצים (skills/, lessons.py) או שכבת-הקול בעצמך. הפלט שלך: comments + interactions + רישום ממצאים כ-`decision_lesson` *מוצע* דרך `record_curator_findings` (שער-יו"ר, INV-LRN3). אתה לא מאשר ולא מטמיע — רק מציע.
- **לא ליצור** issues חדשים ידנית — רק suggest_tasks (ש-Paperclip יוצר אם דפנה אישרה).
- **לא להעיר** סוכנים אחרים.
- **בעיה?** אם MCP נכשל או מסמך חסר — comment קצר עם הסיבה + סגור (status=done). אל תזייף.
diff --git a/docs/spec/07-learning.md b/docs/spec/07-learning.md
index 46e16a2..4f330a2 100644
--- a/docs/spec/07-learning.md
+++ b/docs/spec/07-learning.md
@@ -96,10 +96,24 @@
(`hermes-curator.md:60-70`).
- מזהה **3–5 דפוסים/פערים** חדשים, כל ממצא מתויג `[סגנון]` / `[מבנה]` /
`[לקסיקון משפטי]` / `[טבלאי]` (`hermes-curator.md:99-108`).
-- **מציע** — comment ב-Paperclip + רישום כל ממצא כ-`decision_lesson` דרך
- `POST /api/training/corpus/{corpus_id}/lessons` (`source:"curator"`) שמופיע ב-UI
- תחת הטאב "מה למדנו" (`hermes-curator.md:73-96`).
-- **אינו מעדכן** קבצים בעצמו (skills/, lessons.py, DB) — רק מציע (`hermes-curator.md:125-130`).
+- **מציע** — comment + interaction בערוץ-הפלטפורמה, וגם **רושם כל ממצא מבנית** כ-`decision_lesson`
+ (`source='curator'`, `review_status='proposed'`) דרך כלי-ה-MCP `record_curator_findings`
+ (`hermes-curator.md` §A.5b → `tools/workflow.py::record_curator_findings` → `db.add_decision_lesson`).
+ הממצאים מופיעים ב-`/training` טאב "אוצֵר" (ערוץ ג׳) ועוברים את שער-היו"ר (INV-LRN1).
+ > **דריפט-מימוש שנסגר (2026-06-28):** עד גל זה הסוכן כתב comment בלבד והספ תיאר endpoint שלא נקרא —
+ > הממצאים האיכותיים אבדו, וכרטיס-האוצֵר (`get_curator_stats`) ספר `source='curator'` שאיש לא כתב → 0.
+ > `record_curator_findings` מממש את INV-LRN3 בפועל.
+- **אינו מאשר ואינו מטמיע** — read-only על התוכן; רישום-הממצא הוא *הצעה* מגודרת-שער, לא שינוי-קול.
+ אינו עורך `skills/` / `lessons.py` / שכבת-הקול בעצמו (`hermes-curator.md` §"כללים כלליים").
+- **שני ערוצי-למידה נפרדים על אותו סופי:** ערוץ-האוצֵר (כאן, qualitative, `source='curator'`) ו**פאנל-הסגנון**
+ הדו-סוכני שהצינור מפעיל (§0.6 — DeepSeek+Gemini, `source='panel:deepseek+gemini'`). שניהם
+ `decision_lessons` ממתיני-שער; ערוץ הדיסטילציה (`appeal_type_rules`, §0.4) הוא השלישי.
+
+> **פער-מימוש פתוח (TaskMaster #157):** ה-`PIPELINE-WAKE BRANCH` (`hermes-curator.md:24-43`) מפעיל את
+> הצינור על `final_learning_*` ו**מסיים מיד** — ניתוח-§A האיכותי (וממילא `record_curator_findings`)
+> **אינו רץ אוטומטית על mark-final**, אלא רק ביקיצת-תגובה/ידנית. כדי שממצאי-האוצֵר ייתפסו על כל סופי
+> יש להכריע: שהסוכן ימשיך ל-§A אחרי הפעלת-הצינור (לטפל במירוץ enroll_style_corpus), או שהצינור עצמו
+> יפלוט `source='curator'`. נדחה לתכנון נפרד.
### 1.2 לולאת-פידבק-היו"ר (capture → ניתוח שבועי → לקחים)
diff --git a/mcp-server/src/legal_mcp/server.py b/mcp-server/src/legal_mcp/server.py
index 3b6e936..99fa0f8 100644
--- a/mcp-server/src/legal_mcp/server.py
+++ b/mcp-server/src/legal_mcp/server.py
@@ -1176,6 +1176,15 @@ async def list_chair_feedback(
return await workflow.list_chair_feedback(case_number, category, unresolved_only, _clamp_limit(limit))
+@mcp.tool()
+async def record_curator_findings(case_number: str, findings: list[dict]) -> str:
+ """לכידת ממצאי-האוצֵר על החלטה סופית כ-decision_lessons מובְנים (source='curator',
+ ממתינים לשער-יו"ר) — INV-LRN3. כל ממצא: {"text": "...", "category": style/structure/
+ lexicon/tabular} (או "tag" עברי). מחזיר כמה נרשמו וכמה כפילויות דולגו. read-only על
+ התוכן — הרישום הצעה הממתינה לאישור דפנה ב-/training, לא שינוי-קול ישיר (G10)."""
+ return await workflow.record_curator_findings(case_number, findings)
+
+
@mcp.tool()
async def halacha_corroboration(halacha_id: str) -> dict:
"""החזר את ה-corroboration של הלכה: הציטוטים שמתקפים אותה, הטיפול, וסיכום (X11, read-only)."""
diff --git a/mcp-server/src/legal_mcp/services/db.py b/mcp-server/src/legal_mcp/services/db.py
index 34ae92f..aa9afc5 100644
--- a/mcp-server/src/legal_mcp/services/db.py
+++ b/mcp-server/src/legal_mcp/services/db.py
@@ -2675,6 +2675,22 @@ async def add_decision_lesson(
return dict(row) if row else {}
+async def get_style_corpus_id_by_decision(decision_number: str) -> UUID | None:
+ """Resolve the style_corpus row id for a final decision by its number.
+
+ The curator knows the case_number; lessons attach to the corpus row the
+ learning pipeline enrolled (enroll_style_corpus). Returns None if the final
+ has not been enrolled yet (caller surfaces it — no silent attach).
+ """
+ pool = await get_pool()
+ async with pool.acquire() as conn:
+ row = await conn.fetchrow(
+ "SELECT id FROM style_corpus WHERE decision_number = $1 LIMIT 1",
+ decision_number,
+ )
+ return row["id"] if row else None
+
+
async def update_decision_lesson(
lesson_id: UUID,
*,
@@ -2950,6 +2966,12 @@ async def get_recent_decision_lessons(limit: int = 15, practice_area: str = "")
Gate (INV-LRN1/G10): returns only CHAIR-APPROVED lessons (review_status='approved').
Panel-written proposals stay out of the writer's context until the chair approves
them in /training — the chair's review is the gate, not the panel's 2/2 vote.
+
+ No silent cap (חוקה §6): when more approved lessons match than `limit`, the
+ overflow is dropped by ORDER BY created_at DESC — log it so coverage loss is
+ observable, not invisible. The real fix is lesson synthesis (TaskMaster #158):
+ consolidate overlapping lessons into fewer, richer ones so the set fits without
+ truncation. Until then this WARN is the audit trail.
"""
pool = await get_pool()
async with pool.acquire() as conn:
@@ -2964,6 +2986,20 @@ async def get_recent_decision_lessons(limit: int = 15, practice_area: str = "")
LIMIT $1""",
limit, practice_area,
)
+ if len(rows) >= limit:
+ total = await conn.fetchval(
+ """SELECT count(*) FROM decision_lessons dl
+ JOIN style_corpus sc ON sc.id = dl.style_corpus_id
+ WHERE dl.review_status = 'approved'
+ AND ($1 = '' OR sc.practice_area = $1)""",
+ practice_area,
+ ) or 0
+ if total > limit:
+ logger.warning(
+ "get_recent_decision_lessons: capped %d→%d approved lessons "
+ "(practice_area=%r) — %d not reaching the writer. See TaskMaster #158 (synthesis).",
+ total, limit, practice_area or "*", total - limit,
+ )
return [dict(r) for r in rows]
diff --git a/mcp-server/src/legal_mcp/tools/workflow.py b/mcp-server/src/legal_mcp/tools/workflow.py
index 589c28c..4469b5d 100644
--- a/mcp-server/src/legal_mcp/tools/workflow.py
+++ b/mcp-server/src/legal_mcp/tools/workflow.py
@@ -403,6 +403,78 @@ async def record_chair_feedback(
}, message=f"הערה נרשמה בהצלחה. קטגוריה: {category}.")
+_CURATOR_FINDING_CATEGORIES = {"style", "structure", "lexicon", "tabular", "general"}
+# Agent tags ([סגנון]/[מבנה]/[לקסיקון משפטי]/[טבלאי]) → decision_lessons.category
+_CURATOR_TAG_TO_CATEGORY = {
+ "סגנון": "style", "מבנה": "structure",
+ "לקסיקון משפטי": "lexicon", "לקסיקון": "lexicon", "טבלאי": "tabular",
+}
+
+
+async def record_curator_findings(case_number: str, findings: list[dict]) -> str:
+ """לכידת ממצאי-האוצֵר כ-decision_lessons מובְנים (source='curator', proposed) — INV-LRN3.
+
+ האוצֵר מזהה דפוסי-סגנון בקריאת הסופי; עד כה הם חיו רק כהערה ארעית בערוץ-התגובות
+ (אובד, לא נסקר). כאן הם נתפסים מבנית כך שיופיעו בטאב ״אוצֵר״ ויעברו שער-יו"ר (INV-LRN1/G10).
+ האוצֵר נשאר read-only על התוכן — הרישום הוא הצעה הממתינה לאישור, לא שינוי-קול.
+
+ Args:
+ case_number: מספר התיק הסופי (= decision_number בקורפוס-הסגנון).
+ findings: רשימת ממצאים, כל אחד {"text": "...", "category"/"tag": "..."}.
+ category ∈ style/structure/lexicon/tabular/general (או tag עברי).
+ """
+ if not findings:
+ return err("findings ריק — אין ממצאים לרשום.")
+ corpus_id = await db.get_style_corpus_id_by_decision(case_number)
+ if not corpus_id:
+ return err(
+ f"לא נמצאה רשומת style_corpus ל-{case_number} — ודא שהסופי נקלט לקורפוס-הסגנון "
+ "(enroll_style_corpus) לפני רישום ממצאים."
+ )
+ # Dedup against lessons already on this corpus (any source) — re-running §A
+ # must not pile duplicates (INV-LRN3 reliability).
+ existing = {(_norm(r["lesson_text"])) for r in await db.list_decision_lessons(corpus_id)}
+ written, skipped_dup, skipped_empty = [], 0, 0
+ for f in findings:
+ text = (f.get("text") or "").strip()
+ if not text:
+ skipped_empty += 1
+ continue
+ if _norm(text) in existing:
+ skipped_dup += 1
+ continue
+ raw_cat = (f.get("category") or f.get("tag") or "general").strip()
+ category = _CURATOR_TAG_TO_CATEGORY.get(raw_cat, raw_cat)
+ if category not in _CURATOR_FINDING_CATEGORIES:
+ category = "general"
+ row = await db.add_decision_lesson(
+ corpus_id,
+ lesson_text=text,
+ category=category,
+ source="curator",
+ created_by="curator",
+ review_status="proposed",
+ )
+ if row:
+ written.append(str(row["id"]))
+ existing.add(_norm(text))
+ return ok({
+ "corpus_id": str(corpus_id),
+ "written": len(written),
+ "skipped_duplicate": skipped_dup,
+ "skipped_empty": skipped_empty,
+ "lesson_ids": written,
+ }, message=(
+ f"נרשמו {len(written)} ממצאי-אוצֵר (source=curator, ממתינים לשער-יו\"ר ב-/training). "
+ f"{skipped_dup} כפילויות דולגו."
+ ))
+
+
+def _norm(s: str) -> str:
+ """Normalize lesson text for dedup — collapse whitespace, strip."""
+ return " ".join((s or "").split())
+
+
async def list_chair_feedback(
case_number: str = "",
category: str = "",
diff --git a/web-ui/src/components/training/curator-portrait-panel.tsx b/web-ui/src/components/training/curator-portrait-panel.tsx
index c3f1c9c..512b6b6 100644
--- a/web-ui/src/components/training/curator-portrait-panel.tsx
+++ b/web-ui/src/components/training/curator-portrait-panel.tsx
@@ -45,6 +45,15 @@ export function CuratorPortraitPanel() {
{store}
+ + הלמידה זורמת לכותב בשלושה ערוצים נפרדים, כל אחד + מופעל כשדפנה מסמנת החלטה כסופית. הכרטיס מראה כמה כל ערוץ תרם ומה כבר עובר בפועל לכותב — + רק ידע שאושר בשער-היו״ר משפיע על הטיוטות (INV-LRN1). +
+{value}
+{value}
); @@ -123,10 +221,10 @@ function RecentFindings() { if (!data || data.recent_findings.length === 0) { return (decision_lessons (source="curator")
+ הממתינים לאישורה — בנפרד מלקחי-הפאנל ומכללי-המתודולוגיה שלמעלה.