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>
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>
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>
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>
legal-halacha-drain crashed 29× with asyncpg DeadlockDetectedError. Root cause:
every short-lived cron drain re-runs the idempotent schema migrations on startup
(get_pool → _run_schema_migrations), and three jobs (metadata-drain, halacha-drain,
halacha-supervisor) all fired on the same minute (*/15 / top-of-hour). Two
processes running the DDL concurrently took AccessExclusiveLock in opposite order
→ Postgres killed one with a deadlock.
Two-layer fix:
- Root cause: wrap _run_schema_migrations in a session-level pg_advisory_lock so
only one process applies DDL at a time; concurrent migrators wait instead of
deadlocking. DDL body extracted to _apply_schema_ddl. Idempotent, schema
unchanged.
- Defence-in-depth: give each cron drain a distinct firing minute —
metadata :00, supervisor :05, halacha-drain :10, digest :12, court-fetch :17 —
so siblings no longer start at the same instant. SCRIPTS.md updated to match.
Invariants: G1 (fix at source — the single migration path — not the symptom);
G2 (no parallel control path introduced).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- legal-halacha-supervisor had no SERVICE_LABELS entry → the row rendered the raw
English pm2 name. Add a Hebrew label (the English id stays as the mono subtitle,
like every other row).
- legal-halacha-drain label said "×שעתיים" (every 2h) — wrong. The cron fires
hourly across the night band; the script trims to the 23:00–05:00 IL window.
Relabel to "חלון-לילה 23:00–05:00".
Content/i18n fix only — no visual/layout change (design-gate bug-fix exception).
Implements the chair-approved Claude Design mockup (02b-operations-burst): a single
gold "⚡ הפעל BURST" button on the legal-halacha-drain row that opens a deadline
dialog (datetime-local, default the upcoming Saturday 18:00, quick chips for Sat
18:00 / +5h / midnight). While a burst is active the row shows a gold pill with the
deadline + an "עצור BURST" stop button. Manual only.
- operations.ts: burst_until on OpsService + useDrainBurst hook (POST .../burst).
- page.tsx: BurstControl component, gated to legal-halacha-drain.
- types.ts: regenerated (npm run api:types) — the new burst route.
Active-state is derived from burst_until presence (the supervisor NULLs it at the
deadline, snapshot refetches every 5s) — no impure Date.now() in render.
Invariants: G1/G2 — single DB-backed control (drain_controls.burst_until), shared
with the host supervisor; no parallel path. UI passed the Claude Design gate.
תיקון: כל העלאת קובץ עם שם עברי נכשלה ב-500 תחת backend s3-only. השורש:
`ingest._stage_file` מצרף את שם-הקובץ המקורי כ-S3 object metadata
(`metadata={"filename": src.name}`), ו-`S3Backend.put_bytes` העביר אותו כמו-שהוא
ל-`put_object`. botocore אוכף ASCII-only על S3 metadata → ParamValidationError →
500. שם עברי כמו "יומון 5167 - 11.6.26.pdf" שבר כל upload. נחשף ב-cutover ל-s3-only
(2026-06-11): קליטת היומונים (וגם כל מסמך/פסיקה עם שם עברי) הפסיקה לעבוד; היומון
האחרון שנקלט (5165, 9.6) היה לפני ה-cutover.
התיקון (נרמול-במקור, G1; בשכבת-האחסון היחידה, INV-STG2):
- `_ascii_metadata` מקודד ערכי-metadata לא-ASCII ב-percent-encoding (lossless,
שחזור עם urllib.parse.unquote); ASCII רגיל עובר ללא שינוי (קריאוּת).
- `S3Backend.put_bytes` מחיל אותו על כל ערכי ה-Metadata.
בדיקות: test_ascii_metadata_encodes_hebrew (helper) +
test_s3_put_bytes_sends_ascii_metadata (משחזר את מסלול-הכשל מול fake put_object).
16 עוברות בקובץ.
Invariants: מקיים G1 (נרמול-במקור, לא תיקון-בקריאה), INV-STG2 (שם-קובץ עברי
כ-metadata ולא ככ-key), G2 (אין מסלול-אחסון מקביל — תיקון ה-choke-point היחיד).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The host pm2 supervisor imports legal_mcp.services.db from the host repo checkout,
which can lag main by many commits. Depending on the just-added db.set_drain_burst/
get_drain_burst would require the host checkout to be current. Use raw SQL via the
stable db.get_pool() instead — the supervisor now depends only on get_pool + the
drain_controls.burst_until column (the shared contract with the /operations API).
The container-side API keeps using the typed helpers (it ships the code in-image).
Invariants: G1/G2 unchanged (same single DB column, no parallel path).
The halacha-extraction backlog needs to be worked off the chair's leftover weekly
Claude quota on demand. This adds a MANUAL, time-boxed "burst" — run the drain
continuously now until a chosen deadline (default the upcoming Saturday 18:00 IL),
managed interactively from /operations — plus the permanent health-supervisor that
enforces it.
Backend (this PR; deploys via Coolify + host pm2):
- db: drain_controls.burst_until (SCHEMA_V37) + set_drain_burst/get_drain_burst/
get_drain_bursts. Single source of truth shared by the container-side /operations
API and the host-side supervisor.
- web: POST /api/operations/drains/{name}/burst (on→until|next-Sat-18:00, off→NULL),
and burst_until surfaced per-service in the /operations snapshot.
- scripts/halacha_drain_supervisor.py + legal-halacha-supervisor.config.cjs: pm2 cron
(*/15, zero Claude quota) — re-triggers idle drain, restarts a HUNG run (liveness =
per-chunk checkpoints, NOT log mtime), backs off on 429 until the parsed reset
(fresh-gated), verifies crash-safe staging. Reads burst_until from the DB; burst
auto-expires at the deadline (never bleeds into a fresh week).
UI (separate follow-up PR, after Claude Design approval): the /operations toggle +
date-picker that calls the burst endpoint.
Invariants: G1 (normalize at source — burst lives once in the DB, read by both
surfaces), G2 (no parallel control path — CAPTURE field on the existing
drain_controls + orchestrates the existing drain, not a new one), G12 (no Paperclip
touch), §6 (no silent error-swallow — burst-clear failure is surfaced as a note).
Picks up GET /api/cases/{case_number}/learning-status (PR #233) plus accumulated
schema drift from prior backend changes that never regenerated types.ts. tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
אחרי העלאת החלטה סופית והרצת שני הפייפליינים האוטומטיים (למידת-קול,
חילוץ/אימות-הלכות), התיק לא הציג אם כל תהליך בוצע/הצליח/למה-נכשל. במיוחד
תקלת chair_name ריק (2026-06-12) שמפילה בשקט את העתק-ה-case_law → חילוץ-הלכות
לא מתחיל בכלל, בלי שזה גלוי. כעת מוצגות שתי אינדיקציות ליד כפתורי-ההרצה.
Backend (גזירה ממקור-יחיד, ללא מסלול-מעקב מקביל):
- SCHEMA_V36: draft_final_pairs.learning_run (JSONB) — שדה-תיעוד על פנקס-ההתאמה
(INV-LRN4), חותם את תוצאת-הריצה של פייפליין-הלמידה (succeeded/failed+סיבה+at).
- set_learning_run_outcome() — חיתום הצלחה/כישלון על ה-pair האחרון.
- case_learning_status() — גזירה read-only מ-draft_final_pairs/style_corpus/
decision_lessons/case_law/halachot: בוצע? הצליח? למה-לא? כמה הלכות חולצו.
- final_learning_pipeline.py — חותם outcome בהצלחה וב-except (surfaced, לא בלוע).
- חשיפה: case_get מוסיף learning_status (→MCP + /api/cases/{case}/details) +
endpoint ייעודי GET /api/cases/{case}/learning-status (אותה פונקציה — בלי כפילות).
UI (אושר דרך שער-העיצוב Claude Design — כרטיס 21-final-learning-status):
- useCaseLearningStatus (api/learning.ts) — hook + polling עדין בזמן in-flight.
- LearningStatusBadges — 2 שורות (למידת-קול / חילוץ-הלכות) עם badge + תת-שורה
(מס' לקחים · רישום-קורפוס / מס' הלכות + פירוק אושרו/ממתינות/נדחו / סיבת-כישלון).
- שילוב ב-drafts-panel תחת "החלטה סופית של היו״ר" + אינוולידציה בכפתורי-ההרצה.
אומת מול ה-DB החי: הצליח+5 הלכות (8174-12-24) · נכנס-אך-pending (1200-12-25) ·
לא-נכנס-לקורפוס (8125-09-24) · round-trip חיתום-כישלון. tsc/eslint נקיים.
Invariants: G1 (נרמול-במקור — גזירה, לא טלאי), G2 (אין מסלול מקביל — שדה על
הפנקס הקיים + exposer יחיד), INV-LRN4 (פנקס-ההתאמה), INV-IA1 (מקור-אמת יחיד).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ה-wakeup המיידי ל-CEO לחילוץ-הלכות הוא best-effort; כשהוא נכשל
(למשל churn-פריסה), הבקשה נשענת רק על הדריינר-הלילי והפריט נשאר
'pending' בלי שאיש רואה — עד ~16-18 שעות. מוסיף קטגוריה חמישית
ל-api_chair_pending שמציפה case_law עם halacha_extraction_status=
'pending' שכבר עבר את חלון-הדריינר (requested_at < now()-6h), כך
שכשל-שקט הופך לפריט-יו"ר גלוי במרכז-האישורים ("שלא יישכח").
שאילתת-מקור ישירה (תואם הדפוס), href ל-/precedents (שם כפתור
request-halachot מעיר CEO = retry). אפס שינוי-frontend — דף-האישורים
מרנדר categories גנרית (ApprovalCard לפי label/severity/count/href).
gate-free (לוגיקה/נתון בלבד). הסף 6h תכוונן.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FU-1 (#214) הוסיף ל-halacha_panel_approve.py את הדגל --no-capture
ואת השימוש `if not args.no_capture` בשלב-הלכידה. אבל
final_halacha_pipeline.py קורא ל-hpa.main() עם Namespace שנבנה ביד
(limit/concurrency/apply בלבד) — בלי no_capture. לכן הרצת הצינור
("הרץ הלכות") קרסה ב-AttributeError בדיוק בשלב שמירת-הסבבים, אחרי
שה-apply כבר רץ → 0 סבבים נלכדו לתיק.
תוקן: הוספת `no_capture=False` ל-Namespace. אומת מקצה-לקצה על
8174-12-24 → "captured 49 panel rounds, errors=0".
audit: רק 2 מקומות בונים Namespace ביד לקריאת main() של סקריפט אחר —
זה (תוקן), ו-final_learning_pipeline.py→style_lesson_panel (נבדק, כל
ה-args מסופקים, תקין). אין באגים נוספים מהמחלקה הזו.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
הבעיה: בדף /missing-precedents לא ניתן היה לאתר פסיקה חסרה לפי מספר ההחלטה
החסרה עצמה (למשל 85074). השדה היחיד לחיפוש-תיק עשה get_case_by_number על
מספר ה-ערר שבו צוטטה הפסיקה — ולכן הקלדת מספר-הפסיקה החזירה רשימה ריקה,
למרות שהרשומה קיימת (ערר (ת"א 85074-04-25) ... status=open).
התיקון (הרחבת השדה הקיים, ללא עמוד/שדה חדש — בהנחיית חיים):
- db.list_missing_precedents: פרמטר q חדש — ILIKE על mp.citation +
mp.case_name + cited-in c.case_number (אינדקס-פרמטר יחיד, additive;
שאר הקוראים לא נוגעים).
- GET /api/missing-precedents: פרמטר q; case_id/case_number נשארים
מסננים-מדויקים לקוראים תכנותיים.
- web-ui: התווית "תיק (מספר ערר)" → "מספר תיק", placeholder
"85074 או 1017-03-26"; השדה שולח q (חיפוש חופשי) במקום case_number.
Debounce 350ms נשמר.
api:types לא חודש: ה-hook בונה את ה-querystring ידנית וה-response לא
השתנה; חידוש מול prod (שעוד לא נפרס) רק היה מושך drift לא-קשור.
בדיקות: tsc --noEmit נקי, eslint נקי על הקבצים שהשתנו, py_compile נקי.
Invariants: G2 (הרחבת היכולת הקיימת, לא מסלול-חיפוש מקביל), INV-IA1
(שער/דף יחיד לפסיקה-חסרה — בלי עמוד חדש), §6 (ללא בליעת-שגיאות).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
תיקון: העלאת פסיקה/החלטת-ועדה (precedent-library + internal-decisions) נכשלה
תחת backend s3-only עם "Package not found at '/data/...docx'" / "Converted file
not found". השורש: `ingest._stage_file` כותב את הקובץ דרך `storage.put_file`
ומחזיר נתיב-DATA_DIR, אבל תחת s3-only ה-blob נכתב רק ל-MinIO ואין עותק בדיסק —
ואז הצינור קרא את הנתיב ישירות מהדיסק (extract_text) → קובץ לא קיים. מסלול
תיקי-המקרה לא נפגע כי הוא שומר עותק-דיסק + mirror_file; רק מסלול _stage_file
המשותף קרא את ה-key כאילו הוא על הדיסק.
התיקון (נרמול-במקור, G1; קריאה דרך שכבת-האחסון, INV-STG1):
- `_stage_file` מחזיר עכשיו את ה-KEY (נתיב יחסי-DATA_DIR), לא Path.
- `ingest_document` ו-`digest_library` מאתרים נתיב-קריאה מקומי דרך
`storage.ensure_local` (עותק-דיסק תחת filesystem/dual; הורדה ל-temp תחת
s3-only) ומנקים את ה-temp ב-finally — בלי דליפה ל-/tmp.
- מולטימודל (PDF) קורא את אותו נתיב מקומי מאומת.
בדיקות: test_unified_ingest::test_ingest_reads_via_ensure_local_when_no_disk_copy
מדמה backend ללא עותק-דיסק ומוודא שהצינור משלים (נכשל מול הקוד הישן). 55 עוברות.
Invariants: מקיים INV-STG1 (קריאה/כתיבה רק דרך שכבת-האחסון), G1 (נרמול-במקור,
לא תיקון-בקריאה), G2 (אין מסלול מקביל — תיקון הצינור הקנוני).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
מבטל את ה-toggle "תור נקי / דורש תיקון-חילוץ" שבו "תור נקי" ריק
לגמרי (כל ההלכות-הנקיות נפתרו), והעבודה האמיתית חבויה מאחורי
הכפתור השני שגם מערבב התלבטות-פאנל עם פגמי-חילוץ. אושר ב-Claude
Design (כרטיס 19-halacha-queue-unified).
במקום זה — תור אחד, fetch אחד, פיצול client-side לפי **סוג-הפעולה**:
- "להכרעתך" = הלכות שהפאנל דן בהן (יש panel_round) או נקיות →
אשר/דחה, עם טבלת-ההתלבטות; ממוין פיצול-פאנל-תחילה (FU-3).
- "דורש תיקון-חילוץ" = מסומנות-דגל שלא עברו התלבטות → תיקון-חילוץ.
`useHalachotPending` אוחד לקריאה אחת (exclude_low_quality=false +
order_by_priority + cluster + include_equivalents + include_panel_round);
נוסף `isExtractionFixItem(h)` (= !panel_round && יש דגל). PendingPanel
מפצל ב-useMemo, segmented-control עם מוני שני הדליים. אפס שינוי-backend
(הפרמטרים כבר קיימים מ-#220/#222).
display-only, שער-אישור יחיד (INV-IA/G10). ולידציה: tsc + eslint נקי.
חלק מ-#133.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
תור-אישור-ההלכות הקיים (order_by_priority, #84.3) מקדם עכשיו את ההלכות
שהפאנל התלבט עליהן: split קודם, אחר-כך incomplete — התוויות בעלות-הערך
הגבוה ביותר ללולאת-הלמידה (הכרעת-היו"ר מפרקת אי-ודאות אמיתית ומזינה את
זיקוק-ה-rubric ב-FU-4). uncertainty-sampling על סיגנל-המחלוקת האמיתי של
הפאנל, לא רק confidence-החילוץ.
- list_halachot: LEFT JOIN לאחרון-הסבבים (DISTINCT ON latest round_ts מ-
halacha_panel_rounds) + מפתח-מיון ראשי CASE verdict split→0/incomplete→1/
else→2, לפני מפתחות #84.3 (corroboration→confidence→age). סבבים פה-אחד
ופריטים-ללא-סבב נשארים בזנב עם הסדר הקיים.
- panel_verdict נחשף בכל שורה (UI יכול לתייג "פיצול" + ביקורת-סדר).
- שימוש חוזר בדגל order_by_priority הקיים ובטאב הקיים — בלי מסלול/דגל
מקביל (G2). ה-UI כבר מבקש order_by_priority=true → אפס שינוי-UI, אין
צורך בשער-עיצוב.
- test_halacha_priority_panel_order.py: 3 בדיקות offline (SQL-capture) —
מפתח-מחלוקת ראשי בעדיפות, FIFO ללא דליפת-CASE, panel_verdict נבחר.
Invariants: INV-G10 (capture-only, לא משנה review_status) · G1/G2 ·
INV-IA (אותו שער/טאב). רגרסיה: 76 בדיקות עברו.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One-time host migration (executed 2026-06-12): adds the missing 2-digit month
to 11 case numbers (and corrects 1046-26 → 1024-02-26, a wrong serial).
All legal-ai FKs are on cases.id (UUID) → untouched. The script atomically
migrates, per case, everything that embeds the number as TEXT:
· cases.case_number + every column containing 'cases/{old}/' (file_path AND
image_thumbnail_path — the latter is a DATA_DIR-relative storage key with
no '/data' prefix, hence the slash-less needle)
· disk dir + case.json
· MinIO keys across 3 buckets (legal-immutable = WORM, copy-only)
· Gitea repo rename + local .git remote + description
· Paperclip project name
For the 4 archived cases whose final was ingested, the canonical number is
propagated to the precedent + style corpora identifier fields (case_law,
style_corpus, style_exemplars, citations) per chair decision — document
content / full_text / OCR text is left as the historical record.
Verified: 0 stale identifier/path refs across all 11; documents, thumbnails,
drafts, Gitea, Paperclip all resolve under the new numbers. Per-case backups
in data/audit/renumber-*.json.
Invariants: G1 (normalise at source — single rename op, not read-time patch),
G2 (no parallel path — reuses the app's DB pool + storage semantics),
G12 (Paperclip touched only via its declared surface).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
מציג את התלבטות 3-השופטים (הצבעה+נימוק לכל לינאז' + ה-verdict)
בתוך כרטיס-האישור הקיים של דפנה ב-/precedents → "ממתין לאישור",
כדי שהכרעתה — תווית-הזהב שהלולאה לומדת ממנה — תהיה מיודעת ב*למה*
הפאנל נחלק. אושר ב-Claude Design (כרטיס 18-halacha-deliberation).
Backend (opt-in, ברירת-מחדל off — קוראים קיימים לא מושפעים):
- db.list_halachot(include_panel_round=True) → _annotate_panel_rounds
מצרף את הסבב האחרון מ-halacha_panel_rounds (DISTINCT ON, latest).
- GET /api/halachot?include_panel_round=true.
Frontend:
- Halacha.panel_round (טיפוס ידני; ה-endpoint מחזיר dict).
- תור-הסקירה (useHalachotPending) מבקש include_panel_round בשני
הדליים (clean=keep, needsFix=nli/entailed).
- רכיב PanelDeliberation: טבלת 3-שופטים (✓נתמך/✗הכלל-חורג + נימוק),
תג-ורדיקט "פיצול 2:1", ושורת "שורש המחלוקת" (קפדני↔תמצית) רק
בפיצול-entailment. מוזרק אחרי רשת הכלל/ציטוט.
שער יחיד — אין עמוד/שער חדש (INV-IA/G10); display-only, לא נוגע
ב-review_status. ולידציה: py_compile + tsc --noEmit + eslint נקיים;
בדיקה פונקציונלית: panel_round מצורף ל-6 שיש להן סבב, 1994 בלי.
חלק מ-#133 (FU-2). דורש deploy + (אופ') npm run api:types אחרי.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>