The voice-fingerprint (docs/daphna-voice-fingerprint.md) is the PRIMARY abstract
style channel the writer consumes (07-learning §0.2 channel A), but it was
hand-authored and never regenerated from the growing corpus — the "voice" the
writer adapts stayed frozen while finals accumulated. PR #345 unfroze the
structural half (corpus-measured ratios → writer); this is the prose half.
scripts/regenerate_voice_fingerprint.py reads the corpus (style_corpus stats +
a bounded block-level style_exemplars sample + measured section ratios) and asks
Opus (local) to PROPOSE refinements/additions to the fingerprint — STYLE / METHOD
/ VOICE / LEXICON only, grounded in corpus evidence, never case substance
(INV-LRN5), preserving curated insights rather than replacing them.
CHAIR-GATED, HARD (INV-LRN1): the prose profile is voice-knowledge, not a
low-risk style rule, so it does NOT auto-flow. The script writes a PROPOSAL to
data/curator-proposals/ (the existing curator-proposal surface) and NEVER
overwrites the live fingerprint; the chair reviews and hand-commits accepted parts
— the same manual gate the fingerprint already uses. Runs on the host (claude CLI).
Reuses claude_session + measure_corpus_ratios + the curator-proposals dir (G2 —
no parallel synthesis or gate). SCRIPTS.md + spec §0.2 updated.
Invariants: INV-LRN1 (hard chair gate for voice-knowledge prose), INV-LRN5
(style-only synthesis), G2 (reuses existing synthesis inputs + proposal surface).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The writer, learning panels, and legal_mcp tools run from the HOST tree
(Paperclip agents claude -p with cwd=~/legal-ai; pm2 services import from
mcp-server/src) — not the container. A merge + Coolify deploy only refreshes the
FastAPI container; host-side code stayed on the last manual `git pull`. On
2026-06-28 the host was 16 commits behind, so a day of learning-loop fixes sat
dormant until pulled by hand.
host_sync.sh (run every minute via cron, like the existing data/training pull)
fast-forwards the host tree to origin/main when it falls behind. Fast-forward
ONLY — never forces, so the perpetually-dirty .taskmaster/tasks/tasks.json is
never clobbered (clean ff leaves it; an upstream conflict logs + bails for manual
handling). Agent runs spawn fresh and pick up new code with no restart; only the
long-running pm2 services that import app code (chat_service, court_fetch_service,
reaper) are restarted, and only when their own files changed (precise, minimal
churn). Logs to data/logs/host_sync.log.
Cron entry (added on the host, flock-guarded):
* * * * * flock -n /tmp/host_sync.lock bash ~/legal-ai/scripts/host_sync.sh
SCRIPTS.md updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The style-exemplar corpus (channel B — the writer's block-level retrieval of
Dafna's real prose) was FROZEN at the one-time seed backfill: new finals were
enrolled into style_corpus but never broken into exemplars, so the richest style
channel never grew (8137/8126/8174 had 0 exemplars). The writer kept retrieving
only March–April seed paragraphs no matter how many finals were signed.
Extract the per-decision exemplar logic (section→paragraph→Voyage-embed→replace)
into a shared service `legal_mcp.services.style_exemplars.extract_and_store` —
the SINGLE implementation now used by BOTH the one-time backfill and the live
enrollment path (G2; no parallel extractor). `_enroll_final_in_library` calls it
on every final upload (source='internal_committee', the same source the writer's
search_style_exemplars reads). Voyage embeds over REST → container-safe;
best-effort, surfaced in the upload response, never fails the upload.
Effect: every signed final now grows the exemplar corpus, so the writer's
block-level style retrieval improves with each decision — the core "learn from
every decision" fix for channel B. Path A (style_distance_history) will track
whether the larger exemplar pool reduces style-distance over time.
Invariants: G2 (one extraction path shared by backfill + enroll), INV-LRN5
(style/structure prose only — substance routes elsewhere), INV-LRN4 (the
draft↔final loop now feeds the exemplar channel, not just the lesson channel).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The voice-learning panel produced 81 vetted style lessons — all stuck at
review_status='proposed' with 0 ever approved — so the writer (which reads only
'approved') received NONE of them. The system captured learning but never let it
flow. Chair decision (2026-06-28): a GRADUATED gate by content risk.
- STYLE lessons (categories style/structure/lexicon/tabular) the 2/2 panel kept →
created as review_status='approved' → flow to the writer immediately, reversibly
(chair vetoes in /training). The deepseek+gemini panel only emits style_method
and only on 2/2 consensus, so this is exactly the gate's criterion; substance is
already filtered out and skipped, and routes through the strict halacha gate.
- SUBSTANCE (halacha/precedent/fact) stays a HARD chair gate — unchanged.
style_lesson_panel.py: _review_status_for(category) sets the gate explicitly
(approved for _STYLE_CATEGORIES, else proposed); the apply loop passes it to
db.add_decision_lesson (which already accepts review_status — no DB change).
Spec: INV-LRN1 rewritten as the graduated gate (hard for substance; reversible
auto-flow for consensus style — still "under user control" via veto, per
NCSC/CEPEJ); §0.6 updated to match.
A one-time backfill of the 81 existing panel style lessons to 'approved' runs
separately (DB, post-merge).
Invariants: INV-LRN1 (amended — graduated), INV-LRN5 (style-only, substance never
auto-flows), G10 (human control preserved as reversible veto).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
renumber_cases.py step 6 only renamed the Paperclip project, leaving the
case↔issue linkage on the OLD case number. get_case_issues() keys on two
text surfaces the script never touched:
• plugin_state.legal-case-number (value_json) — the authoritative linkage
• issues.title — the '[ערר {cn}] …' tag the title-path lookup matches
Result: after a renumber the issues kept the old number, get_case_issues
returned [], and post-final actions (run-learning / run-halacha) silently
skipped with reason "no_issue" — surfaced in the UI as "לא הופעלה למידה".
Discovered on case 8137-11-24 (renamed from 8137-24).
Now the Paperclip step rewrites all three surfaces (projects.name +
plugin_state + issue titles); inspect_paperclip + the dry-run report show
the linkage-row and issue-title counts that will be rewritten.
The 11-case migration already ran; the matching DB rows were fixed
manually (incl. this commit's logic) for all stale cases. This change is
for any future renumber.
Invariants: G1 (normalize at source — the linkage, not a read-time
workaround). G12 note: this is a one-time host-side migration script that
already connects directly to the Paperclip DB by design (below the platform
port, like the existing projects/MinIO/Gitea surgery) — it does not add a
parallel runtime path through agent_platform_port.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inline # comments in gitignore are not supported — they were silently
breaking three patterns (data/checkpoints/, data/adapter-migration-state.json,
.claude/agents/.generated/). Moved comments to their own lines and added
missing entries for runtime dirs (data/audit/, data/logs/, etc.) and
temp files (.interaction_tmp.json, .design-build/, .taskmaster bak files).
Also tracks previously untracked legitimate files: scripts, tests, docs,
skills references, .env.example, taskmaster templates.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switches the scanned-PDF fallback from Google Cloud Vision to
Mistral OCR (mistral-ocr-latest) for better Hebrew accuracy and
robustness against broken embedded OCR layers (e.g. case 1044-03-26
which returned English garbage through Vision).
Routing strategy (document-level, not per-page):
- PyMuPDF extracts all pages; pages that pass _text_quality_ok()
use PyMuPDF output directly (free, ~50ms).
- If ANY page fails quality → Mistral OCR called once for the whole
PDF, returning per-page Markdown for all pages (consistent format,
no plain-text/Markdown mix within a document).
Markdown output preserved: Mistral returns ## headers and |tables|;
chunker updated to recognise ATX Markdown headers (##/###) as section
boundaries in _split_into_sections().
Config: GOOGLE_CLOUD_VISION_API_KEY → MISTRAL_API_KEY; allowlist
updated vision.googleapis.com → api.mistral.ai.
MISTRAL_API_KEY added to Coolify container env.
Invariants: G1 (single OCR fallback path, not parallel), G2 (no
duplicate extractor route), INV-AH (Mistral handles gershayim
natively; quote-fix only on PyMuPDF path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The internal citation graph fed only RANKING (raw in-degree), and the per-citation
TREATMENT was never classified — so a precedent distinguished N times got the same
authority boost as one followed N times (INV-COR2 violation), and the signal never
reached the agents' reasoning. Wires the full path:
Phase 1 — scripts/classify_citation_treatments.py: classify each linked edge's
treatment (followed/distinguished/…) from its match_context via
corroboration.classify_treatment (Opus 4.8 @ xhigh, local), filling
precedent_internal_citations.treatment. Idempotent.
Phase 2 — db.refresh_verified_layer: count only NON-negative treatments toward
verified/cite_count (INV-COR2/COR4). Unclassified counts as neutral-positive so
the signal degrades gracefully before classification runs.
Phase 3 — db.citation_authority(ids): per-precedent {total, positive, negative,
unclassified, by_treatment}. Surfaced as `cited_by` in search_precedent_library
hits and precedent_library_get, and `treatment` per incoming citation.
Phase 4 — legal-researcher/analyst/writer prompts: weigh & ARGUE authority
("הלכה שאומצה ב-N החלטות ועדת-ערר"), flag distinguished/overruled, never invent
the count (INV-AH; writer is read-only of the analyst).
Auto-approval stays kill-switched off (chair gate preserved, INV-G10). No schema
change (treatment column already existed). Operational: run the classifier +
refresh_verified_layer over the 379 edges, then sync agents across companies.
Invariants: G2 (one classifier + one authority query, reused), INV-COR2/COR3/COR4
(negative never corroborates; point-specific; ≥N), INV-G10 (no auto-approval),
INV-AH (no invented numbers).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase C — scripts/cull_principles.py: re-adjudicates every existing 'original'
principle with the SAME panel regime (panel_keep_score → classify → apply_cap),
reversible (CSV backup + rejected canonical recoverable), usage-throttled.
panel_extraction.panel_keep_score + apply_cap (shared, G2). Dry-run on 3
decisions: 37→15 survive.
Phase D — services/principles.py: source-derived label הלכה (binding court) /
כלל פרשני (committee) / עיקרון (persuasive); umbrella עקרונות משפטיים. Wired into
canonical_halacha_get/list (principle_class+principle_label). UI string changes
deferred to the Claude Design gate. spec INV-LRN7; SCRIPTS.md; 7 new tests; 428 green.
Phase E needs no new code — synthesis already targets pending_synthesis, which the
cull leaves only on survivors (rejected canonicals → 'rejected').
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Grounded (INV-AH) multi-instance synthesis with drift guard + chair gate
(pending_review, G10). Single path used by backfill, MCP tool, nightly drain.
HELD from production run pending the principles-redesign (rename+cull, #152).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Scope halacha_panel_approve.py to a single case's pending halachot so the
3-judge deliberation (#133/FU-2) can be captured on demand for one case
(e.g. after a fresh extraction) instead of running the whole ~1.5k queue.
Capture-only path unchanged; no auto-approval. Applied before --limit.
Invariants: G1 (scope at source, not a parallel route) · G2 (reuses the
existing panel/capture path) · INV-G10 (chair gate untouched — dry-run
captures to halacha_panel_rounds only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- halacha_batch_reconcile.py --link now exits with error (V41 freezes
equivalent_halachot; equivalences live in canonical_id instead)
- link_equivalent_halachot emits DeprecationWarning (callers ≥ Python 3.2)
- SCRIPTS.md already marks --link as deprecated; no further doc change needed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces canonical_halachot table: one row per unique legal principle,
replacing the equivalent_halachot bidirectional-link model (V28/G2 improvement).
Per-precedent halachot rows become instances that point to their canonical.
Schema (V41):
- canonical_halachot: canonical_statement, rule_type, practice_areas,
subject_tags, embedding (ivfflat), review_status (pending_synthesis→published),
first_established_in FK → case_law, instance_count.
- halachot: +canonical_id FK, +instance_type (original|citation|application),
+treatment; rule_statement + embedding become nullable for citation instances.
- halacha_citation_corroboration: +canonical_id FK so X11 aggregates at
principle level, not instance level. store_corroboration auto-populates it
via INSERT...SELECT.
New DB functions: create_canonical_halacha, nearest_canonical_halacha
(threshold search for Phase 3 lookup-before-insert), refresh_canonical_instance_count,
get_canonical_halacha (principle + instance list).
Backfill: scripts/backfill_canonical_halachot.py — dry-run by default,
--apply to execute. Uses union-find over equivalent_halachot pairs, picks
canonical representative (corroboration→confidence→earliest), creates canonicals,
sets canonical_id + instance_type on all instances.
Invariants: G2 (equivalent_halachot deprecated post-backfill), INV-G10
(canonical review_status gate), INV-DM7 (authority derived, not stored),
INV-AH (canonical_statement grounded in source statements, pending_synthesis
until chair approves).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
27/28 הלכות, 100% quote-verified, ביטחון חציון 0.86 מול 0.78 של Opus
אך 0 פריטים מתחת ל-0.7 (חשד ל-over-confidence). ריאלי כ-fallback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SCRIPTS.md: הסקריפטים החד-פעמיים (25, לפי עמודת Scheduled) הוצאו מסעיף
"## סקריפטים פעילים" אל סעיף חדש "## חד פעמי" בתחתית (לפני .archive),
מקובצים באותם תתי-כותרות ### לפי תת-נושא. כל 79 השורות (54 חוזרות + 25
חד-פעמיות) נשמרו מילה-במילה. הערת-הקונבנציה עודכנה.
/scripts: ה-parser מזהה את סעיף "## חד פעמי" כסקשן נפרד; הרינדור דו-רמתי —
כותרת-סעיף "פעילים" + קבוצות, ואז כותרת-סעיף "חד פעמי" + קבוצות-משנה
מוזחות (ms-5), מקופלות כברירת-מחדל. סטטוס-השורה נגזר מהסעיף.
מאושר דרך שער-העיצוב (מוקאפ 16-scripts עודכן; חיים אישר מראש).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ה-backfill של citation_formatted חשף קריסה ב-apply_to_record: כשפסק-דין
חיצוני מכיל docket שכבר שייך לרשומה כפולה אחרת, נרמול case_number → docket-נקי
נתקל ב-uq_case_law_external_number ומפיל את כל המיזוג (כולל הציטוט).
דוגמה: 'ע"א 3213/97' → '3213/97' שכבר קיים (כפילות נקר).
- db.case_number_collides(case_number, exclude_id) — בודק אם docket כבר שייך
לרשומה לא-internal אחרת (האינדקס החלקי).
- apply_to_record — מדלג על נרמול ה-case_number כשיש התנגשות (כפילות לדדופ
בהמשך, לא ענייננו כאן) וממשיך לכתוב את הציטוט. no-silent-swallow: מתעד warning.
- scripts/backfill_precedent_citations.py — try/except per-row + מונה שגיאות,
כך ששורה אחת לא מפילה את האצווה.
אומת: ריצה-מחדש מלאה ללא קריסה (0 שגיאות); ההתנגשות תועדה ודולגה כצפוי;
פסיקת בית-משפט: 224/228 מולאו, 4 נמנעו (חסר צדדים/תאריך — abstention, INV-AH).
test_fu2b_reconcile ✓.
Invariants: INV-AH (abstention) · G1 (נרמול-בכתיבה נשמר, רק לא קורס) ·
חוקה §6 (אין בליעה שקטה — דילוג מתועד).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
בהעלאה דרך "פסיקה-חסרה" (ענף ועדת-ערר), כשטופס 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>
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>
הדריינר רץ בלילה 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>
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>
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>
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>