Root cause (proven 2026-06-30, legal-ai #164): the comment-sweep wakes the CEO
via ctx.agents.invoke, which does NOT populate $PAPERCLIP_WAKE_REASON or
payload.issueId. The run transcript showed "generic heartbeat with no scoped
wake" — the CEO's `user_commented` branch never fired, it ran a generic
heartbeat that scans only CEO-assigned issues, and chair comments on
chaim/sub-agent-owned issues (e.g. CMPA-116) were invisible → no-op. Two real
6-day-stale chair requests sat undelivered.
Fix: on any wake without a scoped reason, the CEO must DISCOVER pending chair
comments itself. Adds "סריקת הערות-יו"ר תלויות (company-wide)":
- enumerate all active company issues (/api/companies/{id}/issues)
- pending = newest user comment newer than newest agent comment (ignore
system); order-independent sort_by(createdAt) since the API is newest-first
- MANDATORY case binding from the STRUCTURED project.name ("ערר <case> — …"),
never from the title (CMPA-116 has none) nor the comment content (which may
name a different case, e.g. "ניסיתי ב-CMPA-89"). No confident case → do NOT
act; escalate to chair.
- handle each pending comment scoped to its own $ISSUE_ID/$CASE_NUMBER
Verified end-to-end against live data: 13 active issues → exactly 2 pending
(CMPA-116, CMPA-89), both resolved to case 8125-09-24 via project.name, zero
false positives. Step 0 + Step A wired to run the scan on blind wakes.
Refs #164. Pairs with the plugin sweep (delivery) — this is discovery+action.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WS7 of the workflow-redesign initiative required closing 7 known leaks along
the way. Cross-code audit verified ALL 7 were already closed in prior cycles
(FU-1/4/5/6/7 + IA waves); the spec still carried stale "known violation" text.
This PR aligns the spec to actual code state and verifies each closure with a
test. No new code path created — this is a spec-alignment audit, not a re-solve.
7 leaks → status (all closed/gated, each with a test):
1. cross-corpus halachot (#56, GAP-10/FU-4) — cl.source_kind in halacha_filters
(db.py:7516,7519); test_precedent_corpus_isolation.py
2. invisible halacha backlog (GAP-14/FU-5) — health halacha_backlog
(app.py:2352-2364) + extraction_status/halachot_pending
3. no eval harness (GAP-11/FU-5, G8) — scripts/eval_retrieval.py
(P/R/MRR/nDCG vs gold-set+baseline); --self-test ALL PASS
4. export gate not hard-block (GAP-15/FU-6, INV-EX3) — export_docx checks
qa_run_exists + get_critical_qa_failures before exporter (drafting.py:462-494);
test_export_qa_gate.py
5. metadata on internal path (GAP-02/FU-1, INV-ING3) — unified ingest.ingest_document
queues metadata+halacha together (ingest.py:233-234); test_unified_ingest.py
6. DOCX creep to source-of-truth (GAP-17/FU-7, INV-EX1) — active_draft_path is
revision-anchor only; drift caught by cases.blocks_stale flag (V22)
7. UI cache-invalidation (GAP-33/FU-10) — qc.invalidateQueries on mutations
Also added forward-pointer notes for the workflow-redesign additions owned by
sibling tasks (02 documents is_primary/doc_category → #200; 04 re-analysis /
summarize_party_claims / analyze_protocol → #200/#202/#203), kept additive to
avoid line conflicts. block-schema.md interim/template edits deferred to #204/#205.
Invariants: maintains G2 (single source of truth, no parallel path — all leak
closures extend the canonical path), G8 (eval harness now exists), G10 (human
gates visible: halacha backlog + hard export gate), INV-ING3 (unified metadata
queue), INV-EX1 (DOCX is derived; blocks_stale drift flag), INV-RET1/G5 (#56
cross-corpus halacha isolation).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
מנגנון-הייצור (claude_session→claude -p) לא העביר model/effort: BLOCK_CONFIG
הגדיר temp+model per-בלוק שהיו מטא-דאטה-מת (block_writer.py:447 העביר רק
prompt/timeout/tools). הייצור רץ על מודל ברירת-המחדל של ה-CLI עם פרומפטים-חופשיים
→ מקור אי-העקביות בטיוטת-הביניים.
תיקון (עקביות-מבנית, WS5):
- נעיצת model="claude-opus-4-8" (GENERATION_MODEL) לכל בלוקי-ה-AI + effort per-בלוק
שמועבר בפועל דרך claude_session.query → `claude -p --model --effort`.
מפת-effort: ה=medium · ו=medium · ז=high · ח=medium · ט=high · יא=high · י=xhigh.
- ניסוח-פתיחה קבוע לבלוק-ה (תמיד "לפנינו ערר…", סוף ל-OR "עניינה של החלטה זו").
- מבנה-פנימי קבוע לבלוק-ה; סדר-בלוקים בייצוא כבר קבוע (_INTERIM_BLOCK_ORDER).
- הסרת שדות temp/model המתים מ-BLOCK_CONFIG; "temp" deprecated (Opus 4.7/4.8 דוחים
temperature→400). decision_blocks.temperature נשמר=0 לתאימות-עמודה בלבד.
- timeout נגזר מ-effort (לא מפיצול sonnet/opus שכבר לא קיים).
- עדכון endpoint reference /api/settings/mcp/blocks + blocks-tab + McpBlock לחשוף effort.
- ספ: docs/block-schema.md §3 (effort מחליף temperature), docs/spec/06-export.md
(דטרמיניזם-מבני של טיוטת-הביניים).
G2 (מקור-אמת יחיד) — אין מסלול-ייצור מקביל: write_interim_draft מתזמר את אותו
write_and_store_block→write_block של ההחלטה המלאה.
claude_session-local-only — הייצור נשאר דרך גשר-ה-CLI, לא SDK/קונטיינר.
INV-EX1 — סדר-ייצוא דטרמיניסטי נשמר; G11 — עקרונות-הכתיבה ל-12 הבלוקים נשמרים.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Belt-and-suspenders to the plugin sweep (plugin-legal-ai #3). Comments that
arrive mid-run aren't re-read automatically — an agent reads comments once at
run start (§2), so a chair comment that lands after that point and before the
run closes the issue is never seen. That is exactly how a chair comment on
CMP-190 (2026-06-30) was dropped.
Adds a mandatory gate to HEARTBEAT §4ב: before PATCH status=done, re-read
comments and compare to the run-start list. If a new unaddressed chair comment
exists — do not close. Either handle it (and record a comment) or, if it
widens scope, keep the issue open and hand back to the CEO (§4ג). Never mark
done while a chair comment waits unanswered.
The plugin sweep catches orphaned comments within ~2 min; this gate prevents
the drop at the source. Refs legal-ai #164.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chair (Dafna/chaim) reviewed and approved the full corpus-refresh proposal
generated by scripts/regenerate_voice_fingerprint.py and committed it to the live
voice-fingerprint — satisfying the INV-LRN1 hard chair-gate (a human approved;
the script only ever proposed). +81/-15 lines: all the deletions are reword-and-
expand of existing lines (no curated insight lost), the additions are style/method
refinements grounded in the 56-decision corpus (INV-LRN5 — no case substance):
- §1.2 conditional-acceptance as an "anחנו" verb (מקובל עלינו vs נציין)
- §1.3 ruling variation "ראשית, צודק [צד]... אלא ש"
- §1.5 emphasis always annotated "(הדגשת הח\"מ)"
- §1.8 bounded appraiser-restraint (professional plane only; legal plane = no deference)
- §1.9 inverse twin "ניתן היה לסיים... אלא ש"
- §1.11 quantitative anchoring as a reframing device (cross-type)
- §1.12 measured block ratios (background 17 / claims 27 / discussion 42 / summary 14)
- §1.13 faithful rendering of a party's internal reasoning
- §2.2 two-sided pendulum for the restraint paragraph + expanded party-verb catalog
- §4.8 new template "ניתן היה לסיים — אלא ש"
- §5.x agent instructions updated to reference the new patterns
Invariants: INV-LRN1 (chair approved before commit — the gate held), INV-LRN5
(style/method only, no substance).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Golden ratios were measured from the corpus (style_distance.measure_corpus_ratios)
but never reached the writer — it used only the hardcoded STRUCTURE_GUIDANCE text.
So the structural target was frozen, and drafts kept deviating (e.g. discussion
block 10% vs a 40-47% target) with no corrective signal.
_structure_guidance_with_corpus(outcome, practice_area) now appends the
corpus-measured section ratios for the block's outcome to the structure guidance,
at both writer entry points (write_block + get_block_context) via one shared helper
(G2). Because measure_corpus_ratios reads style_corpus — which grows with every
enrolled final (see exemplar-growth, #343) — the writer's structural target now
reflects Dafna's ACTUAL current distribution and improves as the corpus grows.
Deterministic (no LLM); best-effort.
This is the STRUCTURAL half of the voice-profile-refresh (#7). The remaining half —
LLM-regenerating the abstract voice-fingerprint PROSE from the corpus (chair-gated)
— is a distinct larger initiative, deliberately deferred rather than rushed.
Invariants: G1 (the writer gets the live measured target, not a frozen constant),
G2 (one structure-guidance builder shared by both entry points; reuses the existing
measure_corpus_ratios — no parallel measurement), INV-LRN4 (the corpus the loop
grows now feeds the writer's structural target).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chair-feedback chain was DEAD: 27 feedback rows captured, 0 ever became a
lesson the writer reads (no weekly-analysis job ever existed). The chair's own
corrections — the most authoritative signal of all, and exactly "learn from every
returned draft" — went nowhere.
decision_lessons can't carry them: it's FK-coupled to a style_corpus row (a signed
final), which a case-in-progress doesn't have. So chair STYLE feedback instead
rides the discussion_rules channel that already reaches the writer for all blocks —
the same path /training promote uses.
- db.append_global_rule: the locked read-modify-write append, extracted from web
`_append_methodology_override` into one shared impl (G2). The web function is now
a thin wrapper that seeds defaults; chair-feedback calls it directly.
- record_chair_feedback (MCP tool): a STYLE-category correction (style/wrong_tone/
wrong_structure) with a lesson_extracted flows immediately to discussion_rules.
The chair IS the gate — no separate approval (INV-LRN1 graduated gate). SUBSTANCE
feedback (missing_content/factual_error/other) is case-specific → recorded only.
Invariants: INV-LRN1 (chair-authored style = highest authority, flows; substance
not auto-flowed), G2 (single append impl shared by promote + feedback), INV-LRN5
(style channel only).
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>
The learning loop measured anti-patterns but never corrected them. style_distance
detects markdown headers / bullet lists / mid-paragraph mini-lists (from the
canonical lessons.ANTI_PATTERNS), but the writer only received anti-pattern
guidance if a chair `anti_patterns` override existed in appeal_type_rules — and
none does. So _build_style_context's `if ov:` branch injected nothing, the writer
was never told to avoid them, and drafts kept emitting them (8137: 28 hits, the
worst, newest — anti-patterns were trending UP, not down).
Anti-patterns are structural invariants of Dafna's voice (continuous legal
narrative — no markdown, no bullets), not overridable preferences. So inject the
canonical ANTI_PATTERNS notes ALWAYS, from the same list style_distance measures
against (single source of truth), with any chair additions layered on top. This
closes the measure-but-don't-correct gap: the next draft should show the markdown/
bullet anti-patterns drop, and Path A (style_distance_history) will confirm it.
Invariants: G1 (correct at source — the writer, not a post-hoc stripper), G2
(one canonical anti-pattern list shared by detection and instruction — no parallel
list), INV-LRN4 (closes the feedback half of the draft↔final loop).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
תיקוני-מבנה ללולאת-הלמידה (TaskMaster #157):
1. כרטיס ה-curator (/training טאב "אוצֵר") ספר decision_lessons WHERE source='curator'
— ערך שאף מסלול-קוד לא כתב → תמיד 0. עוצב-מחדש (דרך שער-העיצוב Claude Design, אושר)
להציג ביושר את שלושת ערוצי-ההזנה לכותב: דיסטילציה→appeal_type_rules (180, זורם),
פאנל→decision_lessons (81, ממתין), אוצֵר→source='curator'. get_curator_stats שוכתב.
2. drift ספ↔סוכן: 07-learning.md §1.1 + INV-LRN3 קבעו שהאוצֵר רושם ממצאים כ-decision_lesson
source='curator', אך הסוכן כתב comments בלבד — הממצאים האיכותיים אבדו. נוסף כלי-MCP
record_curator_findings + §A.5b ב-hermes-curator.md (read-only נשמר; הצעה מגודרת-שער).
3. get_recent_decision_lessons(limit=15) חתך בשקט — נוסף WARN על מה שנחתך (חוקה §6);
הפתרון האמיתי = סינתזת-לקחים (TaskMaster #158).
Invariants: מקיים INV-LRN1/G10 (שער-יו"ר), INV-LRN3 (לכידה מובנית), INV-IA2/IA5 (מקור-אמת
יחיד), G12 (leak-guard עובר), G2 (אין מסלול מקביל — אותם מאגרים). פער-מימוש פתוח מתועד:
§A לא רץ אוטומטית על mark-final (pipeline-wake exits) → נדחה לתכנון נפרד.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents the prospective held-out methodology shipped in PR #337: why a clean
retrospective held-out is impossible (lessons stored universal/untagged → no
leave-one-out), and how the system instead captures a clean generalization
datapoint at final-upload — before this case's lessons are folded — via a
style_distance snapshot + lesson-pool size into style_distance_history,
surfaced by GET /api/learning/style-distance-history.
Adds §0.7, wires step [7] MEASUREMENT and INV-LRN4 to it. Notes the
change_percent style/content confound and the one-time clean window.
Invariants documented: INV-LRN4 (this is its trend surface), G2 (reuses
style_distance + appeal_type_rules — no parallel metric path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A clean held-out test of voice learning was no longer runnable: every
final-uploaded case already has its lessons folded, and lessons are stored
universal/untagged so leave-one-out is impossible. Path A makes the test
prospective instead — capture the generalization datapoint at the one moment
it's clean.
On final upload, after the draft↔final pair is created but BEFORE this
case's lessons are folded (folding is a separate manual /training step), we
snapshot style_distance (anti_pattern_total, golden-ratio max-deviation,
change_percent) alongside the current voice-lesson pool size. Because the
draft was written with only the PRIOR pool, each row is a clean "with N
accumulated lessons, our draft on this unseen case scored X" datapoint. As
the pool grows over cases, a downward trend = learning generalizes.
- db: SCHEMA_V45 style_distance_history (append-only) + helpers
voice_lesson_pool_sizes / record_style_distance_snapshot /
get_style_distance_history.
- app: best-effort capture in api_upload_final_decision (never fails the
upload); GET /api/learning/style-distance-history for the trend.
Reuses the existing style_distance service + appeal_type_rules pool — no
parallel metric path. The 8 existing cases are already folded, so the table
starts empty and fills from the next final (their clean window is past).
Invariants: G2 (reuse style_distance/appeal_type_rules — one path),
INV-LRN4 (measure the draft↔final gap; this is its trend surface). LLM-free
(style_distance is deterministic) so it runs in the container.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>