Commit Graph

978 Commits

Author SHA1 Message Date
07ecb6a366 feat(halacha): עצירה-רכה של הדריינר בסף-ניצול (75/65) + מקור-אמת יחיד למכסה (#265)
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
G12 Leak-Guard / leak-guard (push) Successful in 5s
Lint — undefined names / undefined-names (push) Successful in 11s
Co-authored-by: Chaim <chaim@marcus-law.co.il>
Co-committed-by: Chaim <chaim@marcus-law.co.il>
2026-06-15 04:11:43 +00:00
ce86821393 Merge pull request 'fix(extraction): reconcile לתיקים-יתומים בתור — pending+requested_at=NULL (#139)' (#264) from worktree-extraction-orphan-reconcile into main
Some checks failed
G12 Leak-Guard / leak-guard (push) Has been cancelled
Lint — undefined names / undefined-names (push) Has been cancelled
Build & Deploy / build-and-deploy (push) Has been cancelled
2026-06-15 04:11:19 +00:00
2343892220 fix(extraction): reconcile לתיקים-יתומים בתור — pending+requested_at=NULL (#139)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
תיק יכול להיות <kind>_extraction_status='pending' עם _requested_at=NULL —
מעולם-לא-נכנס-לתור (מסלולי bulk/מיגרציה, או status שנכתב לפני החותם), והדריינר
(סורק requested_at IS NOT NULL) עיוור אליו לנצח → ה-backlog מתנקז בשקט לאפס.
requeue_stale_processing_extractions מרפא רק 'processing'. נצפה 2026-06-14:
96 תיקים pending אך 0 בתור (תוקנו ידנית).

תיקון (G1 — שחזור invariant במקור, G2 — predicate יחיד):
- db.reconcile_orphaned_pending_extractions(kind=) — kind-agnostic, מחזיר את
  invariant "שורה ברת-חילוץ ⇒ בתור": חותם requested_at ל-rows שהם pending +
  requested_at IS NULL + EXTRACTION_ELIGIBLE_PREDICATE (אותו מסנן של #140 —
  cited_only/chunkless לעולם לא נדחפים). אידמפוטנטי (rows מסומנים לא נתפסים).
- precedent_library.process_pending_extractions קורא reconcile אחרי requeue_stale
  ולפני list — תיקים-משוחזרים נקלטים באותו pass. מנגנון-ריפוי יחיד (G2), לא מסלול
  מקביל; requeue_stale='processing', reconcile='pending'.
- request_halacha_extraction מציב status='pending' עם החותם (סימטרי ל-metadata)
  — סוגר את חלון-ה-drift שמייצר pending+NULL מלכתחילה.

מצב חי נקי (0 יתומים-כשירים אחרי התיקון-הידני); זהו תיקון מונע — הדריינר יְרַפֵּא
יתומים עתידיים אוטומטית.

בדיקות: test_extraction_orphan_reconcile (predicate משותף, pending+NULL בלבד,
מובחן מ-requeue_stale, request_halacha סימטרי), שני ה-kinds. כל 349 עוברות.

Invariants: G1, G2 (predicate משותף עם #140, ריפוי יחיד), INV-G3/INV-DUR1 (X16),
INV-G4 (אין בליעה שקטה — reconcile מתעד), G12.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 04:09:54 +00:00
ea232da92d Merge pull request 'fix(extraction): סינון cited_only מתור/מוני החילוץ (#140)' (#263) from worktree-cited-only-skip-queue into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m40s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 12s
2026-06-15 04:04:33 +00:00
c348903e4b fix(extraction): סינון cited_only מתור/מוני החילוץ (#140)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 7s
Lint — undefined names / undefined-names (pull_request) Successful in 14s
31 שורות case_law עם source_kind='cited_only' (ציטוט-בלבד, ללא full_text/chunks)
נושאות halacha_extraction_status='pending' רק כברירת-מחדל ומזהמות את מונה ה-pending
ובמתזמר/בדף-התפעול — אין להן מה לחלץ.

תיקון (G1 — תיקון-במקור, G2 — מסנן יחיד משותף):
- db.EXTRACTION_ELIGIBLE_PREDICATE — מקור-אמת יחיד ל"שורה ברת-חילוץ" (source_kind
  <> 'cited_only' AND יש precedent_chunks). מוחל ב-list_pending_extraction_requests;
  #139 יעשה בו שימוש-חוזר ל-reconcile (אותו כלל, לא כפול).
- מוני-snapshot מסננים cited_only: halacha_drain_supervisor.db_snapshot,
  web/app.py meta+hal_ext (GROUP BY status).
- reconcile_metadata_status.py מורחב לכסות גם את תור-ההלכות: cited_only→'skipped'
  (אותו terminal-state כמו צד-המטא, תור-תאום, G2). בוצע על ה-DB החי: 31 הועברו
  ל-'skipped' (metadata כבר היה מיושב — אידמפוטנטי). התפלגות-אחרי: halacha
  pending=9 (עבודה אמיתית), skipped=31, completed=309.

בדיקות: test_extraction_queue_eligibility (predicate + list_pending מחיל אותו,
שני ה-kinds). כל 345 בדיקות mcp עוברות. guards נקיים.

Invariants: G1 (terminal-state אמיתי במקור), G2 (predicate יחיד, ללא תור מקביל),
INV-DM1 (stub לא-searchable אינו מועמד-חילוץ), G12 (leak-guard נקי).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 04:03:21 +00:00
7043de0ac2 Merge pull request 'feat(precedents): citation_formatted דטרמיניסטי בקוד — Gemini מחלץ רכיבים, לא מעצב (#145)' (#262) from worktree-precedent-deterministic-citation into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m47s
G12 Leak-Guard / leak-guard (push) Successful in 8s
Lint — undefined names / undefined-names (push) Successful in 22s
2026-06-15 03:38:51 +00:00
d6608ce849 feat(precedents): citation_formatted דטרמיניסטי בקוד — Gemini מחלץ רכיבים, לא מעצב (#145)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
Lint — undefined names / undefined-names (pull_request) Successful in 11s
הבעיה (#145): מחלץ-המטא ביקש מ-Gemini Flash *לעצב* את מראה-המקום המלא
(citation_formatted). ב-JSON-mode חופשי (ללא responseSchema) המודל החזיר JSON
תקין ומלא אך **השמיט בעקביות** דווקא את השדה הזה — אומת על 8070-05-25,
1194-12-25, 1200-12-25 (וגם כשהצדדים זוהו). השדה הקשה ביותר (עיצוב מחרוזת) +
היתר-בפרומפט להשאיר ריק → Flash מפיל אותו.

הפתרון: citation_formatted הוא **שדה-תצוגה נגזר** (X1 §3 / INV-ID2) — מורכב
דטרמיניסטית מרכיבים מובְנים, לא מעוצב ע"י LLM. תפקיד ה-LLM מצטמצם לחילוץ
רכיבים אמינים (שורת-הצדדים, קידומת-ההליך לפסקי-בית-משפט).

- db.format_precedent_citation(record) — מרכיב לפי כללי-הציטוט-האחיד:
  ועדת-ערר (מחוזית/ארצית/בל"מ) מ-proceeding_type+district+source_kind;
  פסקי-בית-משפט מ-court_prefix(LLM)+district-abbrev. מוציא docket נקי
  מ-case_number מזוהם ("עע\"מ 683/13"→"683/13"). נמנע ('') כשחסר רכיב
  (צדדים/docket/תאריך/קידומת) — abstention על המצאה (INV-AH).
- case_law.parties (V39) — שורת "עורר נ' משיב" כבסיס re-derivable.
- מחלץ-המטא: הפרומפט מחלץ parties+citation_prefix (לא citation_formatted);
  apply_to_record מרכיב דטרמיניסטית מהרשומה-האפקטיבית וממלא רק שדה ריק
  (עריכות-יו"ר נשמרות).
- scripts/backfill_precedent_citations.py — backfill 2-מעברים (דטרמיניסטי→LLM),
  מדווח שורות-נמנעות, idempotent.

אומת: 3 הרשומות הידניות משוחזרות תו-בתו; פסק עליון אמיתי מולא end-to-end
(עע"מ 683/13 ... נבו 3.9.2015). test_fu2b_reconcile ✓.

Invariants: INV-ID2/X1§3 (ציטוט=תצוגה נגזר, לא מפתח) · INV-AH (abstention,
אפס המצאה) · G1 (docket נקי) · G2 (מסלול-יחיד — מחליף את נתיב-ה-LLM, לא מקביל).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 03:37:53 +00:00
33b07eebcf Merge pull request 'fix(metadata): לא להתיישב 'completed' בכשל-חילוץ-Gemini חולף (#138)' (#261) from worktree-metadata-no-settle-on-fail into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m42s
G12 Leak-Guard / leak-guard (push) Successful in 6s
Lint — undefined names / undefined-names (push) Successful in 11s
2026-06-15 03:35:47 +00:00
77817a46ad fix(metadata): לא להתיישב 'completed' בכשל-חילוץ-Gemini חולף (#138)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 7s
Lint — undefined names / undefined-names (pull_request) Successful in 17s
צד-המטא-דאטה (precedent_metadata_extractor) קרא רק GEMINI_API_KEY בעוד בסביבה
קיים GOOGLE_GEMINI_API_KEY — תוקן ב-PR #255 (fallback). הבאג המשני שנותר: כש-
extract_and_apply החזיר 'no_metadata' (כשל-Gemini), מסלול-הדריינר
process_pending_extractions התיישב ל-metadata_status='completed' ללא-תנאי, כך
שהרשומה ננטשה בשקט עם מטא ריק והדריינר לא חזר אליה (נצפה: da2d9ccb '4491-02-21',
5fabdac5 '14306-09-23' — completed אך court/date/summary ריקים).

תיקון (G1 — אבחנת-מקור):
- extract_and_apply מבדיל תוצאה-ריקה: יש full_text → 'extraction_failed' (חולף,
  בר-retry); אין full_text → 'no_metadata' (אין מה לחלץ).
- process_pending_extractions (metadata): 'extraction_failed' → חוזר ל-'pending'
  (משמר את חותם-התור) במקום להתיישב 'completed'. retry-loop הקיים מנסה שוב,
  ואחרי-מיצוי הרשומה נשארת בתור. מסלול reextract_metadata כבר עקבי (חוזר pending
  על כל מה שאינו completed/no_changes).

תיקון-נתון (בוצע ידנית דרך כלי-MCP precedent_extract_metadata): da2d9ccb +
5fabdac5 חולצו-מחדש בהצלחה (court/date/summary/headnote/tags מלאים). 0 נותרו
external 'completed-but-empty'.

הערה: מפתח-Gemini אינו נדרש ב-Coolify — המחלץ רץ רק מקומית (precedent_library →
extract_and_apply, host ~/.env עם GOOGLE_GEMINI_API_KEY); app.py מייבא רק את
הקבוע PLACEHOLDER_PENDING_EXTRACTION, לא את פונקציית-החילוץ.

בדיקות: test_metadata_extract_failure_status (transient/permanent/missing). כל
335 בדיקות mcp עוברות. guards נקיים.

Invariants: G1 (אבחנת-מקור, לא התיישבות-בקריאה), INV-G3/X16 (עמידות — בר-retry),
G12 (leak-guard נקי).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 03:34:43 +00:00
e552d831bd Merge pull request 'fix(precedents): חילוץ מספר-תיק קנוני מהציטוט — לא ציטוט-מלא כמזהה (#137)' (#260) from worktree-case-number-from-citation into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 2m0s
G12 Leak-Guard / leak-guard (push) Successful in 7s
Lint — undefined names / undefined-names (push) Successful in 18s
2026-06-15 03:22:18 +00:00
c27987ba72 fix(precedents): חילוץ מספר-תיק קנוני מהציטוט — לא ציטוט-מלא כמזהה (#137)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 14s
בהעלאה דרך "פסיקה-חסרה" (ענף ועדת-ערר), כשטופס case_number ריק המסלול נפל-לאחור
לציטוט המלא (committee_case_number = case_number.strip() or citation), כך שמחרוזת-
תצוגה עם שמות-צדדים הושתלה בשדה-המזהה — הפרת INV-ID2/INV-ID1 (X1). נצפה על
precedent 1bf0bae0 (ערר 85074-04-25 רפאל לוי/חולון): case_number=85074/0425,
case_name=ציטוט שלם.

תיקון (G1 — נרמול-במקור, G2 — שימוש-חוזר בפרסר הקנוני):
- court_citation.case_number_from_citation(citation) — מחזיר את אסימון-המספר
  המנורמל בלבד (classify; '' כשאין מספר). חולץ נכון 85074-04-25 גם מתוך
  "ערר (ת\"א 85074-04-25) ...". reuse של הפרסר היחיד, בלי regex מקביל.
- web/app.py (ענף ועדת-ערר): fallback דרך case_number_from_citation; אם אין
  מספר — HTTPException 400 "נא להזין מספר-תיק ידנית" במקום השתלת ציטוט-מלא.
- db._canonical_case_number: מוקשח לחלץ את אסימון-המספר (זורק זנב שמות-צדדים),
  כך ששדה-המזהה לעולם לא נשמר מזוהם — גם בקריאה ישירה (committee + active cases).
  מספר נקי חוזר ללא שינוי; חודש לא מומצא (X1 §1).
- תיקון-נתון: scripts/fix_137_committee_case_number.py (בוצע) — 1bf0bae0:
  case_number→85074-04-25, case_name→צדדים, token ב-citation_formatted.
  אומת היחיד עם canon(num)≠num ב-internal_committee. אידמפוטנטי.

מחוץ-לתחום (תועד כ-follow-up): מסלול external (precedent_library) משתמש בציטוט-
מלא כמזהה-מורשת — זהו פריט-המיגרציה X1 §5 (138 רשומות external/cited_only),
לא הבאג הזה. prefill ב-UI של /missing-precedents — דורש שער Claude Design.

בדיקות: test_court_citation (case_number_from_citation: party-strip/forms/empty),
test_canonical_case_number (harden). כל 339 בדיקות mcp עוברות. guards נקיים.

Invariants: G1 (נרמול-במקור), INV-ID1/ID2 (מזהה מנורמל, אין ציטוט-מלא כמזהה),
G2 (פרסר יחיד), G12 (leak-guard נקי).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 03:21:10 +00:00
1094ac9967 feat(halacha): ספי-עצירה-רכים לדריינר — 5-שעות 75% / שבועי 65% (עצירה לפני 429) (#259)
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 9s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 10s
Co-authored-by: Chaim <chaim@marcus-law.co.il>
Co-committed-by: Chaim <chaim@marcus-law.co.il>
2026-06-15 03:18:56 +00:00
76a29756c5 Merge pull request 'fix(halacha): שחזור-עצמי לנעילת-advisory דלופה — לא לחסום חילוץ-הלכות (#142)' (#258) from worktree-halacha-lock-leak into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m34s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 11s
2026-06-15 02:57:45 +00:00
eb86e475c3 fix(halacha): import asyncpg — undefined-name guard (annotation ב-_acquire_global_lock)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
Lint — undefined names / undefined-names (pull_request) Successful in 14s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 02:56:49 +00:00
b7ffc0387c fix(halacha): שחזור-עצמי לנעילת-advisory דלופה — לא לחסום חילוץ-הלכות (#142)
Some checks failed
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
Lint — undefined names / undefined-names (pull_request) Failing after 13s
כשה-legal-halacha-drain קרס עם "RuntimeError: Event loop is closed", ה-finally
שמריץ pg_advisory_unlock + pool.release לא רץ, וחיבור-הנעילה הייעודי נשאר חי,
idle, מחזיק את הנעילה הגלובלית — כל extract עתידי החזיר status='busy' לצמיתות
עד pg_terminate_backend ידני (~4.5 דק', CMP-174, 2026-06-14).

תיקון (G1 — נרמול-במקור, G2 — אותה נעילה, בלי מסלול מקביל):
- KEEPALIVE: משימת-רקע נוגעת בחיבור-הנעילה כל 30ש' → state_change נשאר טרי.
  חילוץ חי לעולם לא נראה "תקוע"; קריסה מקפיאה את ה-keepalive ואת state_change.
- שחזור-עצמי בכניסה (_acquire_global_lock): כש-pg_try_advisory_lock נכשל, בודקים
  את ה-holder; רק backend idle עם state_change ישן מ-_LOCK_STALE_AFTER (150ש',
  5× keepalive) הוא orphan דלוף → pg_terminate_backend ואז acquire מחדש.
  backend 'active' או idle-טרי = חילוץ חי, לעולם לא נהרג (מניעת ה-box-freeze).
- נדחתה אופציית pg_advisory_xact_lock: הייתה כופה transaction פתוח לאורך דקות
  (idle-in-transaction bloat) ועדיין לא משחררת מיידית חיבור-orphan חי.

הערה: השתמשתי במונח DB-סטנדרטי "keepalive" (לא "heartbeat") כי leak_guard מסמן
את "heartbeat" כסמל ספציפי-Paperclip (G12).

בדיקות: tests/test_halacha_lock_selfheal.py (7) — free/live-holder/active-holder/
stale-orphan-reclaim/no-holder/keepalive-stop/extract-busy. כל 332 בדיקות mcp עוברות.

Invariants: G1 (תיקון-במקור), G2 (אותה נעילה), G3/X16 (עמידות-פייפליין), G12 (leak-guard נקי).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 02:54:35 +00:00
d7ef3e7f38 Merge pull request 'fix(halacha): fresh CLI 429 is ground truth over the usage endpoint (rate-limit churn)' (#257) from worktree-halacha-ratelimit-groundtruth into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 18s
G12 Leak-Guard / leak-guard (push) Successful in 3s
Lint — undefined names / undefined-names (push) Successful in 10s
2026-06-15 02:28:51 +00:00
1340bff6f1 fix(halacha): a fresh CLI 429 is ground truth over the usage endpoint (rate-limit)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
PR #251 made the OAuth usage endpoint the PRIMARY rate-limit signal and the log
429 only a fallback for when the endpoint is unreachable. Observed 2026-06-15: the
endpoint reported the window <100% (available) while the claude CLI kept 429-ing
("session limit"). The supervisor then read 'rate_limited=false', classified the
drain 'hung', and restart-churned it — RE-EXTRACTING already-completed precedents
under the rate limit and DEGRADING them (e.g. 4624/21 lost halachot 3→1, only
4/18 chunks). delta_done went negative (completed cases reverting).

Fix: a FRESH CLI 429 is ground truth — the call is literally failing.
  • ENTER cooldown on EITHER signal (endpoint-exhausted OR fresh 429), so a 429
    overrides an endpoint that wrongly reports the window available.
  • VETO the early resume while a fresh 429 remains (the endpoint can lie
    "available" mid-storm → without the veto we'd bounce straight back to churn).
  • DEFAULT_COOLDOWN_MIN=30 when a fresh 429 has no parseable reset time.
While limited the drain STOPS (no 429-hammering, no degrading completed cases) and
re-ignites only once quota is back AND no fresh 429 remains.

Tested: 8 unit-tests over the decision matrix (endpoint×429×stored-cooldown),
incl. the exact tonight case and the veto. py_compile clean.

Immediate mitigation already applied out-of-band: drain stopped + disabled
(drain_controls.disabled) to halt the degradation until this deploys.

Invariants: G1 (fix at source — trust the failing call, not a lagging endpoint),
G2 (same cooldown path, no parallel control). Builds on PR #251.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 02:28:16 +00:00
7f9f502f29 Merge pull request 'fix(precedents): normalize citation→docket case_number + enforce source_type↔precedent_level' (#256) from worktree-precedent-casenum-sourcetype into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m51s
G12 Leak-Guard / leak-guard (push) Successful in 6s
Lint — undefined names / undefined-names (push) Successful in 17s
2026-06-14 20:57:34 +00:00
a05df3eb1a fix(precedents): normalize citation→docket case_number + enforce source_type↔precedent_level
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 12s
שני באגים בקליטת-פסיקה חיצונית (התגלו בתיק 1132-09-24 שהועלה דרך "פסקה חסרה"):

1. case_number קיבל את מחרוזת-הציטוט המלאה במקום דוקט נקי. הסיבה: overwrite_case_number=True
   הועבר רק לנתיב-הפנימי (internal_decisions); נתיב-הדריינר ל-external השאיר את הציטוט שב-
   case_number (precedent_library: case_number=citation). היקף: 122 רשומות external_upload.
2. source_type לא נאכף מול precedent_level — רק ה-prompt ביקש מה-LLM. כשה-LLM פלט
   level=ועדת_ערר_מחוזית אך source_type=court_ruling, ההחלטה סווגה בספרייה כ"פסיקת בית משפט".

תיקון (ב-apply_to_record, כך שכל הנתיבים נהנים):
• case_number מנורמל לדוקט הנקי כש-(א) caller כופה או (ב) הערך הנוכחי ציטוט-צורני (רווח/אורך>20);
  guard _is_clean_docket מבטיח שלעולם לא נכתב ערך לא-דוקט לשדה-הזהות (LLM-זבל נדחה).
• _source_type_for_level גוזר source_type מ-precedent_level ודורס אי-עקביות (ועדת_ערר_*→
  appeals_committee; עליון/מנהלי→court_ruling) — מקור-אמת אחד, לא הישענות על עקביות-LLM.

נבדק: 18 unit-tests (docket-validation, level→type mapping) + 3 integration-tests מול
apply_to_record עם DB מדומה (נרמול, אי-דריסת-דוקט-תקין, דחיית-זבל, אכיפת-עקביות). py_compile נקי.
תיקון-נקודתי כבר בוצע ידנית ל-1132-09-24. Backfill ל-122 בנפרד (TaskMaster #141).

Invariants: G1 (תיקון-במקור), G2 (אותו extractor — בלי מסלול מקביל), INV-AH (מקור-אמת
דטרמיניסטי לסיווג, לא ניחוש-LLM). G11 (זהות-תיק נקייה).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:57:08 +00:00
1a4b4fcf63 Merge pull request 'fix(metadata): accept GOOGLE_GEMINI_API_KEY in gemini_session — host metadata extraction was fully broken' (#255) from worktree-gemini-api-key-name into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 2m25s
G12 Leak-Guard / leak-guard (push) Successful in 9s
Lint — undefined names / undefined-names (push) Successful in 14s
2026-06-14 20:33:45 +00:00
a40c4ee828 fix(metadata): accept GOOGLE_GEMINI_API_KEY (canonical) in gemini_session — host metadata extraction broke
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
_api_key() read ONLY `GEMINI_API_KEY`, but the canonical secret (host ~/.env and
Infisical SoT nautilus:/external-apis/gemini) is `GOOGLE_GEMINI_API_KEY`. The key
was present but under the canonical name → `_api_key()` raised "GEMINI_API_KEY אינו
מוגדר" on every call → ALL host precedent-metadata extraction via Gemini failed
silently (186 such errors in the legal-metadata-drain err log, latest 2026-06-14).

Fix: read GEMINI_API_KEY if set, else fall back to GOOGLE_GEMINI_API_KEY. No new
secret, no duplication — aligns the code to the existing SoT name (G1: fix at
source). Verified live: _api_key() resolves (len=53) and a real gemini query_json
call returns {"ok": true}.

Invariants: G1 (fix at source — code reads the canonical secret name, not a
parallel/duplicated env var) · X10 (deploy-env-secrets: single SoT name honored).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 20:33:15 +00:00
6bc9fa89a2 Merge pull request 'feat(ui): פאנל אישור-תכניות — טאב /precedents + מרכז-אישורים (PR-B)' (#254) from worktree-plans-ui into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 42s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 10s
2026-06-14 15:28:22 +00:00
5d75d36e2a feat(ui): פאנל אישור-תכניות — טאב /precedents + מרכז-אישורים (PR-B)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 9s
הטמעת ה-UI למרשם-התכניות אחרי אישור-עיצוב ב-Claude Design (מוקאפ 22-plans-review).
- web-ui/src/lib/api/plans.ts: hooks (usePlansPending, usePlanDuplicates,
  useUpsertPlan, useUpdatePlan, useReviewPlan, useMergePlans) + טיפוס Plan מקומי.
- plans-review-panel.tsx: כרטיס-תכנית עם משפט-הציטוט הקנוני (כפי שייכתב בבלוק ט),
  שדות-תוקף, סימון חוסר-תאריך, באנר "כפילות אפשרית → מזג לכאן" (find_similar_plans,
  מיזוג ידני — G10), עריכה/הוספה inline עם תצוגה-מקדימה חיה של הציטוט.
- precedents/page.tsx: טאב "תכניות" + PlansPendingPill + deep-link tab=plans.
- web/app.py: href קטגוריית-התכניות במרכז-האישורים → /precedents?tab=plans.
- api:types: types.ts מחודש מ-openapi החי (5 נתיבי /api/plans).

מרכז-האישורים (/approvals) מרנדר קטגוריות גנרית — קטגוריית-התכניות (PR-A) מופיעה
אוטומטית. אימות: api:types ✓, tsc --noEmit ✓, lint exit=0 (ללא אזהרות חדשות).

Invariants: G10 (אישור אנושי + מיזוג ידני) · INV-AH (ציטוט דטרמיניסטי, תצוגה-מקדימה
תואמת format_plan_citation) · INV-IA (שער-אחד: טאב קיים + מרכז-אישורים, ללא עמוד חדש) ·
X6 (UI↔API, apiRequest + טיפוסים). עבר שער-העיצוב Claude Design (feedback_claude_design_gate).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 15:27:36 +00:00
70ac888592 Merge pull request 'feat(plans): הרשאות-סוכנים + dedup/merge/edit + API לתור-אישור תכניות (backend)' (#253) from worktree-plans-followups into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m28s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 9s
2026-06-14 14:40:34 +00:00
46bcaa8fa3 feat(plans): הרשאות-סוכנים + dedup/merge/edit + API לתור-אישור תכניות (backend)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
Follow-ups למרשם-התכניות (PR #252), חלק ה-backend (ללא-UI):
- סוכנים: הוספת extract_plans/plan_get/plan_search/plan_list ל-CEO ול-חוקר
  (+ plan_upsert לחוקר); plan_review נשאר אנושי בלבד (G10). .claude/agents/*.md.
- db: _plan_core_token + find_similar_plans (הצפת כפילות-וריאנט לאישור ידני, בלי
  מיזוג-אוטומטי), update_plan (עריכה+renumber, guard התנגשות→merge), merge_plans
  (איחוד aliases, מילוי-חוסר, סתירות→discrepancies, מחיקת מקור).
- plans_extractor: צירוף possible_duplicates לפלט החילוץ.
- web/app.py: GET /api/plans(+/{id},/{id}/duplicates) · POST /api/plans · PATCH
  /api/plans/{id} · POST /api/plans/{id}/review · POST /api/plans/merge; +קטגוריית
  "תכניות הממתינות לאישור" ב-/api/chair/pending.

תיקון-נתונים (DB, מחוץ ל-PR): הל/מח/250 ד' → 7.1.2002, י"פ 5045.
ה-UI (טאב /precedents + מונה /approvals) ב-PR נפרד אחרי שער Claude Design.

Invariants: G1 (נרמול בכתיבה/עריכה) · G2 · G3 · G10 (review_status + מיזוג ידני,
אישור אנושי) · INV-DM2/DM5 · INV-AH · X9 · אין בליעה שקטה (discrepancies).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:39:46 +00:00
09ea1ee599 Merge pull request 'feat(plans): מרשם-תכניות קנוני (V38) + נוסח-ציטוט אחיד דטרמיניסטי לבלוק ט' (#252) from worktree-plans-registry into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m37s
G12 Leak-Guard / leak-guard (push) Successful in 3s
Lint — undefined names / undefined-names (push) Successful in 10s
2026-06-14 13:47:49 +00:00
4be9cf8543 feat(plans): מרשם-תכניות קנוני (V38) + נוסח-ציטוט אחיד דטרמיניסטי לבלוק ט
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
מוסיף ישות קנונית לתכניות בניין-עיר (תב"ע) שחוזרות בין תיקים — SSOT לזהות+תוקף
(פרסום למתן תוקף ברשומות + מס' ילקוט-הפרסומים) + משפט-ייעוד — במקום גזירה-מחדש
מהשומות בכל תיק. בלוק ט מצטט את התוקף בנוסח אחיד דטרמיניסטי (format_plan_citation),
כך שתאריך-פרסום/מס'-ילקוט לעולם לא מהוזים ע"י ה-LLM.

- DB: טבלת plans (V38) + CRUD + _normalize_plan_number (G1) + format_plan_citation;
  upsert idempotent (G3) עם כלל-מיזוג: תוקף מאושר לא נדרס — סתירה נרשמת ב-discrepancies
  (G10 / אין בליעה שקטה).
- services/plans_extractor.py: חילוץ עובדתי (claude CLI מקומי) → pending_review.
- block_writer.py: _build_plans_registry_context מזריק משפטי-ציטוט מאושרים בלבד לבלוק ט;
  תכניות חסרות/לא-מאושרות מסומנות במפורש (לא נבלעות).
- tools/plans.py + server.py: extract_plans / plan_get / plan_search / plan_list /
  plan_upsert / plan_review (שער-יו"ר G10), עם extract/get-symmetry (X9).
- scripts/backfill_plans_registry.py: ייבוא מקורפוס-ההחלטות (טיוטות + סופיי-דפנה).
- docs: block-schema (בלוק ט), SKILL, spec 02-data-model + 04.

Invariants: G1/INV-DM2/X1 (מזהה מנורמל בכתיבה) · G2/INV-DM6 (מקור-אמת יחיד, appraiser_facts
ללא שינוי) · G3 (upsert) · INV-DM4/G9 (provenance) · INV-DM5/G10 (review_status) ·
INV-AH (ציטוט דטרמיניסטי) · G5 (lookup לא קורפוס) · G11/block-schema (נוסח-הציטוט) · X9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 13:46:26 +00:00
83293ca619 Merge pull request 'fix(halacha): authoritative rate-limit detection + early-morning catch-up window (supervisor)' (#251) from worktree-halacha-supervisor-ratelimit into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 7s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 10s
2026-06-14 10:03:24 +00:00
49efa94d60 fix(halacha): authoritative rate-limit detection + early-morning catch-up window (supervisor)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
הדריינר רץ בלילה 13→14.6 אך חילץ 0 הלכות: מכסת-המנוי של claude.ai אזלה
(`api_error_status:429 "You've hit your session limit · resets 2:30am UTC"`,
`total_cost_usd:0`), וה-reset (5:30 IDT) נחת מעט אחרי סגירת החלון (05:00 IDT).
המתזמר סיווג זאת שגוי כ-"hung" ועשה restart-storm כל 15 דק' — כי `scan_rate_limit`
קורא רק 120 שורות-זנב, וה-429 (שורה 8273/9170) נקבר תחת ~900 שורות teardown שה-churn
שלו עצמו ייצר. בנוסף "hold" לא עצר את הדריינר → המשך הלמת-429 ובזבוז המכסה.

Fix A — זיהוי rate-limit עמיד:
  • `quota_exhausted()` חדש: מקור-האמת הוא endpoint-המכסה (`subscription_usage`,
    אותו util שה-UI מציג) — durable, לא תלוי בעומק-זנב-הלוג. log-scrape רק כ-fallback.
  • בזמן מוגבל עוצר דריינר online (`hold-stopped`) כדי לא להלום 429; מצית-מחדש
    כשהמכסה חוזרת (exit מיידי כש-endpoint <100%, או probe `claude -p` אם endpoint למטה).

Fix B — חלון catch-up בוקר [05:00–07:00 IDT):
  • נפתח רק לניקוי backlog שנותר כשהמכסה חזרה (מגודר: לא-מוגבל + תור≠ריק) כדי
    שהמכסה המשוחררת לא תתבזבז עד הלילה הבא. הקצה המורחב מועבר לדריינר (window self-guard).

נתונים בטוחים — תיקים נשארו 'processing' for retry, שום הלכה לא אבדה.
13 unit-tests עוברים (parse endpoint, gating של catch-band, win extension); `status` חי OK.

Invariants: מקיים G1 (תיקון-במקור: זיהוי ממקור-מכסה סמכותי, לא מתסמין-לוג),
G2 (אותו endpoint+מנגנון-חלון קיימים — בלי מסלול מקביל), INV-G3/X16 (לא נוגע
ב-checkpointing הדטרמיניסטי). G12 לא רלוונטי (host-side pm2, בלי Paperclip).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 10:02:35 +00:00
f8791ba4a1 Merge pull request 'ci: שער undefined-names (pyflakes) — שהבאג של PR #249 לא יחזור + תיקון NameError חבוי' (#250) from worktree-ci-undefined-name-guard into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m26s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 10s
2026-06-14 09:59:11 +00:00
0a3bc35623 ci: gate undefined names (pyflakes F821) + fix latent NameError in db.py
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
Prevents recurrence of the case-rename 500 (PR #249), whose root cause was
an undefined name (`paperclip_client`) sitting in a background_tasks callable
— invisible until that code path ran in production.

- scripts/check_undefined_names.py: runs pyflakes on web/, mcp-server/src,
  scripts/ and fails ONLY on "undefined name" / "may be undefined" (the
  runtime-crash class). Unused imports / f-strings are NOT gated — keeps the
  check high-signal and green.
- .gitea/workflows/lint.yaml: runs the guard on every PR and push to main,
  in a throwaway venv (PEP-668 safe).
- db.py: `from datetime import date` → `date, datetime`. The guard surfaced a
  real latent undefined name — `insert_panel_round`'s `round_ts: datetime`
  annotation referenced an unimported `datetime` (benign only because of
  `from __future__ import annotations`; now correct).
- SCRIPTS.md: documented the new guard.

Verified: clean tree → exit 0; injected undefined name → exit 1.

Invariants: engineering rule §6 (no silent failures shipping to runtime).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:58:45 +00:00
1fbb1eede6 Merge pull request 'fix(api): שינוי-שם תיק מחזיר 500 — ניתוב Paperclip דרך שער-הפלטפורמה (INV-G12)' (#249) from worktree-fix-case-update-port into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 17s
G12 Leak-Guard / leak-guard (push) Successful in 3s
2026-06-14 08:38:22 +00:00
e2c94144d0 fix(api): route Paperclip title-sync/webhooks through the Port — 500 on case rename (INV-G12)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
PUT /api/cases/{n} crashed with 500 (NameError: 'paperclip_client' is
not defined) whenever the title changed. The G12 platform-port refactor
dropped the `paperclip_client`/`paperclip_api` module imports from
app.py but left three call sites still referencing the old module names:

  - app.py:2096  paperclip_client.update_project_name  (title sync → fires on every rename)
  - app.py:3806  paperclip_api.emit_export_complete_webhook
  - app.py:7390  paperclip_api.emit_missing_precedent_webhook

All three are background_tasks, so the latter two were latent NameErrors
waiting to fire. Expose the three functions through agent_platform_port
(the single Paperclip seam) and call them via the Port, restoring G12.

Invariants: upholds INV-G12 (all Paperclip access via agent_platform_port).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 08:37:51 +00:00
6fc87de1c5 Merge pull request 'feat(ops): פאנל "מתאמי-סוכנים" ב-/operations — מעבר-אדפטר בכפתור' (#248) from worktree-adapter-migrate-ui into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 2m0s
G12 Leak-Guard / leak-guard (push) Successful in 6s
2026-06-13 12:12:40 +00:00
64612240d5 feat(ops): פאנל "מתאמי-סוכנים" ב-/operations — מעבר-אדפטר בכפתור (any→any)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
שלב-ה-UI של מנגנון מעבר-האדפטר (PR #247). הכפתור ב-/operations מריץ את
scripts/migrate_agent_adapter.py בהוסט דרך גשר-court-fetch (הקונטיינר לא יכול
לבצע — צריך FS-הוסט + DB-המובנה), בדיוק כמו כפתורי-pm2.

- court_fetch_service/server.py: endpoint /adapter-migration מאומת (Bearer,
  COURT_FETCH_SHARED_SECRET) שמריץ את הסקריפט עם allowlist-פעולות ו-args אטומיים
  (exec, ללא shell; הסקריפט מאמת). symbol-light לשמירת G12 בשכבת mcp-server.
- web/app.py: proxy POST /api/operations/agents/migrate-adapter → הגשר.
- web-ui: useMigrateAdapter (operations.ts) + AgentAdaptersPanel לפי המוקאפ
  המאושר 02d-operations-adapters.html: roster per-role (מתאם נוכחי+מודל+בורר-יעד),
  סרגל-חירום גלובלי (הכל→Gemini/החזר→Claude), preflight בדיאלוג + toggle שחרור-כלים,
  דגלי מועבר/א-סימטרי. מחזר usePaperclipAgents לתצוגה.

עיצוב אושר ע"י חיים בשער Claude Design (פרויקט IA Redesign X17).
נבדק: tsc --noEmit נקי, eslint נקי.

Invariants: G12 (גשר symbol-light; הסקריפט בשכבת scripts/ הוא שמדבר Paperclip),
INV-MC1 (שתי החברות יחד), INV-IA "מקום אחד" (/operations). המשך FU-8a.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 12:06:40 +00:00
b9e4c1fde4 Merge pull request 'feat(ops): מעבר-אדפטר בטוח לכל סוכן ← כל אדפטר (any→any) — סוגר FU-8a' (#247) from worktree-adapter-migrate into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 12s
G12 Leak-Guard / leak-guard (push) Successful in 4s
2026-06-13 11:36:56 +00:00
f7a8ad48ac feat(ops): מעבר-אדפטר בטוח לכל סוכן ← כל אדפטר (any→any, שתי החברות) — סוגר FU-8a
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
הקשר: החלפת adapter_type ב-dropdown הגולמי של Paperclip מקריסה את הסוכן מיד —
content_arg adapters (gemini_local/deepseek_local) שוברים את ה-frontmatter המוביל `---`
(yargs/arg-parser קורא אותו כדגל → "Not enough arguments following: prompt"), המודל
חייב להתאים ל-provider, ו-excludeTools של gemini גלובלי לכל הסוכנים. בנוסף — שינוי
חברה-אחת מפר את INV-MC1 (סנכרון מדלג על adapter mismatch → drift שקט).

הכלי מיישב את 3 צירי-הכשל data-driven, מעביר בשתי החברות יחד, הפיך מדויק, ומסרב
מעבר לא-בטוח ב-preflight (סוגר את פער FU-8a "אין שער-ולידציה למעבר-אדפטר").

- scripts/adapter_profiles.py — רישום-פרופילי-אדפטר (מקור-אמת יחיד; אדפטר חדש = רשומה אחת)
- scripts/migrate_agent_adapter.py — --check/--apply/--revert/--verify; frontmatter→.nofm.md
  נגזר, sidecar לשחזור, --relax-tools ל-excludeTools, PATCH דרך /api/agents/{id} (לא DB)
- .gitignore + scripts/SCRIPTS.md

נבדק: --verify (9 סוכנים תקינים), --check (CEO→gemini מציג nofm+model+tool-conflict;
provider-guard תופס מודל לא-תואם, exit≠0), ובדיקות-לוגיקה טהורות (strip/canonical/recompute).

Invariants: מקיים G2 (רישום+קובץ-קנוני יחיד, גרסת-gemini נגזרת), G12 (PATCH-API דרך
המעטפת, לא DB), INV-MC1 (שתי חברות יחד). סוגר FU-8a.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 11:28:16 +00:00
4dce06c04a Merge pull request 'fix(supervisor): gate + display weekly-Sonnet instead of weekly-Opus' (#246) from worktree-supervisor-sonnet into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 26s
G12 Leak-Guard / leak-guard (push) Successful in 9s
2026-06-13 10:58:23 +00:00
eac4dd3ac9 fix(supervisor): gate + display weekly-Sonnet, not weekly-Opus
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 8s
On this claude.ai account the populated per-model weekly cap is Sonnet;
seven_day_opus is null (no separate Opus cap). So quota_available() now gates on
five_hour + seven_day + seven_day_sonnet (was seven_day_opus, which never bound),
and `status` prints weekly-Sonnet. The all-models seven_day cap remains the
backstop for Opus usage regardless. Matches the /operations display (#245).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 10:58:05 +00:00
01bc1b9743 Merge pull request 'fix(operations): cache usage endpoint (avoid 429) + weekly-Sonnet instead of Opus' (#245) from worktree-usage-cache into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m48s
G12 Leak-Guard / leak-guard (push) Successful in 4s
2026-06-13 10:44:23 +00:00
693126484b fix(operations): cache the usage endpoint (avoid 429) + show weekly-Sonnet not Opus
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Two follow-ups to the usage-% feature:

1. The /api/oauth/usage endpoint 429s when polled often — /operations refreshes
   every 5s and each refresh hit it (plus the supervisor + ad-hoc calls). Cache
   the last good payload on the host bridge for 60s and serve it; only re-fetch
   when stale, so Anthropic sees ~1 req/min regardless of dashboard polling. On
   a fetch failure (e.g. a transient 429) serve the last good payload instead of
   blanking the card. The 5-hour window moves slowly, so 60s stays fresh.

2. The third meter showed weekly-Opus, which is null on this account (the
   per-model weekly cap that's actually populated is Sonnet). Switched the
   display to seven_day_sonnet / "שבועי · Sonnet". (The supervisor keeps gating
   on seven_day_opus — the halacha drain runs Opus, so the Opus cap is the
   correct gate even when it's null/inactive.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 10:43:57 +00:00
17460044ac Merge pull request 'feat(operations): show real claude.ai subscription usage % on /operations' (#244) from worktree-operations-usage-ui into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m35s
G12 Leak-Guard / leak-guard (push) Successful in 5s
2026-06-13 10:35:18 +00:00
6f3c3963a4 feat(operations): show real claude.ai subscription usage % on /operations
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
Surfaces the 5-hour / weekly / weekly-Opus utilization the Claude Code status
bar shows — the authoritative number, not a token estimate. Design approved via
the Claude Design gate (card 02c-operations-usage.html).

Three layers:
- court-fetch-service (host bridge): new GET /usage reads the OAuth token from
  ~/.claude/.credentials.json and proxies /api/oauth/usage with the required
  claude-code User-Agent. Read-only, no auth (like /pm2). Host-only — the token
  never enters the container.
- web/app.py: _ops_subscription_usage() proxies the bridge /usage; the
  /api/operations snapshot gains a `subscription_usage` field (null when the
  undocumented endpoint is unreachable).
- web-ui: SubscriptionUsagePanel renders three meters (label · % · bar · reset)
  at the top of /operations; bar turns amber >75% / red >90%; hidden when usage
  is null. Types added to operations.ts (hand-maintained snapshot type).

Also fixes a pre-existing react/no-unescaped-entities lint error in
learning-panel.tsx (escaped a `"` in Hebrew text — renders identically).

tsc --noEmit passes; lint error count 0. (Full next build is blocked only by the
manual-worktree node_modules symlink — the Docker build has real node_modules.)

Invariants: G2 (usage surfaced through the existing host bridge + /api/operations
snapshot — no parallel control path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 10:34:50 +00:00
d093319ffd Merge pull request 'feat(supervisor): gate quota on the real claude.ai usage % (OAuth usage endpoint)' (#243) from worktree-supervisor-usage-endpoint into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 13s
G12 Leak-Guard / leak-guard (push) Successful in 6s
2026-06-13 10:19:46 +00:00
9e46db3c48 feat(supervisor): read real claude.ai usage % from OAuth endpoint for quota gating
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
The supervisor's quota check used a tiny `claude -p` probe to decide whether the
claude.ai subscription had room. That works but is indirect (an Opus-adjacent
round trip) and only answers yes/no. Anthropic exposes the actual utilization —
the same 5-hour / weekly / weekly-Opus percentages the Claude Code status bar
shows — via the (undocumented) GET /api/oauth/usage endpoint.

- subscription_usage(): reads the OAuth token from ~/.claude/.credentials.json
  and GETs /api/oauth/usage with the required `claude-code/*` User-Agent (without
  it the request hits an aggressively rate-limited bucket and 429s). Returns the
  parsed {five_hour, seven_day, seven_day_opus, ...} or None on any failure.
- quota_available(): now prefers the endpoint — a drain run resumes only when the
  5-hour, weekly, AND weekly-Opus windows are all <100% (the extractor runs Opus).
  More precise than the probe and sees every limit the way the UI does. Falls
  back to the `claude -p` probe when the endpoint is unreachable (it's
  undocumented and may change).
- `status` subcommand now prints the live percentages + reset times.

Note: this is the data/logic layer only. Surfacing the % on the /operations
page is a visual UI change and must go through the Claude Design gate first
(web-ui/AGENTS.md) — deferred.

Invariants: G1 (resume decision driven by the authoritative usage state).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 10:19:17 +00:00
8d13e26cc8 Merge pull request 'fix(supervisor): re-probe claude.ai quota instead of blindly waiting for the reported reset' (#242) from worktree-supervisor-quota-probe into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 16s
G12 Leak-Guard / leak-guard (push) Successful in 7s
2026-06-13 09:36:18 +00:00
013fe39ea7 fix(supervisor): re-probe claude.ai quota instead of waiting blindly for the reported reset
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 8s
When the halacha drain hit a 429, the supervisor recorded the reset time the
error reported (e.g. "resets 6:50pm UTC") and then HELD until that timestamp,
re-reading it from its own state every tick without ever checking whether quota
had actually returned. claude.ai usually frees up quota earlier than the message
claims, so the drain sat idle for hours after it could have resumed — and only a
manual kick (clear cooldown + trigger) got it going again.

Now, on any tick where we'd otherwise hold on a cooldown, run a cheap live probe
(`quota_available()` → a tiny `claude -p` call, cost ~0) and resume the instant
it succeeds — at most one probe per 15-min tick, only while we believe we're
limited. Conservative on failure (non-zero exit / timeout / limit message →
stay held), so a flaky probe never resumes the drain into a real 429. Adds a
claude_bin() resolver so the probe works under pm2 cron where PATH is bare.

Invariants: G1 (resume decision driven by actual quota state, not a guessed
timestamp); no new control path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 09:35:45 +00:00
96a1144f43 Merge pull request 'fix(operations): disabling the halacha drain stops a running process immediately' (#241) from worktree-drain-disable-stop into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 10s
G12 Leak-Guard / leak-guard (push) Successful in 6s
2026-06-13 09:03:33 +00:00
a44827c3dd fix(operations): disabling the halacha drain now stops a running process immediately
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
The /operations "disabled" toggle only wrote drain_controls.disabled, which the
drain checks at STARTUP — so a drain already mid-run kept going until the queue
emptied or the night window closed. Disabling did not stop a running drain.

Three layers, immediate + backstops:
- web/app.py operations_drain_toggle: on disable, also stop the running process
  immediately via the host pm2 bridge (_ops_pm2_control). Best-effort — a bridge
  failure doesn't fail the toggle.
- halacha_drain_supervisor.py: each tick now reads the disabled flag (added to
  db_snapshot) and, when set, stops the drain and never re-triggers it —
  regardless of burst/window. Backstop if the UI path failed (≤ one tick).
- drain_halacha_queue.py: re-check is_drain_disabled at the top of every round,
  so a drain disabled mid-run halts at the next round boundary. Per-chunk
  checkpoints mean the in-flight case loses nothing.

SCRIPTS.md updated for both drain and supervisor.

Invariants: G1 (fix at source — the disable control honoured along every path,
not just at startup); G2 (no parallel control path — same drain_controls flag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 09:03:07 +00:00
72f81734f1 Merge pull request 'fix(db): serialise schema migrations with an advisory lock + stagger drain crons' (#240) from worktree-drain-deadlock into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
G12 Leak-Guard / leak-guard (push) Successful in 6s
2026-06-13 08:08:56 +00:00