feat(learning): curator §A fires on mark-final — capture source='curator' findings on every final (#159)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

חלופה ז (agent-centric) — סוגר את פער-המימוש מ-#157:

1. final_learning_pipeline.py — סדר-מחדש: enroll_style_corpus רץ ראשון (לפני הדיסטילציה של עד
   30 דק'), כך שרשומת style_corpus קיימת תוך שניות — והאוצֵר יכול לרשום ממצאים בלי מירוץ.
   enroll/ingest בלתי-תלויים; panel נשאר אחרון. תוויות [1/3]/[2/3] עודכנו.

2. hermes-curator.md PIPELINE-WAKE BRANCH — מבדיל final_learning_* (ממשיך ל-§A, מצב AUTO) מ-
   final_halacha_* (exit כקודם). ב-AUTO: §A.1–§A.5b → record_curator_findings, דילוג על §A.6
   (interaction) כדי לא להעיר את דפנה (הממצאים proposed ונסקרים ב-/training). §A.5b קיבל retry
   כרשת-ביטחון למירוץ-שארית.

3. docs/spec/07-learning.md §0.6 + §1.1 — עודכנו לסדר-הצינור החדש; הערת "פער-מימוש פתוח" → "נסגר".

Invariants: G2 (מחולל-יחיד לממצאי-curator — הסוכן; אין כפילות-תבונה), INV-LRN1/G10 (proposed,
שער-יו"ר), INV-LRN3, INV-DUR1 (סדר-צעדים בשמות יציבים; checkpoint resume — edge-case של ריצה
מקבילה-לפריסה, סיכון נמוך). depends-on #157.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 21:56:36 +00:00
parent 07bd8bee48
commit 4d6268820c
3 changed files with 34 additions and 17 deletions

View File

@@ -91,15 +91,22 @@ async def main(args: argparse.Namespace) -> int:
# The 3 steps as durable nodes (X16 / INV-DUR1) — shared runtime with
# final_halacha (scripts/_pipeline_runtime.py). A crash/OOM in the long style
# panel [3] resumes from [3] instead of re-paying the Opus distillation [1].
# panel [3] resumes from [3] instead of re-paying the Opus distillation [2].
#
# Order (#159): enroll FIRST. enroll creates the style_corpus row (fast, no LLM)
# that decision_lessons attach to. Running it before the up-to-30-min distillation
# means the corpus exists within seconds — so the curator agent's §A (which fires
# in parallel on a final_learning_* wake) can record source='curator' findings
# without racing the 30-min step. enroll/ingest are mutually independent; panel
# needs both, so it stays last.
async def step_ingest(results: dict) -> dict:
# [1] distillation (Opus) — skip if already analyzed (idempotent; --force to redo).
# [2] distillation (Opus) — skip if already analyzed (idempotent; --force to redo).
status = await _latest_pair_status(case["id"])
if status == "analyzed" and not args.force:
print("[1/3] ingest_final_version — דולג (הזוג כבר analyzed; --force לחידוש)")
print("[2/3] ingest_final_version — דולג (הזוג כבר analyzed; --force לחידוש)")
return {"ingest": "skipped:analyzed"}
print("[1/3] ingest_final_version — דיסטילציית טיוטה↔סופי…", flush=True)
print("[2/3] ingest_final_version — דיסטילציית טיוטה↔סופי…", flush=True)
raw = await ingest_final_version(case_number, file_path=final_path)
try:
env = json.loads(raw)
@@ -120,8 +127,9 @@ async def main(args: argparse.Namespace) -> int:
return {"ingest": "done"}
async def step_enroll(results: dict) -> dict:
# [2] enroll into style_corpus (idempotent) — lessons need a corpus_id.
print("[2/3] רישום לקורפוס-הסגנון (idempotent)…", flush=True)
# [1] enroll into style_corpus (idempotent) — lessons need a corpus_id, and
# it must exist early (before the long distillation) so curator §A can attach.
print("[1/3] רישום לקורפוס-הסגנון (idempotent)…", flush=True)
if await _has_style_corpus(case_number):
print(" ✓ כבר רשום בקורפוס-הסגנון")
return {"enroll": "exists"}
@@ -150,8 +158,8 @@ async def main(args: argparse.Namespace) -> int:
return {"panel_rc": rc or 0}
steps = [
_pipeline_runtime.Step("ingest_final_version", step_ingest),
_pipeline_runtime.Step("enroll_style_corpus", step_enroll),
_pipeline_runtime.Step("ingest_final_version", step_ingest),
_pipeline_runtime.Step("style_panel", step_panel),
]
checkpoint_db = config.DATA_DIR / "checkpoints" / "learning.sqlite"