diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..60bd23e
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,12 @@
+# API Keys (Required to enable respective provider)
+ANTHROPIC_API_KEY="your_anthropic_api_key_here" # Required: Format: sk-ant-api03-...
+PERPLEXITY_API_KEY="your_perplexity_api_key_here" # Optional: Format: pplx-...
+OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI models. Format: sk-proj-...
+GOOGLE_API_KEY="your_google_api_key_here" # Optional, for Google Gemini models.
+MISTRAL_API_KEY="your_mistral_key_here" # Optional, for Mistral AI models.
+XAI_API_KEY="YOUR_XAI_KEY_HERE" # Optional, for xAI AI models.
+GROQ_API_KEY="YOUR_GROQ_KEY_HERE" # Optional, for Groq models.
+OPENROUTER_API_KEY="YOUR_OPENROUTER_KEY_HERE" # Optional, for OpenRouter models.
+AZURE_OPENAI_API_KEY="your_azure_key_here" # Optional, for Azure OpenAI models (requires endpoint in .taskmaster/config.json).
+OLLAMA_API_KEY="your_ollama_api_key_here" # Optional: For remote Ollama servers that require authentication.
+GITHUB_API_KEY="your_github_api_key_here" # Optional: For GitHub import/export features. Format: ghp_... or github_pat_...
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 84a79f7..05348be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,7 +6,8 @@ data/backups/
data/precedent-library/
data/.auto-sync.log
data/*.db
-data/checkpoints/ # X16 durable-pipeline SQLite checkpoints (runtime artifact)
+# X16 durable-pipeline SQLite checkpoints (runtime artifact)
+data/checkpoints/
*.bak-pre-*
mcp-server/.venv/
__pycache__/
@@ -18,6 +19,30 @@ kiryat-yearim/
continuation-prompt.md
node_modules/
data/eval/eval-report-*
-data/adapter-migration-state.json # revert snapshot for migrate_agent_adapter.py (runtime state)
-.claude/agents/.generated/ # frontmatter-stripped instruction copies for content_arg adapters (generated)
+# revert snapshot for migrate_agent_adapter.py (runtime state)
+data/adapter-migration-state.json
+# frontmatter-stripped instruction copies for content_arg adapters (generated)
+.claude/agents/.generated/
.claude/worktrees/
+
+# TaskMaster backups (runtime)
+.taskmaster/tasks/tasks.json.bak.*
+
+# Build artifacts
+.design-build/
+
+# Temp files
+.interaction_tmp.json
+
+# Runtime eval/ab-test data
+data/ab_halacha_*.json
+data/ab_run_*.log
+data/x11_treatment_run_*.log
+
+# Runtime data directories
+data/audit/
+data/bulletins/
+data/digests/
+data/internal-decisions/
+data/learning/
+data/logs/
diff --git a/.taskmaster/templates/example_prd.txt b/.taskmaster/templates/example_prd.txt
new file mode 100644
index 0000000..194114d
--- /dev/null
+++ b/.taskmaster/templates/example_prd.txt
@@ -0,0 +1,47 @@
+
+# Overview
+[Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.]
+
+# Core Features
+[List and describe the main features of your product. For each feature, include:
+- What it does
+- Why it's important
+- How it works at a high level]
+
+# User Experience
+[Describe the user journey and experience. Include:
+- User personas
+- Key user flows
+- UI/UX considerations]
+
+
+# Technical Architecture
+[Outline the technical implementation details:
+- System components
+- Data models
+- APIs and integrations
+- Infrastructure requirements]
+
+# Development Roadmap
+[Break down the development process into phases:
+- MVP requirements
+- Future enhancements
+- Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks]
+
+# Logical Dependency Chain
+[Define the logical order of development:
+- Which features need to be built first (foundation)
+- Getting as quickly as possible to something usable/visible front end that works
+- Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches]
+
+# Risks and Mitigations
+[Identify potential risks and how they'll be addressed:
+- Technical challenges
+- Figuring out the MVP that we can build upon
+- Resource constraints]
+
+# Appendix
+[Include any additional information:
+- Research findings
+- Technical specifications]
+
\ No newline at end of file
diff --git a/.taskmaster/templates/example_prd_rpg.txt b/.taskmaster/templates/example_prd_rpg.txt
new file mode 100644
index 0000000..5ad908f
--- /dev/null
+++ b/.taskmaster/templates/example_prd_rpg.txt
@@ -0,0 +1,511 @@
+
+# Repository Planning Graph (RPG) Method - PRD Template
+
+This template teaches you (AI or human) how to create structured, dependency-aware PRDs using the RPG methodology from Microsoft Research. The key insight: separate WHAT (functional) from HOW (structural), then connect them with explicit dependencies.
+
+## Core Principles
+
+1. **Dual-Semantics**: Think functional (capabilities) AND structural (code organization) separately, then map them
+2. **Explicit Dependencies**: Never assume - always state what depends on what
+3. **Topological Order**: Build foundation first, then layers on top
+4. **Progressive Refinement**: Start broad, refine iteratively
+
+## How to Use This Template
+
+- Follow the instructions in each `` block
+- Look at `` blocks to see good vs bad patterns
+- Fill in the content sections with your project details
+- The AI reading this will learn the RPG method by following along
+- Task Master will parse the resulting PRD into dependency-aware tasks
+
+## Recommended Tools for Creating PRDs
+
+When using this template to **create** a PRD (not parse it), use **code-context-aware AI assistants** for best results:
+
+**Why?** The AI needs to understand your existing codebase to make good architectural decisions about modules, dependencies, and integration points.
+
+**Recommended tools:**
+- **Claude Code** (claude-code CLI) - Best for structured reasoning and large contexts
+- **Cursor/Windsurf** - IDE integration with full codebase context
+- **Gemini CLI** (gemini-cli) - Massive context window for large codebases
+- **Codex/Grok CLI** - Strong code generation with context awareness
+
+**Note:** Once your PRD is created, `task-master parse-prd` works with any configured AI model - it just needs to read the PRD text itself, not your codebase.
+
+
+---
+
+
+
+Start with the problem, not the solution. Be specific about:
+- What pain point exists?
+- Who experiences it?
+- Why existing solutions don't work?
+- What success looks like (measurable outcomes)?
+
+Keep this section focused - don't jump into implementation details yet.
+
+
+## Problem Statement
+[Describe the core problem. Be concrete about user pain points.]
+
+## Target Users
+[Define personas, their workflows, and what they're trying to achieve.]
+
+## Success Metrics
+[Quantifiable outcomes. Examples: "80% task completion via autopilot", "< 5% manual intervention rate"]
+
+
+
+---
+
+
+
+Now think about CAPABILITIES (what the system DOES), not code structure yet.
+
+Step 1: Identify high-level capability domains
+- Think: "What major things does this system do?"
+- Examples: Data Management, Core Processing, Presentation Layer
+
+Step 2: For each capability, enumerate specific features
+- Use explore-exploit strategy:
+ * Exploit: What features are REQUIRED for core value?
+ * Explore: What features make this domain COMPLETE?
+
+Step 3: For each feature, define:
+- Description: What it does in one sentence
+- Inputs: What data/context it needs
+- Outputs: What it produces/returns
+- Behavior: Key logic or transformations
+
+
+Capability: Data Validation
+ Feature: Schema validation
+ - Description: Validate JSON payloads against defined schemas
+ - Inputs: JSON object, schema definition
+ - Outputs: Validation result (pass/fail) + error details
+ - Behavior: Iterate fields, check types, enforce constraints
+
+ Feature: Business rule validation
+ - Description: Apply domain-specific validation rules
+ - Inputs: Validated data object, rule set
+ - Outputs: Boolean + list of violated rules
+ - Behavior: Execute rules sequentially, short-circuit on failure
+
+
+
+Capability: validation.js
+ (Problem: This is a FILE, not a CAPABILITY. Mixing structure into functional thinking.)
+
+Capability: Validation
+ Feature: Make sure data is good
+ (Problem: Too vague. No inputs/outputs. Not actionable.)
+
+
+
+## Capability Tree
+
+### Capability: [Name]
+[Brief description of what this capability domain covers]
+
+#### Feature: [Name]
+- **Description**: [One sentence]
+- **Inputs**: [What it needs]
+- **Outputs**: [What it produces]
+- **Behavior**: [Key logic]
+
+#### Feature: [Name]
+- **Description**:
+- **Inputs**:
+- **Outputs**:
+- **Behavior**:
+
+### Capability: [Name]
+...
+
+
+
+---
+
+
+
+NOW think about code organization. Map capabilities to actual file/folder structure.
+
+Rules:
+1. Each capability maps to a module (folder or file)
+2. Features within a capability map to functions/classes
+3. Use clear module boundaries - each module has ONE responsibility
+4. Define what each module exports (public interface)
+
+The goal: Create a clear mapping between "what it does" (functional) and "where it lives" (structural).
+
+
+Capability: Data Validation
+ → Maps to: src/validation/
+ ├── schema-validator.js (Schema validation feature)
+ ├── rule-validator.js (Business rule validation feature)
+ └── index.js (Public exports)
+
+Exports:
+ - validateSchema(data, schema)
+ - validateRules(data, rules)
+
+
+
+Capability: Data Validation
+ → Maps to: src/utils.js
+ (Problem: "utils" is not a clear module boundary. Where do I find validation logic?)
+
+Capability: Data Validation
+ → Maps to: src/validation/everything.js
+ (Problem: One giant file. Features should map to separate files for maintainability.)
+
+
+
+## Repository Structure
+
+```
+project-root/
+├── src/
+│ ├── [module-name]/ # Maps to: [Capability Name]
+│ │ ├── [file].js # Maps to: [Feature Name]
+│ │ └── index.js # Public exports
+│ └── [module-name]/
+├── tests/
+└── docs/
+```
+
+## Module Definitions
+
+### Module: [Name]
+- **Maps to capability**: [Capability from functional decomposition]
+- **Responsibility**: [Single clear purpose]
+- **File structure**:
+ ```
+ module-name/
+ ├── feature1.js
+ ├── feature2.js
+ └── index.js
+ ```
+- **Exports**:
+ - `functionName()` - [what it does]
+ - `ClassName` - [what it does]
+
+
+
+---
+
+
+
+This is THE CRITICAL SECTION for Task Master parsing.
+
+Define explicit dependencies between modules. This creates the topological order for task execution.
+
+Rules:
+1. List modules in dependency order (foundation first)
+2. For each module, state what it depends on
+3. Foundation modules should have NO dependencies
+4. Every non-foundation module should depend on at least one other module
+5. Think: "What must EXIST before I can build this module?"
+
+
+Foundation Layer (no dependencies):
+ - error-handling: No dependencies
+ - config-manager: No dependencies
+ - base-types: No dependencies
+
+Data Layer:
+ - schema-validator: Depends on [base-types, error-handling]
+ - data-ingestion: Depends on [schema-validator, config-manager]
+
+Core Layer:
+ - algorithm-engine: Depends on [base-types, error-handling]
+ - pipeline-orchestrator: Depends on [algorithm-engine, data-ingestion]
+
+
+
+- validation: Depends on API
+- API: Depends on validation
+(Problem: Circular dependency. This will cause build/runtime issues.)
+
+- user-auth: Depends on everything
+(Problem: Too many dependencies. Should be more focused.)
+
+
+
+## Dependency Chain
+
+### Foundation Layer (Phase 0)
+No dependencies - these are built first.
+
+- **[Module Name]**: [What it provides]
+- **[Module Name]**: [What it provides]
+
+### [Layer Name] (Phase 1)
+- **[Module Name]**: Depends on [[module-from-phase-0], [module-from-phase-0]]
+- **[Module Name]**: Depends on [[module-from-phase-0]]
+
+### [Layer Name] (Phase 2)
+- **[Module Name]**: Depends on [[module-from-phase-1], [module-from-foundation]]
+
+[Continue building up layers...]
+
+
+
+---
+
+
+
+Turn the dependency graph into concrete development phases.
+
+Each phase should:
+1. Have clear entry criteria (what must exist before starting)
+2. Contain tasks that can be parallelized (no inter-dependencies within phase)
+3. Have clear exit criteria (how do we know phase is complete?)
+4. Build toward something USABLE (not just infrastructure)
+
+Phase ordering follows topological sort of dependency graph.
+
+
+Phase 0: Foundation
+ Entry: Clean repository
+ Tasks:
+ - Implement error handling utilities
+ - Create base type definitions
+ - Setup configuration system
+ Exit: Other modules can import foundation without errors
+
+Phase 1: Data Layer
+ Entry: Phase 0 complete
+ Tasks:
+ - Implement schema validator (uses: base types, error handling)
+ - Build data ingestion pipeline (uses: validator, config)
+ Exit: End-to-end data flow from input to validated output
+
+
+
+Phase 1: Build Everything
+ Tasks:
+ - API
+ - Database
+ - UI
+ - Tests
+ (Problem: No clear focus. Too broad. Dependencies not considered.)
+
+
+
+## Development Phases
+
+### Phase 0: [Foundation Name]
+**Goal**: [What foundational capability this establishes]
+
+**Entry Criteria**: [What must be true before starting]
+
+**Tasks**:
+- [ ] [Task name] (depends on: [none or list])
+ - Acceptance criteria: [How we know it's done]
+ - Test strategy: [What tests prove it works]
+
+- [ ] [Task name] (depends on: [none or list])
+
+**Exit Criteria**: [Observable outcome that proves phase complete]
+
+**Delivers**: [What can users/developers do after this phase?]
+
+---
+
+### Phase 1: [Layer Name]
+**Goal**:
+
+**Entry Criteria**: Phase 0 complete
+
+**Tasks**:
+- [ ] [Task name] (depends on: [[tasks-from-phase-0]])
+- [ ] [Task name] (depends on: [[tasks-from-phase-0]])
+
+**Exit Criteria**:
+
+**Delivers**:
+
+---
+
+[Continue with more phases...]
+
+
+
+---
+
+
+
+Define how testing will be integrated throughout development (TDD approach).
+
+Specify:
+1. Test pyramid ratios (unit vs integration vs e2e)
+2. Coverage requirements
+3. Critical test scenarios
+4. Test generation guidelines for Surgical Test Generator
+
+This section guides the AI when generating tests during the RED phase of TDD.
+
+
+Critical Test Scenarios for Data Validation module:
+ - Happy path: Valid data passes all checks
+ - Edge cases: Empty strings, null values, boundary numbers
+ - Error cases: Invalid types, missing required fields
+ - Integration: Validator works with ingestion pipeline
+
+
+
+## Test Pyramid
+
+```
+ /\
+ /E2E\ ← [X]% (End-to-end, slow, comprehensive)
+ /------\
+ /Integration\ ← [Y]% (Module interactions)
+ /------------\
+ / Unit Tests \ ← [Z]% (Fast, isolated, deterministic)
+ /----------------\
+```
+
+## Coverage Requirements
+- Line coverage: [X]% minimum
+- Branch coverage: [X]% minimum
+- Function coverage: [X]% minimum
+- Statement coverage: [X]% minimum
+
+## Critical Test Scenarios
+
+### [Module/Feature Name]
+**Happy path**:
+- [Scenario description]
+- Expected: [What should happen]
+
+**Edge cases**:
+- [Scenario description]
+- Expected: [What should happen]
+
+**Error cases**:
+- [Scenario description]
+- Expected: [How system handles failure]
+
+**Integration points**:
+- [What interactions to test]
+- Expected: [End-to-end behavior]
+
+## Test Generation Guidelines
+[Specific instructions for Surgical Test Generator about what to focus on, what patterns to follow, project-specific test conventions]
+
+
+
+---
+
+
+
+Describe technical architecture, data models, and key design decisions.
+
+Keep this section AFTER functional/structural decomposition - implementation details come after understanding structure.
+
+
+## System Components
+[Major architectural pieces and their responsibilities]
+
+## Data Models
+[Core data structures, schemas, database design]
+
+## Technology Stack
+[Languages, frameworks, key libraries]
+
+**Decision: [Technology/Pattern]**
+- **Rationale**: [Why chosen]
+- **Trade-offs**: [What we're giving up]
+- **Alternatives considered**: [What else we looked at]
+
+
+
+---
+
+
+
+Identify risks that could derail development and how to mitigate them.
+
+Categories:
+- Technical risks (complexity, unknowns)
+- Dependency risks (blocking issues)
+- Scope risks (creep, underestimation)
+
+
+## Technical Risks
+**Risk**: [Description]
+- **Impact**: [High/Medium/Low - effect on project]
+- **Likelihood**: [High/Medium/Low]
+- **Mitigation**: [How to address]
+- **Fallback**: [Plan B if mitigation fails]
+
+## Dependency Risks
+[External dependencies, blocking issues]
+
+## Scope Risks
+[Scope creep, underestimation, unclear requirements]
+
+
+
+---
+
+
+## References
+[Papers, documentation, similar systems]
+
+## Glossary
+[Domain-specific terms]
+
+## Open Questions
+[Things to resolve during development]
+
+
+---
+
+
+# How Task Master Uses This PRD
+
+When you run `task-master parse-prd .txt`, the parser:
+
+1. **Extracts capabilities** → Main tasks
+ - Each `### Capability:` becomes a top-level task
+
+2. **Extracts features** → Subtasks
+ - Each `#### Feature:` becomes a subtask under its capability
+
+3. **Parses dependencies** → Task dependencies
+ - `Depends on: [X, Y]` sets task.dependencies = ["X", "Y"]
+
+4. **Orders by phases** → Task priorities
+ - Phase 0 tasks = highest priority
+ - Phase N tasks = lower priority, properly sequenced
+
+5. **Uses test strategy** → Test generation context
+ - Feeds test scenarios to Surgical Test Generator during implementation
+
+**Result**: A dependency-aware task graph that can be executed in topological order.
+
+## Why RPG Structure Matters
+
+Traditional flat PRDs lead to:
+- ❌ Unclear task dependencies
+- ❌ Arbitrary task ordering
+- ❌ Circular dependencies discovered late
+- ❌ Poorly scoped tasks
+
+RPG-structured PRDs provide:
+- ✅ Explicit dependency chains
+- ✅ Topological execution order
+- ✅ Clear module boundaries
+- ✅ Validated task graph before implementation
+
+## Tips for Best Results
+
+1. **Spend time on dependency graph** - This is the most valuable section for Task Master
+2. **Keep features atomic** - Each feature should be independently testable
+3. **Progressive refinement** - Start broad, use `task-master expand` to break down complex tasks
+4. **Use research mode** - `task-master parse-prd --research` leverages AI for better task generation
+
diff --git a/data/halacha_night_check.sh b/data/halacha_night_check.sh
new file mode 100755
index 0000000..ca2059d
--- /dev/null
+++ b/data/halacha_night_check.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+# One-shot morning verdict for the halacha night drain (scheduled 2026-06-15 04:30 UTC
+# via chaim's crontab; throwaway — lives under data/, not tracked in scripts/).
+# Captures whether last night's run (with the PR #251 fix: durable rate-limit
+# detection + 05:00–07:00 catch-up window) actually drained the backlog.
+# Baseline at install time (2026-06-14 13:xx IDT): pending=96 done=248 halachot=4099.
+set -u
+export HOME=/home/chaim
+REPO=/home/chaim/legal-ai
+PY="$REPO/mcp-server/.venv/bin/python"
+OUT="$REPO/data/logs/halacha_night_report_$(TZ=Asia/Jerusalem date +%Y%m%d).md"
+SUP_LOG=/home/chaim/.pm2/logs/legal-halacha-supervisor-out.log
+DRAIN_ERR=/home/chaim/.pm2/logs/legal-halacha-drain-error.log
+mkdir -p "$REPO/data/logs"
+
+{
+ echo "# דוח-בוקר: ריצת-הלכות הלילה — $(TZ=Asia/Jerusalem date '+%Y-%m-%d %H:%M %Z')"
+ echo
+ echo "בסיס-השוואה (אתמול 13:xx IDT): pending=96 · done=248 · halachot=4099"
+ echo
+ echo '## מצב נוכחי (supervisor status)'
+ echo '```'
+ cd "$REPO" && "$PY" scripts/halacha_drain_supervisor.py status 2>&1
+ echo '```'
+ echo
+ echo '## פעולות המתזמר ב-12 השעות האחרונות (modes/actions)'
+ echo '```'
+ grep -E 'מצב:|פעולה:|catch-up|rate-limit|נעצר' "$SUP_LOG" 2>/dev/null | tail -40
+ echo '```'
+ echo
+ echo '## אותות rate-limit בלוג-הדריינר (24ש אחרונות בלוג)'
+ echo '```'
+ echo "429 hits (tail 4000): $(tail -4000 "$DRAIN_ERR" 2>/dev/null | grep -c '429')"
+ echo "session-limit msgs: $(tail -4000 "$DRAIN_ERR" 2>/dev/null | grep -c 'hit your session limit')"
+ echo "extraction_failed: $(tail -4000 "$DRAIN_ERR" 2>/dev/null | grep -c 'extraction_failed')"
+ echo "hold-stopped (fix A): $(grep -c 'hold-stopped' "$SUP_LOG" 2>/dev/null)"
+ echo "catch-up opened (B): $(grep -c 'catch-up בוקר' "$SUP_LOG" 2>/dev/null)"
+ echo '```'
+ echo
+ echo "_(נוצר ע\"י data/halacha_night_check.sh; ניתן למחוק את שורת ה-crontab של 15.6.)_"
+} > "$OUT" 2>&1
+
+echo "report written: $OUT"
diff --git a/docs/precedent-corpus-redesign/01-claude-architecture-data-audit.md b/docs/precedent-corpus-redesign/01-claude-architecture-data-audit.md
new file mode 100644
index 0000000..c097281
--- /dev/null
+++ b/docs/precedent-corpus-redesign/01-claude-architecture-data-audit.md
@@ -0,0 +1,170 @@
+# ממצאי ביקורת — ארכיטקטורת קורפוס־הפסיקה + מצב הדאטה בפועל
+
+> **מקור:** Claude (Opus 4.8) · **תאריך:** 2026-06-20 · **קונטקסט:** חקירה לקראת תכנון־מחדש של קורפוס־הפסיקה.
+> מסמך זה הוא **אחד מכמה** קלטי־סוכנים שחיים אוסף; ייעודו להזין את שלב־הסינתזה. אינו תכנית — הוא **אבחון**.
+>
+> **שאלת־המוצא של חיים:** "הקורפוס נבנה מראש לא נכון, אני כל הזמן מתעסק בתיקונים. האם כדאי ליצור מחדש את קורפוס־הפסיקה ולהתחיל דף נקי?"
+>
+> **המודל הרצוי (כפי שחיים תיאר אותו):** מאגר פסקי־דין והחלטות ועדות־ערר; חוקר־התקדימים מזהה בשלב ניתוח־הערר פס"ד/החלטות שדנו במקרה דומה או הלכה דומה; הסוכן־הכותב משייך ומזכיר אותם בפרק הדיון וההכרעה בסגנון דפנה. שלושה מקורות־הזנה: (1) החלטות דפנה עצמה, (2) ועדות־ערר אחרות שמצטטות פסיקה, (3) פס"ד עליון/מחוזי.
+
+---
+
+## 0. תקציר־מנהלים (TL;DR)
+
+**מה ש"בנוי לא נכון" אינו הסכמה — היא שכבת־הביצוע.** הסכמה כבר תואמת בדיוק את המודל שחיים תיאר: טבלה אחת (`case_law`) ששלושת המקורות נכנסים אליה דרך `source_kind`/`source_type`; שלושת ה־`search_*` הם שלושה *מסננים* על אותו מקור, לא שלושה מאגרים מקבילים (G2 מקוים ברמת־הסכמה). **רֵבילד של הסכמה ייצר בדיוק את אותה סכמה** — ולכן אינו פותר דבר, ומסכן ב־second-system syndrome.
+
+מה שכן מחולל את "התיקונים האינסופיים" — שלושה כשלי־ביצוע מדידים:
+1. **חוזה־קליטה רופף** → 66% מהפסיקה בלי `practice_area`, 31 רשומות ריקות, אכיפת־שלמות (INV-DM1) מופרת בפועל.
+2. **צינור הלכות→קנוני מייצר רעש** → 5,472 קנוני, מתוכם 5,456 סינגלטונים, **0 published** → השכבה שאמורה להזין את הכותב (INV-G10) **אינרטית לגמרי**.
+3. **כפילות `style_corpus`** → 55 החלטות דפנה חיות בשני נתיבי־אחזור.
+
+**המלצה:** לא לשרוף את הסכמה. כן לבצע "איפוס שכבות־נגזרות" צר (truncate ל-chunks+halachot+canonical והרצה־מחדש), **אבל רק אחרי תיקון החוזה והסף** — אחרת מחזירים את אותו בלגן (G1: תיקון במקור, לא בקריאה). מסמכי־המקור (363 רשומות, 332 עם full_text) נשמרים; הם יקרים ו/או ניתנים לקליטה־מחדש מ־PDF.
+
+---
+
+## 1. מצב הדאטה בפועל (שאילתות חיות מול legal_ai @ localhost:5433, 2026-06-20)
+
+```
+┌─────────────────────────────────────┬─────────┬──────────────────────────────────┐
+│ Metric │ Count │ Reading │
+├─────────────────────────────────────┼─────────┼──────────────────────────────────┤
+│ case_law (total precedents) │ 363 │ קטן — re-ingestable │
+│ • external_upload (court rulings) │ 240 │ מקור (3) — עליון/מחוזי │
+│ • internal_committee (ועדות ערר) │ 92 │ מקור (1)+(2) │
+│ • (יתר — ללא source_kind מובהק) │ 31 │ = הרשומות הריקות (ראה למטה) │
+│ style_corpus (החלטות דפנה) │ 55 │ כפילות עם internal_committee │
+│ precedent_chunks │ 11,904 │ נגזר — מתחדש מ-full_text │
+│ halachot (total) │ 5,489 │ נגזר │
+│ • approved/published │ 1,352 │ 25% בלבד │
+│ • pending_review (backlog ידני) │ 2,402 │ 44% — צוואר־בקבוק │
+│ canonical_halachot (V41) │ 5,472 │ כמעט 1:1 עם halachot ⚠️ │
+│ • singletons (instance_count=1) │ 5,456 │ דה־דופ כמעט לא קרה │
+│ • merged (instance_count>=2) │ 16 │ 0.3% מיזוג │
+│ • published (מגיע לסוכן הכותב) │ 0 │ ⚠️ השכבה אינרטית לחלוטין │
+│ case_law w/o practice_area │ 240 │ 66% — חופף-בדיוק לפסיקה החיצונית │
+│ case_law missing summary │ 27 │ │
+│ case_law w/ 0 chunks / no full_text │ 31 │ רשומות שבורות/ריקות │
+│ distinct practice_area │ 4 │ rishuy/betterment/197/(ריק) │
+└─────────────────────────────────────┴─────────┴──────────────────────────────────┘
+
+practice_area breakdown:
+ (ריק) 240 ← כל הפסיקה החיצונית ללא סיווג
+ rishuy_uvniya 70
+ betterment_levy 50
+ compensation_197 3
+```
+
+**קריאות מפתח:**
+- **240 = 240:** מספר הפסיקה־החיצונית שווה־בדיוק למספר חסרי־`practice_area`. כלומר אף פס"ד חיצוני לא סווג לתחום — סינון לפי תחום באחזור פשוט לא עובד עליהם.
+- **5,456 / 5,472 סינגלטונים:** מנוע הקנוניזציה (V41) רץ אך לא מאחד. סף 0.85 כנראה הדוק מדי, או שהחילוץ מנסח כל הלכה ייחודית מספיק כדי לא להתלכד.
+- **0 published canonical:** לפי INV-G10 רק קנוני `published` מגיע לכתיבה. אפס. **כל מנגנון V41 כרגע מנותק מהכתיבה בפועל.**
+- **2,402 pending_review:** צוואר־הבקבוק הוא אישור־אנושי ידני, לא טכנולוגיה.
+
+---
+
+## 2. מפת הארכיטקטורה — קורפוס אחד, שלושה מסננים
+
+**פיזית: טבלה אחת.** כל שלושת המקורות מתכנסים ל-`case_law`, מתחתיה `precedent_chunks` (FK) ו-`halachot` (FK), ומעל ה-`halachot` שכבת `canonical_halachot` (V41).
+
+```
+ canonical_halachot (עקרונות מאוחדים — V41; כיום אינרטי, 0 published)
+ ▲ 1:many
+ halachot (מופע-להלכה per precedent; review_status gate)
+ ▲ FK
+ case_law (רישום מרכזי — court rulings + ועדות-ערר)
+ ├─ source_kind='external_upload' → פס"ד עליון/מחוזי [מקור 3]
+ └─ source_kind='internal_committee'→ ועדות-ערר + דפנה [מקור 1+2]
+ ▼ FK
+ precedent_chunks (chunks + embedding vector(1024))
+
+ בנפרד:
+ style_corpus (55 החלטות דפנה — נתיב-אחזור מקביל ל-search_decisions)
+ document_chunks (מסמכי-תיק + style; FK→documents→cases)
+```
+
+**שלושת נתיבי־האחזור (לא שלושה מאגרים — שלושה scopes):**
+
+```
+┌──────────────────────────────┬─────────────────────────┬────────────────────────────┐
+│ Tool │ Table / filter │ Purpose │
+├──────────────────────────────┼─────────────────────────┼────────────────────────────┤
+│ search_decisions │ document_chunks │ סגנון/קול: החלטות דפנה │
+│ │ (scoped case/area) │ + מסמכי-תיק │
+│ search_precedent_library │ case_law + chunks/halach │ source_kind=external_upload│
+│ │ ot, source_kind filter │ → פסיקה חיצונית │
+│ search_internal_decisions │ אותן פונקציות DB, │ source_kind=internal_ │
+│ │ source_kind אחר │ committee → ועדות-ערר │
+└──────────────────────────────┴─────────────────────────┴────────────────────────────┘
+```
+
+שתי האחרונות קוראות **לאותן פונקציות DB** (`search_precedent_library_semantic`/`_lexical`) עם `source_kind` שונה. זו הפרדה־בשאילתה, לא קוד מקביל.
+
+---
+
+## 3. שלושת המקורות של חיים → איפה הם נופלים היום
+
+```
+┌────────────────────────────────────┬───────────────────────────────┬─────────────────────────┐
+│ Source (חיים) │ Stored as │ Retrieval │
+├────────────────────────────────────┼───────────────────────────────┼─────────────────────────┤
+│ (1) החלטות דפנה עצמה │ style_corpus + (מהוגר ל-) │ search_decisions + │
+│ │ case_law internal_committee │ search_internal_decisions│
+│ │ chair_name='דפנה תמיר' │ ← כפילות / נתיב-כפול │
+│ (2) ועדות-ערר אחרות │ case_law internal_committee │ search_internal_decisions│
+│ │ chair_name=<אחר>, district │ │
+│ (3) פס"ד עליון/מחוזי │ case_law external_upload │ search_precedent_library │
+│ │ source_type='court_ruling' │ │
+└────────────────────────────────────┴───────────────────────────────┴─────────────────────────┘
+```
+
+**מסקנה:** המודל המנטלי של חיים **כבר ממומש בסכמה**. אין צורך להמציא מבנה חדש — צריך לאכוף את המבנה הקיים בקליטה, ולחבר את שכבת־הקנוני לכתיבה.
+
+---
+
+## 4. הדיאגנוזה — מה "בנוי לא נכון" (3 מחוללי־כאב)
+
+### 4.1 חוזה־קליטה רופף (root cause #1)
+- 66% מהפסיקה ללא `practice_area`; 27 ללא summary; 31 ללא full_text/chunks.
+- אין אכיפה ש"`searchable=false` עד שהמטא שלם" → INV-DM1 מופר בפועל.
+- **כל העלאה מוסיפה חוב** במקום רשומה שלמה. זה המקור לתיקונים החוזרים.
+
+### 4.2 צינור הלכות→קנוני מייצר רעש, לא ערך (root cause #2)
+- 5,456/5,472 סינגלטונים → דה־דופ לא עובד (סף 0.85? ניסוח־חילוץ?).
+- **0 published** → השכבה שאמורה להזין את הכותב (INV-G10) מנותקת.
+- 2,402 בתור־אישור־ידני → הצינור מייצר מהר יותר ממה שאדם מאשר.
+- **זה בולע את רוב זמן־התחזוקה.**
+
+### 4.3 כפילות style_corpus (root cause #3)
+- 55 החלטות דפנה בשני מקומות + שני נתיבי־אחזור.
+- צריך מקור־אמת אחד: או `style_corpus` SoT וה-`case_law` נגזר, או הפוך.
+
+---
+
+## 5. ההמלצה — לא רֵבילד־סכמה; "איפוס שכבות־נגזרות" אחרי תיקון־חוזה
+
+**אסור** לשרוף ולהעלות־מחדש את `case_law` — תבנה אותה סכמה ותאבד מטא־דאטה ידני. **כן** הגיוני רֵבילד צר של ה**נגזר**, אבל בסדר הזה (G1 — מקור לפני תסמין):
+
+1. **קודם החוזה:** אכוף ב-`*_upload` שדות־חובה (practice_area, summary, full_text); כשל → `searchable=false`. נקה/מחק את 31 הריקות.
+2. **תקן את הקנוניזציה:** כוונן סף 0.85, הגדר מתי `published`, ובדוק ניסוח־החילוץ. בלי זה אין טעם להריץ מחדש.
+3. **רק אז** re-derive מהמקור הקיים: chunks → halachot → canonical.
+4. **הכרע style_corpus:** מקור־אמת אחד.
+
+**מתי רֵבילד־מלא כן מוצדק:** רק אם יתגלה שמסמכי־המקור עצמם (PDF/full_text של 363) פגומים/חסרים. המספרים *לא* מראים זאת (332/363 עם full_text תקין).
+
+---
+
+## 6. שאלות פתוחות לשלב־הסינתזה
+- **למה 0 published בקנוני?** האם זה סף, ניסוח־חילוץ, או שפשוט אף אחד לא אישר? (קריטי — קובע אם V41 שמיש בכלל.)
+- **style_corpus מול case_law:** מי SoT? (משפיע על search_decisions מול search_internal_decisions.)
+- **practice_area לפסיקה חיצונית:** לחלץ אוטומטית בקליטה, או להשאיר ידני?
+- **תור־האישור (2,402):** האם המנגנון (פאנל/active-learning, #133) יכול לסגור את הפער, או שצריך לחתוך את קצב־החילוץ?
+
+---
+
+## 7. נספח — קבצים מרכזיים (file:line)
+- סכמה: `mcp-server/src/legal_mcp/services/db.py` (case_law, precedent_chunks, halachot, canonical_halachot)
+- קליטה חיצונית: `mcp-server/src/legal_mcp/tools/precedent_library.py` (`precedent_library_upload`)
+- קליטה פנימית: `mcp-server/src/legal_mcp/tools/internal_decisions.py` (`internal_decision_upload`)
+- הגירת style→case_law: `mcp-server/src/legal_mcp/services/internal_decisions.py` (`migrate_from_style_corpus`, chair/district hardcoded)
+- אחזור: `mcp-server/src/legal_mcp/tools/search.py`, `services/hybrid_search.py`, `services/db.py` (`search_precedent_library_semantic`/`_lexical`)
+- ספ: `docs/spec/02-data-model.md` (INV-DM1–7), `docs/spec/03-retrieval.md` (INV-RET1–5), `docs/spec/00-constitution.md` (G2/G10)
diff --git a/docs/precedent-corpus-redesign/06-extractor-generosity-committee-application.md b/docs/precedent-corpus-redesign/06-extractor-generosity-committee-application.md
new file mode 100644
index 0000000..8eaff78
--- /dev/null
+++ b/docs/precedent-corpus-redesign/06-extractor-generosity-committee-application.md
@@ -0,0 +1,125 @@
+# 06 — נדיבות-המחלץ: `application` בהחלטות-ועדה (כימות חוצה-קורפוס)
+
+> **קלט-נתונים** ליוזמה, נמדד חי על **5,489 רשומות `halachot`** (כל הקורפוס, 2026-06-20).
+> נולד משאלת-חיים: "8508-03-24 מפיק 71 הלכות ממתינות — האם המחלץ נדיב מדי על החלטות-ועדה,
+> או שזה ספציפי לתיק הזה?" התשובה: **שיטתי, לא ספציפי — אבל הנדיבות מוצדקת.**
+>
+> **מתכתב עם [`00-final-synthesis.md`](00-final-synthesis.md):** הנתונים כאן **מחזקים** את הכרעת-הסינתזה
+> ("לא לחתוך") ומוסיפים שני דברים שלא היו לה: (א) כימות חוצה-קורפוס של ה-`application`, (ב) ממצא
+> חדש וגדול יותר — `nli_unsupported` על הפסיקה החיצונית. ראה §4 (יישוב-מתח) ו-§5 (דרכי-פעולה).
+
+---
+
+## 1. הממצא המרכזי — `application` הוא תופעה שיטתית של החלטות-ועדה
+
+הפרדנו את הקורפוס לפי `authority` (נגזר דטרמיניסטית מ-`precedent_level`, [02-data-model §162](../spec/02-data-model.md)):
+`binding` = פסיקת-עליון/מנהלי · `persuasive` = ועדת-ערר מחוזית.
+
+```text
+source rows rt=application nli_unsupported any-flag
+court (binding) 3,603 0.2% 39.7% 45.4%
+committee (persuasive) 1,842 13.7% 25.0% 30.5%
+```
+
+- **`rule_type='application'` הוא כמעט-בלעדית של ועדות:** 13.7% מול 0.2%. מתוך 258 רשומות-`application`
+ בכל הקורפוס, **~252 מגיעות מהחלטות-ועדה.** פער של פי-~70.
+- **עקבי בין תחומים** (לא עניין-שמאות נקודתי):
+```text
+committee, rt=application by practice_area:
+ rishuy_uvniya 13.5% betterment_levy 14.1% compensation_197 18.8%
+```
+
+**הפרשנות (תואם [02-data-model §163](../spec/02-data-model.md)):** `application` = "החלה תלוית-עובדות —
+לרוב לא-הלכה". ועדת-ערר היא גוף מיישֵם: היא לא יוצרת הלכה (רק בית-משפט עושה זאת), אלא **מיישמת
+פסיקת-עליון קיימת** (לוסטרניק, דלי-דליה) על עובדות-התיק. לכן חילוץ מהחלטת-ועדה מייצר באופן מובנה
+שיעור גבוה של יישומי-דוקטרינה. **זו תכונה של מקור-הנתונים, לא באג של המחלץ.**
+
+## 2. 8508-03-24 — מייצג בקצה-העליון, לא חריג
+
+```text
+דירוג 8508-03-24: 7 מתוך 45 תיקי-ועדה (≥15 רשומות) · 27% application (rt או flag)
+חציון תיקי-הוועדה: 11.9%
+מעליו: 1001-02-19 (40%) · 1044-08-22 (37%) · 9002-24 (33%) · 1007-01-25 (30%)
+```
+
+8508 הוא ~פי-2.3 מהחציון (רבעון-עליון), אבל מה שבולט בו הוא בעיקר ש**הוא התיק הארוך בקורפוס**
+(111 רשומות) — אז 27% נותן 30 פריטי-`application` במספר מוחלט, הגבוה בקורפוס. כלומר: **כמות גבוהה,
+שיעור גבוה-אך-נורמלי.** אין כאן פתולוגיה ייחודית לתיק.
+
+## 3. מנגנון-הניתוב הקיים (איך זה כבר מטופל ב-UI)
+
+הפיצול בתור-ההלכות (`/precedents` → "תור הלכות") אינו לפי `exclude_low_quality`, אלא לפי
+`isExtractionFixItem(h) = (quality_flags.length>0) && !panel_round`
+([web-ui/.../precedent-library.ts:652](../../web-ui/src/lib/api/precedent-library.ts#L652)):
+
+```text
+8508-03-24, 71 ממתינות (מצב 2026-06-20):
+ bucket # panel? →"להכרעתך" →"דורש תיקון-חילוץ"
+ clean (ללא דגל) 40 15/40 40 0
+ application 23 0/23 0 23
+ nli_unsupported 4 4/4 4 0
+ thin_restatement 4 0/4 0 4
+```
+
+**משמעות:** פריטי-`application` (חסרי-פאנל) כבר מנותבים ל**"דורש תיקון-חילוץ"** — **מחוץ** לתור-ההכרעה
+של היו"ר. כלומר המערכת כבר מסננת אותם מהעומס-הידני. (הפאנל התלת-מודלי, [halacha_panel_approve.py:191](../../scripts/halacha_panel_approve.py#L191),
+מטפל רק בדליים `clean`+`nli`; `application` ו-`defect` עוקפים אותו במכוון.)
+
+## 4. ⚠️ יישוב-המתח מול הסינתזה — `application` ≠ "רעש"
+
+זו הנקודה הקריטית להעברה. **אסור** לתרגם "13.7% application" ל"13.7% רעש לחיתוך". המבחן בסינתזה
+(§2 שם) כבר הוכיח שחיתוך-אגרסיבי על 8508 השמיד את **לוסטרניק** ו-~22 עקרונות-ליבה. רוב פריטי-ה-`application`
+הם בדיוק יישומי-הדוקטרינה הללו — **בני-ציטוט שהכותב צריך**. דוגמאות אמיתיות מ-8508 שמסומנות `application`:
+
+```text
+"ציפיות הנובעות אך ממיקומם של המקרקעין... אין לנטרלן" ← לוסטרניק מיושמת — לשמור!
+"בחישוב שווי במצב קודם יש לכלול ציפיות כלליות... ולא ספציפיות" ← ליבת חישוב היטל-ההשבחה
+"קביעת מקדם מצויה בליבת שיקול-דעת השמאי, והוועדה לא תתערב" ← סטנדרט אי-התערבות מיושם
+```
+
+לכן: **הנתון הזה תומך ב"שמור-בספק" של עמוד-1/2 בסינתזה.** המסקנה הנכונה אינה "לסנן application" אלא
+"`application` מאשר שעקרוני-הוועדה הם persuasive-יישומיים — לדרג אותם נמוך באחזור (רמה B), לא למחוק
+אותם (רמה A)". `importance=0` ל-8508 כבר משקיע אותם ממילא.
+
+## 5. ⭐ ממצא-לוואי גדול יותר — `nli_unsupported` על הפסיקה החיצונית
+
+```text
+nli_unsupported: court (binding) 39.7% ≫ committee 25.0%
+any-flag: court 45.4% · committee 30.5%
+```
+
+**כמעט מחצית מעקרוני-הפסיקה-החיצונית נושאים דגל-איכות, בעיקר `nli_unsupported`** (הכלל אינו נגזר
+לוגית מהציטוט התומך שלו, [halacha_quality.py:282](../../mcp-server/src/legal_mcp/services/halacha_quality.py#L282)).
+זה **מגמד מספרית** את סוגיית-ה-`application`, ונוגע ישירות ל"רמה A = ניקוי-רעש" של הסינתזה:
+**מהו ה"רעש" שמנקים?** הדגל הדומיננטי הוא `nli`, והוא מרוכז בפסיקה, לא בוועדות.
+
+שתי השערות מתחרות, שצריך להכריע ביניהן **לפני** שמשתמשים ב-`nli` כמסנן-רעש ברמה A:
+- **(א) הבודק מחמיר מדי** — סף-ה-NLI חותך יישורים לגיטימיים → 40% הם false-positives, וה"רעש" מדומה.
+- **(ב) החילוץ-מהפסיקה לקוי** — ציטוטים תומכים שלא מיישרים לכלל → בעיית-חילוץ אמיתית בקנה-מידה.
+
+ההכרעה משנה את כל אסטרטגיית רמה-A. **אנו ממליצים לאמת זאת על מדגם-זהב לפני כל שימוש ב-`nli` כסיגנל.**
+
+---
+
+## 6. דרכי-פעולה מוצעות (לסוכן-הקורפוס)
+
+ממוין מהשמרני לאגרסיבי. ההמלצה שלנו: **B כברירת-מחדל + C כעבודה-מקבילה**; להימנע מ-A ומ-D.
+
+| # | פעולה | טיעון | סיכון | המלצה |
+|---|-------|-------|-------|-------|
+| **A** | לכוונן את ה-prompt לדכא `application` מוועדות במקור | מטפל-בשורש (G1); חוסך 14% רשומות | **גבוה** — סותר את מבחן-8508; משמיד יישומי-לוסטרניק בני-ציטוט | ✗ לא |
+| **B** | להשאיר את החילוץ; לסמוך על הניתוב הקיים (`application`→"תיקון", מחוץ לתור-היו"ר) + לדרג נמוך ב-RRF | תואם-סינתזה (שמור-בספק + דרג-בזמן-אחזור); אפס סיכון-אובדן | הרעש נשאר ב-DB (אחסון בלבד) | ✓ **כן — ברירת-מחדל** |
+| **C** | לחקור קודם את `nli_unsupported` (40% פסיקה): מדגם-זהב, להכריע (א) מחמיר-מדי מול (ב) חילוץ-לקוי | זה הסיגנל הגדול; הכרחי לפני שמגדירים "רעש" ברמה A | דורש מדגם מתויג-ידנית | ✓ **כן — במקביל** |
+| **D** | להפסיק חילוץ-עקרונות מוועדות לגמרי | רוב עקרוני-הוועדה הם שכתוב-persuasive של עליון | **קיצוני** — נוגד 07-learning §61 (ועדות ברות-ציטוט במכוון); נוגע INV-LRN | ✗ לא (אלא בהכרעת-יו"ר מפורשת) |
+
+### ההמלצה המזוקקת
+1. **לא לגעת בחילוץ-מוועדות** — הנתון מאשר שהנדיבות מוצדקת; `application`=יישום-בר-ציטוט, לא זבל. עקבי עם
+ הכרעת-הסינתזה "לא לחתוך".
+2. **רמה B עושה את העבודה** — `importance` boost ב-RRF מטביע את עקרוני-הוועדה ה-persuasive מתחת
+ לפסיקה-המחייבת, בלי למחוק דבר. 8508 (`importance≈0`) שוקע ממילא.
+3. **להעביר את ה-`nli` לראש תור-המחקר** — לפני שמשתמשים בו כמסנן-רעש ברמה A, לאמת אם 40% אמיתי.
+
+---
+
+> **מקור-הנתונים:** `GET /api/halachot?limit=100000` (5,489 שורות) + per-case `?case_law_id=…`.
+> ניתן לשחזר את כל המספרים מהשאילתות האלו. הקאנון הוא live — שיעורים ינועו ככל שהדריינר/פאנל רצים.
diff --git a/docs/superpowers/plans/2026-05-16-hooks-execution.md b/docs/superpowers/plans/2026-05-16-hooks-execution.md
new file mode 100644
index 0000000..7e8bad4
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-16-hooks-execution.md
@@ -0,0 +1,348 @@
+# Hooks: Case Status Webhooks Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** כאשר status של תיק משתנה ב-legal-ai (e.g. `qa_failed`, `exported`), ה-plugin מקבל webhook, מעדכן את ה-issue ב-Paperclip, ומעיר את ה-CEO במקרה הצורך.
+
+**Architecture:** Legal-ai REST API קורא `pc_request("POST", "/api/plugins/marcusgroup.legal-ai/webhooks/case-status", ...)` אחרי כל שינוי status. ה-plugin מטפל ב-`onWebhook()` ומגיב: מוסיף תגובה לissue, מעיר CEO אם QA נכשל.
+
+**Tech Stack:** TypeScript (plugin-legal-ai), Python/FastAPI (legal-ai web), `@paperclipai/plugin-sdk@2026.325.0`, `httpx` (Python), `pc_request` helper.
+
+---
+
+## File Map
+
+| Action | File |
+|--------|------|
+| Modify | `plugin-legal-ai/src/worker.ts` — add `onWebhook()` to `definePlugin({})` |
+| Modify | `plugin-legal-ai/plugin.json` — add `"webhooks.receive"` capability |
+| Modify | `plugin-legal-ai/src/manifest.ts` — add webhook capability |
+| Modify | `legal-ai/web/app.py` — emit webhook after `PUT /api/cases/{case_number}` |
+| Modify | `legal-ai/web/paperclip_api.py` — add `emit_webhook()` helper |
+
+---
+
+## Task 1: Add `emit_webhook` helper ב-Python
+
+**Files:**
+- Modify: `legal-ai/web/paperclip_api.py`
+
+- [ ] **Step 1: קרא את הקובץ הקיים**
+
+```bash
+head -90 /home/chaim/legal-ai/web/paperclip_api.py
+```
+
+- [ ] **Step 2: הוסף את ה-helper בסוף הקובץ**
+
+פתח `/home/chaim/legal-ai/web/paperclip_api.py` והוסף אחרי הפונקציה `pc_request`:
+
+```python
+async def emit_case_status_webhook(
+ case_number: str,
+ old_status: str,
+ new_status: str,
+ company_id: str | None = None,
+ run_id: str | None = None,
+) -> None:
+ """Notify the Paperclip plugin that a case status changed.
+
+ Fire-and-forget: logs errors but never raises, so callers aren't blocked.
+ """
+ try:
+ await pc_request(
+ "POST",
+ "/api/plugins/marcusgroup.legal-ai/webhooks/case-status",
+ json={
+ "caseNumber": case_number,
+ "oldStatus": old_status,
+ "newStatus": new_status,
+ "companyId": company_id,
+ "timestamp": datetime.utcnow().isoformat() + "Z",
+ },
+ run_id=run_id,
+ timeout=5.0,
+ )
+ except Exception as exc:
+ logger.warning("emit_case_status_webhook failed: %s", exc)
+```
+
+> **הערה:** `datetime` ו-`logger` כבר מיובאים ב-`app.py`. בדוק שהם מיובאים גם ב-`paperclip_api.py` — אם לא, הוסף `from datetime import datetime` ו-`import logging; logger = logging.getLogger(__name__)` בראש הקובץ.
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /home/chaim/legal-ai
+git add web/paperclip_api.py
+git commit -m "feat: add emit_case_status_webhook helper"
+```
+
+---
+
+## Task 2: צרף webhook לendpoint `PUT /api/cases/{case_number}`
+
+**Files:**
+- Modify: `legal-ai/web/app.py`
+
+- [ ] **Step 1: מצא את ה-endpoint**
+
+```bash
+grep -n "PUT\|case_number\|update_case" /home/chaim/legal-ai/web/app.py | head -30
+```
+
+- [ ] **Step 2: קרא את הenable endpoint המלא**
+
+זהה את הסקציה המלאה של ה-endpoint ואת הייבוא הקיים.
+
+- [ ] **Step 3: הוסף import ל-emit_webhook**
+
+בראש `app.py`, בסקציית ה-imports מ-`paperclip_api`:
+```python
+from .paperclip_api import pc_request, emit_case_status_webhook
+```
+
+- [ ] **Step 4: הוסף webhook emit בתוך הendpoint**
+
+אחרי שהקוד מעדכן את ה-case (לפני ה-`return`), הוסף:
+
+```python
+# Notify plugin about status change (fire-and-forget)
+if updates.get("status") and old_status != updates["status"]:
+ background_tasks.add_task(
+ emit_case_status_webhook,
+ case_number=case_number,
+ old_status=old_status,
+ new_status=updates["status"],
+ company_id=str(case.get("company_id")),
+ )
+```
+
+> אם ה-endpoint כבר מקבל `background_tasks: BackgroundTasks` — השתמש בו. אם לא, הוסף `background_tasks: BackgroundTasks` לחתימת הפונקציה. הוסף `from fastapi import BackgroundTasks` ל-imports.
+
+- [ ] **Step 5: שמור את ה-`old_status` לפני ה-update**
+
+בתחילת ה-endpoint handler, לפני קריאת ה-DB update:
+```python
+old_status = (await db.get_case(case_number) or {}).get("status", "")
+```
+
+- [ ] **Step 6: בדיקה בסיסית — שלח PUT ידני**
+
+```bash
+curl -s -X PUT https://legal-ai.nautilus.marcusgroup.org/api/cases/1130-25 \
+ -H "Content-Type: application/json" \
+ -d '{"status": "in_progress"}' | jq .status
+```
+
+בדוק ב-Paperclip logs שהwebhook נשלח (עדיין לא מטופל בצד ה-plugin):
+```bash
+pm2 logs paperclip --lines 20
+```
+
+- [ ] **Step 7: Commit**
+
+```bash
+cd /home/chaim/legal-ai
+git add web/app.py
+git commit -m "feat: emit case-status webhook on PUT /api/cases/:case"
+```
+
+---
+
+## Task 3: הוסף `onWebhook()` ל-plugin
+
+**Files:**
+- Modify: `plugin-legal-ai/src/worker.ts`
+
+- [ ] **Step 1: קרא את `definePlugin({})` הקיים**
+
+```bash
+grep -n "definePlugin\|onWebhook\|onHealth\|onShutdown" /home/chaim/plugin-legal-ai/src/worker.ts
+```
+
+- [ ] **Step 2: הוסף את `onWebhook` handler**
+
+בתוך הobject שמועבר ל-`definePlugin({})`, אחרי `setup(ctx)`:
+
+```typescript
+onWebhook: async (input) => {
+ const { endpointKey, payload, companyId } = input as {
+ endpointKey: string;
+ payload: {
+ caseNumber: string;
+ oldStatus: string;
+ newStatus: string;
+ companyId: string;
+ timestamp: string;
+ };
+ companyId: string;
+ };
+
+ if (endpointKey !== "case-status") return;
+
+ const { caseNumber, oldStatus, newStatus } = payload;
+ ctx.logger.info(`Webhook: ${caseNumber} ${oldStatus} → ${newStatus}`);
+
+ // Find the Paperclip issue linked to this case
+ const stateKey = `case:${caseNumber}`;
+ const issueId = await ctx.state.get({ companyId }, stateKey);
+ if (!issueId) {
+ ctx.logger.warn(`No issue found for case ${caseNumber}`);
+ return;
+ }
+
+ const statusLabels: Record = {
+ in_progress: "🔄 בעבודה",
+ drafted: "✍️ טיוטה מוכנה",
+ qa_failed: "❌ QA נכשל",
+ exported: "📄 יוצא ל-DOCX",
+ reviewed: "✅ נבדק",
+ final: "🎯 סופי",
+ };
+
+ const label = statusLabels[newStatus] ?? newStatus;
+
+ // Post a status comment on the issue
+ await ctx.issues.createComment({
+ issueId: issueId as string,
+ body: `**עדכון סטטוס:** ${label} (היה: ${oldStatus})`,
+ });
+
+ // Wake CEO if QA failed
+ if (newStatus === "qa_failed") {
+ const companies = await ctx.companies.list();
+ const company = companies.find((c) => c.id === companyId);
+ if (!company) return;
+
+ const CEO_IDS: Record = {
+ "42a7acd0-30c5-4cbd-ac97-7424f65df294": "752cebdd-6748-4a04-aacd-c7ab0294ef33",
+ "8639e837-4c9d-47fa-a76b-95788d651896": "cdbfa8bc-3d61-41a4-a2e7-677ec7d34562",
+ };
+ const ceoId = CEO_IDS[companyId];
+ if (ceoId) {
+ await ctx.agents.invoke(ceoId, companyId, {
+ prompt: `תיק ${caseNumber} נכשל בבדיקת QA. עיין בתוצאות QA ותקן את הבעיות.`,
+ reason: "qa_failed webhook",
+ });
+ }
+ }
+},
+```
+
+> **הערה:** `ctx` חייב להיות נגיש ב-`onWebhook`. אם `ctx` מוגדר בתוך `setup()` בלבד — הוצא אותו ל-closure חיצוני של ה-plugin object (ראה את הדפוס הקיים ב-`worker.ts`).
+
+- [ ] **Step 3: בדק TypeScript**
+
+```bash
+cd /home/chaim/plugin-legal-ai && npx tsc --noEmit
+```
+
+Expected: 0 errors.
+
+- [ ] **Step 4: Build**
+
+```bash
+cd /home/chaim/plugin-legal-ai && npm run build
+```
+
+Expected: `dist/worker.js` נוצר ללא שגיאות.
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /home/chaim/plugin-legal-ai
+git add src/worker.ts
+git commit -m "feat: add onWebhook handler for case-status events"
+```
+
+---
+
+## Task 4: הוסף capability ל-`plugin.json` ול-`manifest.ts`
+
+**Files:**
+- Modify: `plugin-legal-ai/plugin.json`
+- Modify: `plugin-legal-ai/src/manifest.ts`
+
+- [ ] **Step 1: הוסף `"webhooks.receive"` ל-capabilities**
+
+ב-`plugin-legal-ai/plugin.json`, בarray `"capabilities"`, הוסף:
+```json
+"webhooks.receive"
+```
+
+- [ ] **Step 2: הוסף גם ב-`manifest.ts`**
+
+```bash
+grep -n "capabilities\|webhooks" /home/chaim/plugin-legal-ai/src/manifest.ts
+```
+
+הוסף `"webhooks.receive"` לarray שם.
+
+- [ ] **Step 3: Re-install plugin**
+
+```bash
+cd /home/chaim/plugin-legal-ai && npm run build
+npx paperclipai plugin uninstall marcusgroup.legal-ai \
+ --api-base http://localhost:3100 --api-key pcapi_legal_install_key_2026
+npx paperclipai plugin install /home/chaim/plugin-legal-ai \
+ --api-base http://localhost:3100 --api-key pcapi_legal_install_key_2026
+pm2 restart paperclip
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /home/chaim/plugin-legal-ai
+git add plugin.json src/manifest.ts
+git commit -m "feat: add webhooks.receive capability to plugin manifest"
+```
+
+---
+
+## Task 5: בדיקה end-to-end
+
+- [ ] **Step 1: Deploy legal-ai**
+
+```bash
+cd /home/chaim/legal-ai
+git push origin main
+# המתן לבנייה (~2-4 דקות)
+```
+
+- [ ] **Step 2: שנה סטטוס תיק**
+
+```bash
+curl -s -X PUT https://legal-ai.nautilus.marcusgroup.org/api/cases/1130-25 \
+ -H "Content-Type: application/json" \
+ -d '{"status": "qa_failed"}' | jq .status
+```
+
+- [ ] **Step 3: בדוק שהתגובה הוספה ל-Paperclip issue**
+
+```bash
+# מצא את ה-issue הקשור לתיק 1130-25
+pm2 logs paperclip --lines 30 | grep "1130-25\|webhook\|qa_failed"
+```
+
+Expected: תגובה "❌ QA נכשל" הוספה לissue. CEO הועיר.
+
+- [ ] **Step 4: בדוק שינוי סטטוס שגרתי (לא QA)**
+
+```bash
+curl -s -X PUT https://legal-ai.nautilus.marcusgroup.org/api/cases/1130-25 \
+ -H "Content-Type: application/json" \
+ -d '{"status": "drafted"}' | jq .status
+```
+
+Expected: תגובה "✍️ טיוטה מוכנה" בissue. CEO **לא** הועיר.
+
+---
+
+## אימות סופי
+
+| בדיקה | פקודה | תוצאה מצופה |
+|-------|-------|-------------|
+| QA נכשל → CEO מועיר | `PUT status=qa_failed` | תגובה + agent invocation |
+| exported → תגובה בלבד | `PUT status=exported` | תגובה בלבד |
+| שינוי ללא status | `PUT title=...` | שום webhook |
+| תיק ללא issue | webhook לתיק חדש | לוג warning, ללא crash |
diff --git a/docs/superpowers/plans/2026-05-16-per-agent-claude-md.md b/docs/superpowers/plans/2026-05-16-per-agent-claude-md.md
new file mode 100644
index 0000000..f012aaa
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-16-per-agent-claude-md.md
@@ -0,0 +1,306 @@
+# Per-Agent CLAUDE.md Versioning & Validation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** הוסף validation ו-version tracking לקבצי הוראות הסוכנים. כעת, לפני שה-sync script מחיל שינויים, הוא מאמת שכל `instructionsFilePath` קיים. בנוסף, metadata של הסוכן ב-DB יכיל `claude_md_mtime` — השינוי האחרון בקובץ — כדי לזהות drift.
+
+**Architecture:** `sync_agents_across_companies.py` מקבל `--check-instructions` flag שסורק את כל הסוכנים ומדווח על קבצים חסרים/ישנים. ב-`--apply`, מתווספת בדיקת pre-flight שמבטלת את הsync אם קובץ חסר. `agents.metadata` מקבל `claude_md_mtime` עם ה-mtime בפועל של הקובץ.
+
+**Tech Stack:** Python 3.10+, asyncpg, httpx, `os.path.getmtime()`, Paperclip REST API (`PATCH /api/agents/{id}`).
+
+---
+
+## File Map
+
+| Action | File |
+|--------|------|
+| Modify | `legal-ai/scripts/sync_agents_across_companies.py` — `--check-instructions` flag, pre-flight, metadata update |
+
+זה הקובץ היחיד שצריך לגעת בו. כל שאר הלוגיקה קיימת כבר.
+
+---
+
+## Task 1: הוסף `--check-instructions` flag
+
+**Files:**
+- Modify: `legal-ai/scripts/sync_agents_across_companies.py`
+
+- [ ] **Step 1: קרא את החלק של `argparse` בסקריפט**
+
+```bash
+grep -n "argparse\|add_argument\|--verify\|--dry-run\|--apply" \
+ /home/chaim/legal-ai/scripts/sync_agents_across_companies.py | head -20
+```
+
+- [ ] **Step 2: הוסף את הargument**
+
+מצא את הסקציה שמגדירה `args` והוסף:
+
+```python
+parser.add_argument(
+ "--check-instructions",
+ action="store_true",
+ help="Scan all agents' instructionsFilePath and report missing/outdated files",
+)
+```
+
+- [ ] **Step 3: הוסף את הפונקציה `check_instructions()`**
+
+הוסף לפני `async def main()`:
+
+```python
+async def check_instructions(agents: list[dict]) -> bool:
+ """Print a report of all agents' instruction files. Returns True if all OK."""
+ all_ok = True
+ print(f"\n{'Agent':<30} {'File':<60} {'Status':<15} {'Size':>8} {'Modified'}")
+ print("-" * 120)
+
+ for agent in agents:
+ adapter_cfg = agent.get("adapter_config") or {}
+ if isinstance(adapter_cfg, str):
+ import json as _json
+ adapter_cfg = _json.loads(adapter_cfg)
+
+ file_path = adapter_cfg.get("instructionsFilePath", "")
+ name = agent.get("name", agent.get("id", "?"))[:29]
+
+ if not file_path:
+ print(f"{name:<30} {'(none)':<60} {'⚠️ NOT SET':<15}")
+ continue
+
+ if not os.path.exists(file_path):
+ print(f"{name:<30} {file_path[-59:]:<60} {'❌ MISSING':<15}")
+ all_ok = False
+ continue
+
+ stat = os.stat(file_path)
+ size_kb = stat.st_size // 1024
+ mtime = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
+
+ # Compare with DB metadata
+ metadata = agent.get("metadata") or {}
+ if isinstance(metadata, str):
+ import json as _json
+ metadata = _json.loads(metadata)
+
+ db_mtime = metadata.get("claude_md_mtime", "")
+ actual_mtime = str(int(stat.st_mtime))
+ drift = " ⚠️ DRIFT" if db_mtime and db_mtime != actual_mtime else ""
+
+ print(f"{name:<30} {file_path[-59:]:<60} {'✅ OK':<15} {size_kb:>6}KB {mtime}{drift}")
+
+ print()
+ return all_ok
+```
+
+> `from datetime import datetime` ו-`import os` — בדוק שמיובאים בראש הסקריפט. אם לא, הוסף.
+
+- [ ] **Step 4: הוסף קריאה ל-`check_instructions()` ב-`main()`**
+
+בתוך `async def main()`, אחרי שloading הagents מה-DB:
+
+```python
+if args.check_instructions:
+ all_ok = await check_instructions(master_agents + mirror_agents)
+ sys.exit(0 if all_ok else 1)
+```
+
+- [ ] **Step 5: בדיקה**
+
+```bash
+cd /home/chaim/legal-ai
+python scripts/sync_agents_across_companies.py --check-instructions
+```
+
+Expected: טבלה עם כל הסוכנים, paths, סטטוס ✅/❌.
+
+- [ ] **Step 6: Commit**
+
+```bash
+cd /home/chaim/legal-ai
+git add scripts/sync_agents_across_companies.py
+git commit -m "feat: add --check-instructions flag to sync script"
+```
+
+---
+
+## Task 2: הוסף pre-flight validation לפני `--apply`
+
+**Files:**
+- Modify: `legal-ai/scripts/sync_agents_across_companies.py`
+
+- [ ] **Step 1: מצא את נקודת הכניסה של `--apply`**
+
+```bash
+grep -n "args.apply\|if.*apply\|apply.*mode" \
+ /home/chaim/legal-ai/scripts/sync_agents_across_companies.py | head -10
+```
+
+- [ ] **Step 2: הוסף pre-flight לפני apply**
+
+בתחילת בלוק `--apply`, לפני כל שינוי:
+
+```python
+if args.apply:
+ # Pre-flight: abort if any agent is missing its instructions file
+ print("🔍 Pre-flight: checking instruction files...")
+ all_ok = await check_instructions(master_agents + mirror_agents)
+ if not all_ok:
+ print("❌ Abort: one or more instruction files are missing. Fix paths before --apply.")
+ sys.exit(1)
+ print("✅ Pre-flight passed.\n")
+ # ... rest of apply logic ...
+```
+
+- [ ] **Step 3: בדיקה — הפעל עם קובץ חסר (סימולציה)**
+
+```bash
+# שנה זמנית path לקובץ שלא קיים
+cd /home/chaim/legal-ai
+python scripts/sync_agents_across_companies.py --dry-run 2>&1 | head -5
+```
+
+Expected: dry-run עובר. אם תנסה `--apply` עם agent שhis file חסר — הsync יבוטל.
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /home/chaim/legal-ai
+git add scripts/sync_agents_across_companies.py
+git commit -m "feat: add pre-flight instruction file validation before --apply"
+```
+
+---
+
+## Task 3: עדכן `agents.metadata` עם `claude_md_mtime`
+
+**Files:**
+- Modify: `legal-ai/scripts/sync_agents_across_companies.py`
+
+- [ ] **Step 1: מצא את `compute_diff()` או `apply_diff()`**
+
+```bash
+grep -n "def compute_diff\|def apply_diff\|def build_patch\|metadata" \
+ /home/chaim/legal-ai/scripts/sync_agents_across_companies.py | head -20
+```
+
+- [ ] **Step 2: הוסף פונקציה `get_claude_md_mtime()`**
+
+```python
+def get_claude_md_mtime(adapter_config: dict) -> str | None:
+ """Return the Unix mtime of the agent's instructionsFilePath, or None if missing."""
+ path = adapter_config.get("instructionsFilePath", "")
+ if not path or not os.path.exists(path):
+ return None
+ return str(int(os.path.getmtime(path)))
+```
+
+- [ ] **Step 3: שלב mtime בbuild של metadata patch**
+
+מצא את המקום שמכין את ה-`metadata` לsync. הוסף:
+
+```python
+# Build metadata patch with claude_md_mtime
+current_metadata = master_agent.get("metadata") or {}
+if isinstance(current_metadata, str):
+ import json as _json
+ current_metadata = _json.loads(current_metadata)
+
+adapter_cfg = master_agent.get("adapter_config") or {}
+if isinstance(adapter_cfg, str):
+ import json as _json
+ adapter_cfg = _json.loads(adapter_cfg)
+
+mtime = get_claude_md_mtime(adapter_cfg)
+if mtime:
+ current_metadata["claude_md_mtime"] = mtime
+ current_metadata["claude_md_last_synced"] = datetime.utcnow().isoformat() + "Z"
+```
+
+כלול את ה-`current_metadata` המעודכן ב-PATCH לAPI.
+
+- [ ] **Step 4: בדוק שה-metadata מתעדכן**
+
+הפעל `--dry-run` וחפש `claude_md_mtime` בoutput:
+```bash
+python scripts/sync_agents_across_companies.py --dry-run 2>&1 | grep -i "mtime\|metadata" | head -10
+```
+
+לאחר `--apply`, בדוק ב-DB:
+```bash
+psql -h localhost -p 54329 -U paperclip -c \
+ "SELECT name, metadata->>'claude_md_mtime' AS mtime FROM agents WHERE metadata->>'claude_md_mtime' IS NOT NULL LIMIT 5" \
+ paperclip
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /home/chaim/legal-ai
+git add scripts/sync_agents_across_companies.py
+git commit -m "feat: track claude_md_mtime in agents.metadata during sync"
+```
+
+---
+
+## Task 4: הוסף `make check-agents` shortcut
+
+**Files:**
+- Modify: `legal-ai/Makefile` (אם קיים) אחרת הוסף alias
+
+- [ ] **Step 1: בדוק אם Makefile קיים**
+
+```bash
+ls /home/chaim/legal-ai/Makefile 2>/dev/null && echo "EXISTS" || echo "MISSING"
+```
+
+- [ ] **Step 2א: אם Makefile קיים — הוסף target**
+
+```makefile
+check-agents:
+ python scripts/sync_agents_across_companies.py --check-instructions
+
+sync-agents-dry:
+ python scripts/sync_agents_across_companies.py --dry-run
+
+sync-agents:
+ python scripts/sync_agents_across_companies.py --apply
+```
+
+- [ ] **Step 2ב: אם Makefile לא קיים — הוסף alias ל-`~/.bashrc`**
+
+```bash
+echo "alias check-agents='cd /home/chaim/legal-ai && python scripts/sync_agents_across_companies.py --check-instructions'" >> ~/.bashrc
+source ~/.bashrc
+```
+
+- [ ] **Step 3: בדוק**
+
+```bash
+# אם Makefile:
+make -C /home/chaim/legal-ai check-agents
+# אם alias:
+check-agents
+```
+
+Expected: טבלת סוכנים מוצגת.
+
+- [ ] **Step 4: Commit (אם Makefile)**
+
+```bash
+cd /home/chaim/legal-ai
+git add Makefile
+git commit -m "feat: add check-agents and sync-agents make targets"
+```
+
+---
+
+## אימות סופי
+
+| בדיקה | פקודה | תוצאה מצופה |
+|-------|-------|-------------|
+| `--check-instructions` | `python sync_agents... --check-instructions` | טבלה עם ✅ לכל agent |
+| Pre-flight בולם apply | מחק קובץ זמנית + `--apply` | Abort עם הודעה ברורה |
+| mtime ב-DB | `SELECT metadata->>'claude_md_mtime' FROM agents` | timestamp לכל agent |
+| DRIFT זוהה | שנה קובץ + `--check-instructions` | ⚠️ DRIFT מוצג |
+| shortcut | `check-agents` או `make check-agents` | עובד |
diff --git a/docs/superpowers/plans/2026-05-16-scheduled-agents.md b/docs/superpowers/plans/2026-05-16-scheduled-agents.md
new file mode 100644
index 0000000..2a09ada
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-16-scheduled-agents.md
@@ -0,0 +1,412 @@
+# Scheduled Background Agents Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** הוסף 2 cron jobs לplugin: (1) תזכורת על תיקים תקועים 3+ ימים, (2) ניתוח פידבק יו"ר שבועי עם עדכון `decision-lessons.md` אוטומטי.
+
+**Architecture:** שני jobs חדשים נרשמים ב-`ctx.jobs.register()`. Job 1 קורא `/api/cases/stale?days=3` (endpoint חדש ב-legal-ai) ומוסיף תגובה לissues תקועים. Job 2 קורא `/api/chair-feedback/weekly-summary` (endpoint חדש), שולח לCEO agent שמעדכן את `decision-lessons.md`.
+
+**Tech Stack:** TypeScript (plugin-legal-ai jobs), Python/FastAPI (legal-ai web), asyncpg, Paperclip SDK `ctx.jobs.register()`, `ctx.agents.invoke()`.
+
+---
+
+## File Map
+
+| Action | File |
+|--------|------|
+| Modify | `plugin-legal-ai/src/worker.ts` — register 2 new jobs |
+| Modify | `plugin-legal-ai/plugin.json` — declare 2 new job entries |
+| Modify | `plugin-legal-ai/src/manifest.ts` — add to jobs array |
+| Modify | `legal-ai/web/app.py` — add `GET /api/cases/stale` + `GET /api/chair-feedback/weekly-summary` |
+| Modify | `legal-ai/web/database.py` (or equivalent DB module) — add `get_stale_cases()` + `get_weekly_chair_feedback()` |
+
+---
+
+## Task 1: הוסף endpoint `GET /api/cases/stale`
+
+**Files:**
+- Modify: `legal-ai/web/app.py`
+- Modify: `legal-ai/web/database.py` (שם DB queries מנוהלים — בדוק עם `grep -n "async def get_cases\|from.*database\|import.*db" /home/chaim/legal-ai/web/app.py | head -10`)
+
+- [ ] **Step 1: מצא את module ה-DB**
+
+```bash
+grep -n "^from\|^import\|db\." /home/chaim/legal-ai/web/app.py | head -20
+```
+
+זהה את שם הmodule שמכיל את DB queries (בד"כ `database.py` או `db.py`).
+
+- [ ] **Step 2: הוסף `get_stale_cases()` לmodule ה-DB**
+
+```python
+async def get_stale_cases(days: int = 3) -> list[dict]:
+ """Return cases whose status is not 'final' and haven't been updated in `days` days."""
+ async with get_db_connection() as conn:
+ rows = await conn.fetch(
+ """
+ SELECT case_number, title, status, company_id,
+ updated_at,
+ now() - updated_at AS age
+ FROM cases
+ WHERE status NOT IN ('final', 'new')
+ AND updated_at < now() - ($1 || ' days')::interval
+ ORDER BY updated_at ASC
+ """,
+ str(days),
+ )
+ return [dict(r) for r in rows]
+```
+
+> `get_db_connection()` — השתמש בדפוס הקיים בקובץ. אם זה `asyncpg.connect()` ישיר, `asyncpg.create_pool()`, או context manager — העתק את הדפוס.
+
+- [ ] **Step 3: הוסף endpoint ב-`app.py`**
+
+```python
+@app.get("/api/cases/stale")
+async def list_stale_cases(days: int = 3):
+ """Cases stuck in non-final status for more than `days` days."""
+ cases = await db.get_stale_cases(days=days)
+ return {
+ "cases": [
+ {
+ "case_number": c["case_number"],
+ "title": c["title"],
+ "status": c["status"],
+ "company_id": str(c["company_id"]),
+ "days_stale": c["age"].days,
+ }
+ for c in cases
+ ],
+ "total": len(cases),
+ }
+```
+
+- [ ] **Step 4: בדיקה**
+
+```bash
+curl -s "https://legal-ai.nautilus.marcusgroup.org/api/cases/stale?days=1" | jq .total
+```
+
+Expected: JSON עם רשימת תיקים.
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /home/chaim/legal-ai
+git add web/app.py web/database.py # או השם הנכון
+git commit -m "feat: add GET /api/cases/stale endpoint"
+```
+
+---
+
+## Task 2: הוסף endpoint `GET /api/chair-feedback/weekly-summary`
+
+**Files:**
+- Modify: `legal-ai/web/app.py`
+- Modify: DB module
+
+- [ ] **Step 1: בדוק את מבנה טבלת `chair_feedback`**
+
+```bash
+sqlite3 /home/chaim/.paperclip/instances/default/data/app.db \
+ ".schema chair_feedback" 2>/dev/null || \
+psql -h localhost -p 5433 -U legal_ai -c "\d chair_feedback" legal_ai 2>/dev/null || \
+grep -rn "chair_feedback" /home/chaim/legal-ai/mcp-server/src/ | head -10
+```
+
+- [ ] **Step 2: הוסף `get_weekly_chair_feedback()` לDB module**
+
+```python
+async def get_weekly_chair_feedback(days: int = 7) -> list[dict]:
+ """Return chair feedback entries from the last `days` days."""
+ async with get_db_connection() as conn:
+ rows = await conn.fetch(
+ """
+ SELECT cf.case_number, cf.feedback_text, cf.created_at,
+ cf.feedback_type, c.title
+ FROM chair_feedback cf
+ JOIN cases c ON c.case_number = cf.case_number
+ WHERE cf.created_at > now() - ($1 || ' days')::interval
+ ORDER BY cf.created_at DESC
+ """,
+ str(days),
+ )
+ return [dict(r) for r in rows]
+```
+
+> אם שמות השדות שונים (בדוק ב-Step 1) — התאם.
+
+- [ ] **Step 3: הוסף endpoint**
+
+```python
+@app.get("/api/chair-feedback/weekly-summary")
+async def get_chair_feedback_weekly(days: int = 7):
+ """Feedback entries from the past week, formatted for the learning agent."""
+ entries = await db.get_weekly_chair_feedback(days=days)
+ if not entries:
+ return {"summary": "", "entry_count": 0}
+
+ lines = [
+ f"- תיק {e['case_number']} ({e['title']}): {e['feedback_text']}"
+ for e in entries
+ ]
+ summary = "\n".join(lines)
+ return {"summary": summary, "entry_count": len(entries), "entries": entries}
+```
+
+- [ ] **Step 4: בדיקה**
+
+```bash
+curl -s "https://legal-ai.nautilus.marcusgroup.org/api/chair-feedback/weekly-summary" | jq .entry_count
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+cd /home/chaim/legal-ai
+git add web/app.py web/database.py
+git commit -m "feat: add GET /api/chair-feedback/weekly-summary endpoint"
+```
+
+---
+
+## Task 3: הוסף jobs לplugin
+
+**Files:**
+- Modify: `plugin-legal-ai/plugin.json`
+- Modify: `plugin-legal-ai/src/manifest.ts`
+
+- [ ] **Step 1: קרא את ה-`jobs` הקיים ב-`plugin.json`**
+
+```bash
+cat /home/chaim/plugin-legal-ai/plugin.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d['jobs'], indent=2))"
+```
+
+- [ ] **Step 2: הוסף 2 jobs חדשים לarray `"jobs"` ב-`plugin.json`**
+
+```json
+{
+ "jobKey": "stale-case-reminder",
+ "displayName": "תזכורת תיקים תקועים",
+ "description": "מזהה תיקים שלא עודכנו 3+ ימים ומוסיף תגובה לissue",
+ "schedule": "0 8 * * *"
+},
+{
+ "jobKey": "weekly-feedback-analysis",
+ "displayName": "ניתוח פידבק שבועי",
+ "description": "מסכם פידבק יו\"ר מהשבוע האחרון ומעדכן את decision-lessons.md",
+ "schedule": "0 19 * * 0"
+}
+```
+
+> `"0 8 * * *"` = כל יום בשעה 08:00. `"0 19 * * 0"` = כל ראשון ב-19:00.
+
+- [ ] **Step 3: עדכן `manifest.ts`**
+
+```bash
+grep -n "jobs\|jobKey\|schedule" /home/chaim/plugin-legal-ai/src/manifest.ts
+```
+
+הוסף את אותם 2 objects לarray `jobs` ב-`manifest.ts`.
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /home/chaim/plugin-legal-ai
+git add plugin.json src/manifest.ts
+git commit -m "feat: declare stale-case-reminder and weekly-feedback-analysis jobs"
+```
+
+---
+
+## Task 4: Implement job handlers ב-`worker.ts`
+
+**Files:**
+- Modify: `plugin-legal-ai/src/worker.ts`
+
+- [ ] **Step 1: קרא את handler של `sync-case-status` הקיים**
+
+```bash
+grep -n "sync-case-status\|jobs.register\|jobKey" /home/chaim/plugin-legal-ai/src/worker.ts
+```
+
+העתק את הדפוס.
+
+- [ ] **Step 2: הוסף את `stale-case-reminder` handler**
+
+בתוך `setup(ctx)`, אחרי רישום ה-job הקיים:
+
+```typescript
+ctx.jobs.register("stale-case-reminder", async (job) => {
+ ctx.logger.info("stale-case-reminder: starting");
+
+ const config = await ctx.config.get();
+ const apiBase = (config.legalApiBaseUrl as string) ?? "http://localhost:8085";
+
+ const resp = await ctx.http.fetch(`${apiBase}/api/cases/stale?days=3`);
+ if (!resp.ok) {
+ ctx.logger.error(`stale-case-reminder: API error ${resp.status}`);
+ return;
+ }
+
+ const data = (await resp.json()) as {
+ cases: Array<{
+ case_number: string;
+ title: string;
+ status: string;
+ company_id: string;
+ days_stale: number;
+ }>;
+ };
+
+ for (const staleCase of data.cases) {
+ const issueId = await ctx.state.get(
+ { companyId: staleCase.company_id },
+ `case:${staleCase.case_number}`
+ );
+ if (!issueId) continue;
+
+ await ctx.issues.createComment({
+ issueId: issueId as string,
+ body: `⚠️ **תיק תקוע** — ${staleCase.days_stale} ימים ללא עדכון (סטטוס: ${staleCase.status}). האם יש צורך בפעולה?`,
+ });
+
+ ctx.logger.info(
+ `stale-case-reminder: posted reminder for ${staleCase.case_number} (${staleCase.days_stale}d stale)`
+ );
+ }
+
+ ctx.logger.info(`stale-case-reminder: done. ${data.cases.length} cases reminded`);
+});
+```
+
+- [ ] **Step 3: הוסף את `weekly-feedback-analysis` handler**
+
+```typescript
+ctx.jobs.register("weekly-feedback-analysis", async (job) => {
+ ctx.logger.info("weekly-feedback-analysis: starting");
+
+ const config = await ctx.config.get();
+ const apiBase = (config.legalApiBaseUrl as string) ?? "http://localhost:8085";
+
+ const resp = await ctx.http.fetch(`${apiBase}/api/chair-feedback/weekly-summary`);
+ if (!resp.ok) {
+ ctx.logger.error(`weekly-feedback-analysis: API error ${resp.status}`);
+ return;
+ }
+
+ const data = (await resp.json()) as {
+ summary: string;
+ entry_count: number;
+ };
+
+ if (data.entry_count === 0) {
+ ctx.logger.info("weekly-feedback-analysis: no feedback this week, skipping");
+ return;
+ }
+
+ // Invoke the CEO agent to process feedback and update decision-lessons.md
+ const companies = await ctx.companies.list();
+ for (const company of companies) {
+ // CEO IDs per company
+ const CEO_IDS: Record = {
+ "42a7acd0-30c5-4cbd-ac97-7424f65df294": "752cebdd-6748-4a04-aacd-c7ab0294ef33",
+ "8639e837-4c9d-47fa-a76b-95788d651896": "cdbfa8bc-3d61-41a4-a2e7-677ec7d34562",
+ };
+ const ceoId = CEO_IDS[company.id];
+ if (!ceoId) continue;
+
+ await ctx.agents.invoke(ceoId, company.id, {
+ prompt: `ניתוח פידבק שבועי יו"ר (${data.entry_count} פריטים):
+
+${data.summary}
+
+המשימה: עדכן את /home/chaim/legal-ai/docs/legal-decision-lessons.md עם הלקחים החדשים שעולים מהפידבק. הוסף רק לקחים חדשים שלא קיימים כבר. קבץ לפי נושא.`,
+ reason: "weekly-feedback-analysis scheduled job",
+ });
+
+ ctx.logger.info(
+ `weekly-feedback-analysis: invoked CEO for company ${company.id} (${data.entry_count} feedback entries)`
+ );
+ break; // One CEO is enough — lessons file is shared
+ }
+});
+```
+
+- [ ] **Step 4: TypeScript check**
+
+```bash
+cd /home/chaim/plugin-legal-ai && npx tsc --noEmit
+```
+
+Expected: 0 errors.
+
+- [ ] **Step 5: Build**
+
+```bash
+cd /home/chaim/plugin-legal-ai && npm run build
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+cd /home/chaim/plugin-legal-ai
+git add src/worker.ts
+git commit -m "feat: implement stale-case-reminder and weekly-feedback-analysis jobs"
+```
+
+---
+
+## Task 5: Re-install plugin + בדיקה
+
+- [ ] **Step 1: Deploy legal-ai**
+
+```bash
+cd /home/chaim/legal-ai && git push origin main
+# המתן ~3 דקות
+curl -s https://legal-ai.nautilus.marcusgroup.org/api/health | jq .status
+```
+
+- [ ] **Step 2: Re-install plugin**
+
+```bash
+cd /home/chaim/plugin-legal-ai && npm run build
+npx paperclipai plugin uninstall marcusgroup.legal-ai \
+ --api-base http://localhost:3100 --api-key pcapi_legal_install_key_2026
+npx paperclipai plugin install /home/chaim/plugin-legal-ai \
+ --api-base http://localhost:3100 --api-key pcapi_legal_install_key_2026
+pm2 restart paperclip
+```
+
+- [ ] **Step 3: בדוק שה-jobs רשומים**
+
+```bash
+curl -s -H "Authorization: Bearer pcapi_legal_install_key_2026" \
+ http://localhost:3100/api/plugins/marcusgroup.legal-ai/jobs | jq .[].jobKey
+```
+
+Expected: `"sync-case-status"`, `"stale-case-reminder"`, `"weekly-feedback-analysis"`.
+
+- [ ] **Step 4: הפעל job ידנית לבדיקה**
+
+```bash
+curl -s -X POST -H "Authorization: Bearer pcapi_legal_install_key_2026" \
+ http://localhost:3100/api/plugins/marcusgroup.legal-ai/jobs/stale-case-reminder/run | jq .
+```
+
+Expected: Job הופעל. בדוק logs:
+```bash
+pm2 logs paperclip --lines 30 | grep "stale-case-reminder"
+```
+
+---
+
+## אימות סופי
+
+| בדיקה | פקודה | תוצאה מצופה |
+|-------|-------|-------------|
+| API stale endpoint | `curl .../api/cases/stale?days=1` | JSON עם cases |
+| API feedback endpoint | `curl .../api/chair-feedback/weekly-summary` | JSON עם summary |
+| Jobs רשומים | `GET .../api/plugins/.../jobs` | 3 jobs רשומים |
+| Stale reminder ידני | `POST .../jobs/stale-case-reminder/run` | תגובות בissues |
+| Feedback analysis ידני | `POST .../jobs/weekly-feedback-analysis/run` | CEO מועיר |
diff --git a/mcp-server/tests/test_adapter_profiles.py b/mcp-server/tests/test_adapter_profiles.py
new file mode 100644
index 0000000..988459b
--- /dev/null
+++ b/mcp-server/tests/test_adapter_profiles.py
@@ -0,0 +1,21 @@
+"""Adapter-profile compatibility gates for Paperclip migration."""
+from __future__ import annotations
+
+import importlib.util
+from pathlib import Path
+
+_SCRIPT = Path(__file__).resolve().parents[2] / "scripts" / "adapter_profiles.py"
+_spec = importlib.util.spec_from_file_location("adapter_profiles", _SCRIPT)
+profiles = importlib.util.module_from_spec(_spec)
+_spec.loader.exec_module(profiles)
+
+
+def test_codex_local_profile_accepts_openai_model_ids():
+ assert profiles.model_matches_provider("gpt-5.3-codex", "codex_local")
+ assert profiles.model_matches_provider("o4-mini", "codex_local")
+ assert profiles.model_matches_provider("codex-mini-latest", "codex_local")
+
+
+def test_codex_local_profile_rejects_foreign_model_ids():
+ assert not profiles.model_matches_provider("claude-opus-4-8", "codex_local")
+ assert not profiles.model_matches_provider("gemini-3.1-pro-preview", "codex_local")
diff --git a/scripts/exec_summary_1043.py b/scripts/exec_summary_1043.py
new file mode 100644
index 0000000..cb8a453
--- /dev/null
+++ b/scripts/exec_summary_1043.py
@@ -0,0 +1,145 @@
+"""Ad-hoc: executive summary (סיכום מנהלים) DOCX for case 1043-02-26.
+Reuses the dafna decision template styles (David font + RTL) via the
+analysis_docx_exporter helpers. One-off prep document for chaim's meeting
+with the chair — NOT a decision draft.
+"""
+from pathlib import Path
+import sys
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "mcp-server" / "src"))
+
+from docx import Document
+from legal_mcp.services.analysis_docx_exporter import (
+ TEMPLATE_PATH,
+ _clear_body,
+ _add_paragraph,
+ _add_runs_with_inline_bold,
+ _mark_paragraph_rtl,
+ _mark_run_rtl,
+)
+
+CASE = "1043-02-26"
+OUT = Path(f"/home/chaim/legal-ai/data/cases/{CASE}/exports/סיכום-מנהלים-v1.docx")
+
+
+def H1(doc, t):
+ _add_paragraph(doc, t, "Heading 1")
+
+
+def H2(doc, t):
+ _add_paragraph(doc, t, "Heading 2")
+
+
+def P(doc, t):
+ p = doc.add_paragraph(style="Normal")
+ _add_runs_with_inline_bold(p, t)
+ _mark_paragraph_rtl(p)
+ return p
+
+
+def BULLET(doc, t):
+ p = doc.add_paragraph(style="List Paragraph")
+ _add_runs_with_inline_bold(p, t)
+ _mark_paragraph_rtl(p)
+ return p
+
+
+def LABEL(doc, label, value):
+ p = doc.add_paragraph(style="Normal")
+ r = p.add_run(label + ": ")
+ r.bold = True
+ _mark_run_rtl(r)
+ r2 = p.add_run(value)
+ _mark_run_rtl(r2)
+ _mark_paragraph_rtl(p)
+ return p
+
+
+def main():
+ doc = Document(str(TEMPLATE_PATH))
+ _clear_body(doc)
+
+ H1(doc, "סיכום מנהלים — הכנה לדיון")
+ P(doc, "**ערר 1043-02-26 — הקמת מתקן למיון פסולת (אתר \"קומפוסט דלילה\", תכנית מי/1030)**")
+ P(doc, "מסמך הכנה פנימי לקראת דיון עם יו\"ר הוועדה. אינו החלטה ואינו טיוטת החלטה.")
+
+ H2(doc, "פרטי התיק")
+ LABEL(doc, "סוג הערר", "רישוי ובנייה (1xxx) — ערר על סירוב בקשה להיתר")
+ LABEL(doc, "עוררים (מבקשי ההיתר)", "קיבוץ נחשון; חברת אלקטרה אקו גרין פארק")
+ LABEL(doc, "משיבים", "הוועדה המקומית מטה יהודה; מושבי גפן/תירוש/כפר הריף; קיבוצי כפר מנחם/רבדים; מועצה אזורית יואב (מתנגדים)")
+ LABEL(doc, "מושא הערר", "החלטת הוועדה המקומית מטה יהודה מיום 9.2.26 לסרב לבקשה (מס' 20240972)")
+ LABEL(doc, "המקרקעין", "גוש 5093 ח\"ל 4 מגרש 1, אתר \"דלילה\" (~137 דונם), מצפון לכפר מנחם, ליד כביש 383")
+ LABEL(doc, "תקן ביקורת", "ועדת הערר כמוסד תכנון בעל סמכות מקורית — שיקול דעת תכנוני עצמאי; ביקורת רחבה יותר בחלק המשפטי-פרשני")
+
+ H2(doc, "מהות המחלוקת")
+ P(doc, "מבקשי ההיתר מבקשים להקים מתקן מיון פסולת/קומפוסטציה הכולל מבנה קומפוסטציה אחוד (~19,118 מ\"ר עיקרי), משטחי הבשלה פתוחים ובריכה תפעולית, בצירוף שתי הקלות: הגבהת גובה מ-8 ל-20 מ', והגדלת תכסית מ-3.5% ל-22%. הוועדה המקומית סירבה, בקובעה כי השינוי מהותי ומקומו בעדכון התכנית ולא בהליך רישוי. השאלה המרכזית: האם הבקשה תואמת את תכנית מי/1030, או חורגת ממנה באופן המחייב תיקון תכנית.")
+
+ H2(doc, "החלטת הוועדה המקומית (מושא הערר)")
+ P(doc, "**דחתה** את טענת המתנגדים לפקיעת התכנית (בהסתמך על ע\"א 3213/97 נקר) ואת טענת השימושים האסורים.")
+ P(doc, "**קיבלה** והפכה לבסיס הסירוב: (1) היקף הבינוי חורג משלב א' לפי הוראת השלביות; (2) הבינוי המונוליטי שונה דרמטית מנספח הבינוי; (3) משטחי ההבשלה הפתוחים מנוגדים לסעיף 6.9 (טיפול במבנים סגורים) — שהפרתו מוגדרת בתכנית כסטייה ניכרת.")
+
+ H2(doc, "טענות סף (לדיון תמציתי)")
+ BULLET(doc, "**פקיעת תכנית** — סעיף הפקיעה (5 שנים) הושמט מהנוסח המאושר (ראו ממצאי ניתוח-העומק).")
+ BULLET(doc, "**מיצוי התנגדות / \"מועד ב'\"** — האם טענות שהוכרעו בשלב התכנית מועלות מחדש בשלב הרישוי.")
+ BULLET(doc, "**זכות עמידה** — מועד חתימת חוזה החכירה מול מועד פתיחת הבקשה (הוכרע: קיבוץ נחשון חוכר רשום).")
+ BULLET(doc, "**פגמי פרסום / היעדר יידוע** (ס' 149(א)(2א)) — היעדר תשריט, אי-יידוע ועדה מקומית יואב.")
+ BULLET(doc, "**איחור בהגשת ההתנגדויות** — מול אינטרס ההסתמכות (רכישת קרקע ב-35 מיליון ₪).")
+
+ H2(doc, "חמש הסוגיות המהותיות")
+
+ P(doc, "**סוגיה 1 (מכריעה) — שלביות הביצוע: בינוי מול היקף פעילות**")
+ BULLET(doc, "השאלה: האם הוראת השלביות (ס' 7.1) חלה על הבינוי הפיזי או רק על היקף ההפעלה?")
+ BULLET(doc, "עמדות: עוררים — נוגעת להיקף הפעילות; ועדה/משיבים — חלה על הבינוי (4 מבנים ≈ 8,000 מ\"ר מול ~19,118 מבוקשים).")
+ BULLET(doc, "לאן נוטה: **לטובת הוועדה** — לשון \"הקמת 4 מבני קומפוסט\" מתייחסת לבינוי; נספחים 06, 11 מצביעים על מנגנון פיילוט מדורג מהותי.")
+ BULLET(doc, "תקדים: עע\"מ 10089/07 אירוס הגלבוע (אין לעקוף תכנון נדרש דרך היתר).")
+
+ P(doc, "**סוגיה 2 (מכריעה) — משטחי ההבשלה וסעיף 6.9: \"טיפול\" מול \"אחסון\"**")
+ BULLET(doc, "השאלה: האם ההבשלה הפתוחה היא \"טיפול\" החוסה תחת 6.9 (→ סטייה ניכרת החוסמת הקלה, ס' 151), או \"אחסון\"?")
+ BULLET(doc, "עמדות: עוררים (ד\"ר ענבר, נספח 22) — אחסון מוצר סופי נטול ריח; ועדה — חלק בלתי נפרד מהטיפול.")
+ BULLET(doc, "לאן נוטה: **נחלשה לוועדה** — נספח 07 §122-127: ועדת המשנה לעררים אישרה הבשלה פתוחה כ\"נכון וסביר\" (אך כינתה זאת \"שלב אחרון של טיפול\"). מתח פרשני אמיתי.")
+ BULLET(doc, "תקדים: עע\"מ 402/03 עמותת העצמאים (ס' 151 — סטייה ניכרת).")
+
+ P(doc, "**סוגיה 3 — ההקלות בגובה ובתכסית + טענת \"טעות סופר\"**")
+ BULLET(doc, "השאלה: האם ההקלות בגדרי הקלה או סטייה ניכרת? והאם התכסית 3.5% היא \"טעות סופר\"?")
+ BULLET(doc, "לאן נוטה: טענת טעות הסופר **התחזקה מאוד** — נספח 12 (מופקד) מראה ש-3.5% נגזרה מתפיסה שהחממות \"אינן שטח לבניה\"; נספחים 06 §14 ו-07 §130 הורו לתקן; הסכם רמ\"י נוקב ב-30,280 מ\"ר.")
+ BULLET(doc, "תקדים: עמ\"נ 25955-11-22 ברק-רחביה (פרשנות הרמונית); בג\"ץ 2667/17 מטה בנימין (\"פרשנות אפשרית\").")
+
+ P(doc, "**סוגיה 4 — תצורת הבינוי מול נספח בינוי מנחה ונספח נופי מחייב**")
+ BULLET(doc, "השאלה: האם המבנה המונוליטי + הבריכה חורגים ממרחב הגמישות של נספח מנחה, לנוכח הנספח הנופי המחייב?")
+ BULLET(doc, "תקדים: **ערר 1033-25 אבו גוש** (תקדים דפנה ישיר — נספח בינוי מנחה אינו המלצה בלבד); בג\"ץ 6525/15 עמק שווה.")
+
+ P(doc, "**סוגיה 5 — שיקולים זרים / NIMBY בהחלטת הסירוב**")
+ BULLET(doc, "השאלה: האם הסירוב נגוע בשיקולים זרים? (עשוי להתייתר אם הבחינה העצמאית מכריעה).")
+ BULLET(doc, "לאן נוטה: **נתמך בראיות** — תמליל (נספח 19, \"ועדה פוליטית\") + הצוות המקצועי המליץ לאשר (נספח 17). מנגד — בסיס מהותי לא-NIMBY (נספחים 25, 30).")
+
+ H2(doc, "ממצאי ניתוח-העומק — מה התחדש לאחר מיצוי 22 נספחי הרקע")
+ BULLET(doc, "**6.9 (ליבת התיק) נחלש לוועדה** — ועדת המשנה לעררים כבר אישרה הבשלה פתוחה.")
+ BULLET(doc, "**טעות הסופר בתכסית התחזקה** — שתי ערכאות הורו לתקן את חישוב השטח.")
+ BULLET(doc, "**פקיעה — הוכרע עובדתית** — סעיף הפקיעה היה בנוסח המופקד (נספח 12) והושמט מהמאושר; נותרה מחלוקת משפטית בלבד (נקר/לויתן מול חמדת הגליל).")
+ BULLET(doc, "**אופי המתקן מטה למשיבים** — תת\"ל 220 + ויתור על 8 מבני קומפוסט לטובת מתקן תרמי (נספחים 25, 30) → טיעון \"פריסת סלאמי\"/עקיפת תכנון.")
+ BULLET(doc, "**תיקון עובדתי** — פסה\"ד שדחה את העתירה (נספח 10) ניתן נגד תכנית דרך הגישה, לא נגד מי/1030.")
+ BULLET(doc, "**פער תחבורתי** — 80-92 משאיות/יום (מוסדות התכנון) מול 320-400 (ד\"ר לינק) — טעון יישוב.")
+
+ H2(doc, "פסיקה הדורשת אימות חיצוני (אינה בקורפוס הסמכותי)")
+ BULLET(doc, "ע\"א 3213/97 **נקר** — \"הדין המתפרסם ברבים מחייב\" (עוגן הוועדה לדחיית הפקיעה).")
+ BULLET(doc, "עע\"מ 4768/22 **חמדת הגליל** — פקיעת תכנית (עוגן המשיבים).")
+ BULLET(doc, "ע\"א 482/99 בלפוריה; בג\"ץ 5636/13 מתיישבי תימורים; בג\"ץ 9098/01 גניס; דנ\"א 3993/07 איקאפוד.")
+
+ H2(doc, "שאלות פתוחות להכרעת היו\"ר + סדר דיון מומלץ")
+ BULLET(doc, "(1) סיווג ההבשלה — טיפול (6.9) או אחסון (4.1.1)?")
+ BULLET(doc, "(2) דין הפקיעה לאור ההשמטה המוכחת מהנוסח המאושר.")
+ BULLET(doc, "(3) האם התכסית 3.5% היא טעות סופר הניתנת לתיקון פרשני?")
+ BULLET(doc, "(4) האם תת\"ל 220 + הוויתור על מבני הקומפוסט הופכים את הבקשה ל\"עקיפת תכנון\" (אירוס הגלבוע)?")
+ BULLET(doc, "(5) הסעד: סירוב מלא / אישור מותנה בהתאמה לשלב א' / החזרה לוועדה המקומית עם הנחיות.")
+ P(doc, "**סדר דיון מומלץ:** טענות סף (פקיעה → מיצוי/השתק → עמידה/פרסום) ← סוגיה 1 (שלביות) ← סוגיה 2 (6.9) ← סוגיה 3 (הקלות/טעות סופר) ← סוגיה 4 (תצורת בינוי) ← סוגיה 5 (שיקולים זרים).")
+
+ H2(doc, "הערכת תרחישים")
+ P(doc, "התמונה שקולה. לטובת הוועדה: סוגיה 1 (שלביות) וטיעון \"עקיפת תכנון/סלאמי\" נוכח תת\"ל 220 — חזקים. לטובת העוררים: סוגיה 2 (6.9) נחלשה, טעות הסופר התחזקה, ו-NIMBY נתמך בראיות. **התרחיש הסביר ביותר:** קבלה חלקית / החזרה מותנית או דחייה — תלוי בעיקר בשאלת היקף הבינוי בשלב א' ובשאלת \"עקיפת התכנון\". ההכרעה במובהק של יו\"ר הוועדה.")
+
+ OUT.parent.mkdir(parents=True, exist_ok=True)
+ doc.save(str(OUT))
+ print(f"saved: {OUT}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/ingest_incoming_batch.py b/scripts/ingest_incoming_batch.py
new file mode 100644
index 0000000..6a0d35d
--- /dev/null
+++ b/scripts/ingest_incoming_batch.py
@@ -0,0 +1,102 @@
+"""Batch ingest of appeals-committee decisions staged in data/precedents/incoming/.
+
+Sequential (NOT concurrent — avoids the 2026-05-31 load-spike incident) ingest of
+each .doc/.docx via the canonical internal pipeline, followed by metadata extraction
+per case (the internal path does NOT auto-queue metadata — INV-ING3). Halacha is
+auto-queued by ingest; drain it separately via MCP precedent_process_pending.
+
+case_number canonical follows the filename/Nevo convention validated against the
+corpus + missing_precedents list:
+ - מרכז/חיפה/ת"א committees number with month: NNNN/MM/YY → NNNN-MM-YY
+ - ירושלים/צפון committees number without month: NNNN/YY → NNNN-YY
+decision_date / summary / subject_tags / appeal_subtype are left empty on purpose —
+the metadata extractor fills them from the full text (more reliable than parsing here).
+
+Run: mcp-server/.venv/bin/python scripts/ingest_incoming_batch.py
+Config (POSTGRES_URL, VOYAGE_API_KEY, ANTHROPIC_API_KEY) auto-loads from ~/.env.
+"""
+
+import asyncio
+import os
+import sys
+import traceback
+from pathlib import Path
+
+sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "mcp-server", "src"))
+
+from legal_mcp.services import internal_decisions as svc
+from legal_mcp.services import precedent_metadata_extractor as meta
+
+INC = "/home/chaim/legal-ai/data/precedents/incoming"
+
+# file, case_number(canonical-with-slashes), chair, district, court, practice_area
+DECISIONS = [
+ ("105-07.doc", "105/07", "דרור לביא-אפרת", "צפון", "rishuy_uvniya"),
+ ("ARAR-17-105-44.doc", "105/17", "רונית אלפר", "מרכז", "betterment_levy"),
+ ("ARAR-18-1029.doc", "1029/18", "אליעד וינשל", "ירושלים", "rishuy_uvniya"),
+ ("ARAR-20-1018-44.doc", "1018/20", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"),
+ ("ARAR-20-1023-55.doc", "1023/20", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"),
+ ("ARAR-21-1080-55.doc", "1080/21", "נילי בן משה ידגר", "צפון", "rishuy_uvniya"),
+ ("ARAR-21-11-1051.doc", "1051/11/21", "רונית אלפר", "מרכז", "rishuy_uvniya"),
+ ("ARAR-22-01-1015.doc", "1015/01/22", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"),
+ ("ARAR-22-06-1029.doc", "1029/06/22", "מאיה אשכנזי", "מרכז", "rishuy_uvniya"),
+ ("ARAR-22-08-1044.doc", "1044/08/22", "מאיה אשכנזי", "מרכז", "rishuy_uvniya"),
+ ("ARAR-22-10-1050.doc", "1050/10/22", "מאיה אשכנזי", "מרכז", "rishuy_uvniya"),
+ ("ARAR-22-1079.doc", "1079/22", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"),
+ ("ARAR-23-04-1010.doc", "1010/04/23", "מאיה אשכנזי", "מרכז", "rishuy_uvniya"),
+ ("ARAR-23-08-1074-9.doc", "1074/08/23", "מיכל הלברשטם דגני", "חיפה", "rishuy_uvniya"),
+ ("ARAR-23-1034.docx", "1006/23", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"),
+ ("ARAR-23-1073.doc", "1073/23", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"),
+ ("ARAR-23-1085.doc", "1085/23", "נילי בן משה ידגר", "צפון", "rishuy_uvniya"),
+ ("ARAR-24-01-1009-5.docx","1009/01/24", "מיכל דגני הלברשטם", "תל אביב", "rishuy_uvniya"),
+ ("ARAR-24-05-1044.doc", "1044/05/24", "שרית אריאלי בן שמחון", "ירושלים", "rishuy_uvniya"),
+ ("ARAR-25-01-10072.docx", "1007/01/25", "יפעת בן אריה שטיינברג", "תל אביב", "rishuy_uvniya"),
+]
+
+
+async def main():
+ results = []
+ for fname, case_number, chair, district, parea in DECISIONS:
+ fp = Path(INC) / fname
+ rec = {"file": fname, "case_number": case_number}
+ if not fp.exists():
+ rec["error"] = "file-missing"
+ print(f"✗ {fname}: file missing", flush=True)
+ results.append(rec)
+ continue
+ try:
+ out = await svc.ingest_internal_decision(
+ file_path=fp,
+ case_number=case_number,
+ chair_name=chair,
+ district=district,
+ court=f"ועדת הערר לתכנון ובנייה — מחוז {district}",
+ practice_area=parea,
+ proceeding_type="ערר",
+ is_binding=False,
+ )
+ cid = out.get("case_law_id")
+ rec["case_law_id"] = cid
+ rec["chunks"] = out.get("chunks")
+ print(f"✓ ingest {case_number}: id={cid} chunks={out.get('chunks')}", flush=True)
+ # metadata (internal path does not auto-queue it)
+ m = await meta.extract_and_apply(cid)
+ rec["meta_status"] = m.get("status")
+ sug = m.get("suggested") or {}
+ rec["suggested_case_number"] = sug.get("case_number_clean")
+ rec["citation_formatted"] = sug.get("citation_formatted")
+ rec["meta_date"] = sug.get("decision_date_iso")
+ print(f" meta {case_number}: {m.get('status')} | clean={sug.get('case_number_clean')} | {sug.get('citation_formatted')}", flush=True)
+ except Exception as e:
+ rec["error"] = f"{type(e).__name__}: {e}"
+ print(f"✗ {fname} ({case_number}): {e}", flush=True)
+ traceback.print_exc()
+ results.append(rec)
+
+ print("\n===SUMMARY===", flush=True)
+ for r in results:
+ print(r, flush=True)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/scripts/ocr_benchmark_mistral.py b/scripts/ocr_benchmark_mistral.py
new file mode 100644
index 0000000..4ce89b6
--- /dev/null
+++ b/scripts/ocr_benchmark_mistral.py
@@ -0,0 +1,392 @@
+#!/usr/bin/env python3
+"""OCR Benchmark: Current system (PyMuPDF + Google Vision) vs Mistral OCR 4.0
+
+Usage:
+ python scripts/ocr_benchmark_mistral.py [--docs N] [--output PATH]
+
+Downloads PDFs from MinIO, calls Mistral OCR API, compares against
+already-extracted text stored in the DB, and writes a Markdown report.
+"""
+
+from __future__ import annotations
+
+import argparse
+import asyncio
+import base64
+import json
+import re
+import subprocess
+import sys
+import tempfile
+import time
+from pathlib import Path
+from typing import TypedDict
+
+import httpx
+
+# ── Config ───────────────────────────────────────────────────────────────────
+
+MISTRAL_API_KEY = "UsZjLCX30ev6pox0KgvXyuFP3ktsPYpN"
+MISTRAL_OCR_MODEL = "mistral-ocr-latest"
+MISTRAL_OCR_URL = "https://api.mistral.ai/v1/ocr"
+MINIO_ALIAS = "legalminio"
+MINIO_BUCKET = "legal-documents"
+
+# Documents to benchmark — selected for diversity:
+# - main appeal (40 pp, 4.4 MB, large digital doc)
+# - permit (9 pp, 1.7 MB, likely partially scanned)
+# - protocol (5 pp, 178 KB, administrative typed)
+# - response (12 pp, ~390 KB, digital legal brief)
+DOCS_TO_BENCHMARK = [
+ {
+ "title": "כתב ערר",
+ "minio_key": "cases/1027-04-26/documents/originals/כתב ערר - מושב נחם נ׳ ועדה מקומית בית שמש.pdf",
+ "doc_type": "appeal",
+ "pages": 40,
+ },
+ {
+ "title": "נספח 1 — היתר הבנייה",
+ "minio_key": "cases/1027-04-26/documents/originals/נספח 1 - היתר הבנייה (פורסם 17.03.26).pdf",
+ "doc_type": "permit",
+ "pages": 9,
+ },
+ {
+ "title": "נספח 14 — פרוטוקול ועדה מחוזית",
+ "minio_key": "cases/1027-04-26/documents/originals/נספח 14 - פרוטוקול דיון ועדה מחוזית - 06.06.23.pdf",
+ "doc_type": "protocol",
+ "pages": 5,
+ },
+ {
+ "title": "תגובת המשיבה 3",
+ "minio_key": "cases/1027-04-26/documents/originals/תגובת המשיבה 3 לערר ולבקשה להתליית היתר בנייה.pdf",
+ "doc_type": "response",
+ "pages": 12,
+ },
+]
+
+# ── DB access (read-only: fetch already-extracted text) ──────────────────────
+
+def _fetch_extracted_texts(case_number: str) -> dict[str, str]:
+ """Pull extracted_text from DB for all docs in the case."""
+ import psycopg2 # type: ignore
+ conn = psycopg2.connect(
+ host="localhost", port=5433, dbname="legal_ai",
+ user="legal_ai", password="od0ASJZFYibOlWK59krLvvETmgqwlXe8",
+ )
+ cur = conn.cursor()
+ cur.execute(
+ """
+ SELECT d.title, d.extracted_text, d.file_path, d.page_count
+ FROM documents d
+ JOIN cases c ON c.id = d.case_id
+ WHERE c.case_number = %s AND d.extracted_text IS NOT NULL
+ """,
+ (case_number,),
+ )
+ rows = cur.fetchall()
+ cur.close()
+ conn.close()
+ return {row[0]: {"text": row[1], "file_path": row[2], "pages": row[3]} for row in rows}
+
+
+# ── MinIO download ────────────────────────────────────────────────────────────
+
+def download_from_minio(minio_key: str, dest: Path) -> None:
+ """Download a file from MinIO using mcli."""
+ src = f"{MINIO_ALIAS}/{MINIO_BUCKET}/{minio_key}"
+ result = subprocess.run(
+ ["mcli", "cp", src, str(dest)],
+ capture_output=True, text=True,
+ )
+ if result.returncode != 0:
+ raise RuntimeError(f"mcli cp failed: {result.stderr}")
+
+
+# ── Text quality metrics ─────────────────────────────────────────────────────
+
+_HEBREW_RE = re.compile(r'[-]')
+_WORD_RE = re.compile(r'\S+')
+
+
+def compute_metrics(text: str) -> dict:
+ """Compute quality metrics for extracted text."""
+ if not text:
+ return {"chars": 0, "words": 0, "avg_word_len": 0,
+ "single_char_pct": 0, "words_per_line": 0,
+ "hebrew_pct": 0, "quality_ok": False}
+
+ words = _WORD_RE.findall(text)
+ n_words = len(words)
+ if n_words == 0:
+ return {"chars": len(text), "words": 0, "avg_word_len": 0,
+ "single_char_pct": 0, "words_per_line": 0,
+ "hebrew_pct": 0, "quality_ok": False}
+
+ avg_len = sum(len(w) for w in words) / n_words
+ single_char_pct = sum(1 for w in words if len(w) == 1) / n_words
+
+ lines = [ln for ln in text.split("\n") if ln.strip()]
+ words_per_line = n_words / len(lines) if lines else 0
+
+ letters = re.findall(r'[a-zA-Z-]', text)
+ hebrew_pct = (
+ sum(1 for c in letters if _HEBREW_RE.match(c)) / len(letters)
+ if letters else 0
+ )
+
+ quality_ok = (
+ n_words >= 10
+ and avg_len >= 2.5
+ and single_char_pct <= 0.4
+ and words_per_line >= 3.0
+ and hebrew_pct >= 0.5
+ )
+
+ return {
+ "chars": len(text),
+ "words": n_words,
+ "avg_word_len": round(avg_len, 2),
+ "single_char_pct": round(single_char_pct * 100, 1),
+ "words_per_line": round(words_per_line, 1),
+ "hebrew_pct": round(hebrew_pct * 100, 1),
+ "quality_ok": quality_ok,
+ }
+
+
+def count_known_abbrev_errors(text: str) -> int:
+ """Count common Hebrew abbreviation OCR errors (pre-fix indicators)."""
+ patterns = ['עוהייד', 'עוייד', 'הנייל', 'ביהמייש', 'עייי', 'בייכ', 'תמייא']
+ return sum(text.count(p) for p in patterns)
+
+
+def count_correct_abbrevs(text: str) -> int:
+ """Count correctly rendered Hebrew abbreviations."""
+ correct = ['עו"ד', 'הנ"ל', 'ביהמ"ש', 'ע"י', 'ב"כ', 'תמ"א', 'ס"ק']
+ return sum(text.count(p) for p in correct)
+
+
+# ── Mistral OCR ───────────────────────────────────────────────────────────────
+
+def call_mistral_ocr(pdf_path: Path) -> tuple[str, float]:
+ """Call Mistral OCR API on a PDF. Returns (extracted_text, elapsed_seconds)."""
+ with open(pdf_path, "rb") as f:
+ pdf_b64 = base64.b64encode(f.read()).decode()
+
+ payload = {
+ "model": MISTRAL_OCR_MODEL,
+ "document": {
+ "type": "document_url",
+ "document_url": f"data:application/pdf;base64,{pdf_b64}",
+ },
+ "include_image_base64": False,
+ }
+
+ t0 = time.time()
+ with httpx.Client(timeout=300.0) as client:
+ resp = client.post(
+ MISTRAL_OCR_URL,
+ headers={
+ "Authorization": f"Bearer {MISTRAL_API_KEY}",
+ "Content-Type": "application/json",
+ },
+ json=payload,
+ )
+ elapsed = time.time() - t0
+
+ if resp.status_code != 200:
+ raise RuntimeError(f"Mistral OCR error {resp.status_code}: {resp.text[:500]}")
+
+ data = resp.json()
+ # Response: {"pages": [{"index": 0, "markdown": "..."}, ...]}
+ pages = data.get("pages", [])
+ text = "\n\n".join(p.get("markdown", "") for p in pages)
+ return text, elapsed
+
+
+# ── Report ────────────────────────────────────────────────────────────────────
+
+def render_metric_table(current: dict, mistral: dict) -> str:
+ rows = [
+ ("תווים", f"{current['chars']:,}", f"{mistral['chars']:,}"),
+ ("מילים", f"{current['words']:,}", f"{mistral['words']:,}"),
+ ("אורך מילה ממוצע", str(current['avg_word_len']), str(mistral['avg_word_len'])),
+ ("% מילים חד-תוויות", f"{current['single_char_pct']}%", f"{mistral['single_char_pct']}%"),
+ ("מילים לשורה", str(current['words_per_line']), str(mistral['words_per_line'])),
+ ("% תווים עבריים", f"{current['hebrew_pct']}%", f"{mistral['hebrew_pct']}%"),
+ ("איכות כוללת", "✅" if current['quality_ok'] else "❌", "✅" if mistral['quality_ok'] else "❌"),
+ ]
+ lines = ["| מדד | OCR נוכחי | Mistral OCR |", "|-----|-----------|-------------|"]
+ for label, cur_val, mis_val in rows:
+ lines.append(f"| {label} | {cur_val} | {mis_val} |")
+ return "\n".join(lines)
+
+
+def build_report(results: list[dict], output_path: Path) -> None:
+ lines = [
+ "# השוואת OCR: מערכת נוכחית מול Mistral OCR",
+ f"\n**תיק:** 1027-04-26 — בל\"מ מושב נחם מפעל בטון בית שמש ",
+ f"**תאריך:** {time.strftime('%Y-%m-%d %H:%M')} ",
+ f"**מודל Mistral:** `{MISTRAL_OCR_MODEL}` ",
+ f"**מערכת נוכחית:** PyMuPDF (born-digital) + Google Cloud Vision (scanned) ",
+ "\n---\n",
+ "## סיכום מנהלים\n",
+ ]
+
+ # Summary table
+ sum_lines = ["| מסמך | עמודים | נוכחי תווים | Mistral תווים | Mistral זמן | עדיפות |",
+ "|------|--------|-------------|---------------|-------------|--------|"]
+ for r in results:
+ if "error" in r:
+ sum_lines.append(f"| {r['title']} | {r['pages']} | — | שגיאה | — | — |")
+ continue
+ cur_chars = r["current_metrics"]["chars"]
+ mis_chars = r["mistral_metrics"]["chars"]
+ winner = "🔵 נוכחי" if cur_chars > mis_chars * 1.05 else (
+ "🟢 Mistral" if mis_chars > cur_chars * 1.05 else "⚖️ שקול")
+ sum_lines.append(
+ f"| {r['title']} | {r['pages']} | {cur_chars:,} | {mis_chars:,} | "
+ f"{r['mistral_elapsed']:.1f}s | {winner} |"
+ )
+ lines.extend(sum_lines)
+ lines.append("\n---\n")
+
+ # Per-document detail
+ for r in results:
+ lines.append(f"## {r['title']} ({r['pages']} עמודים)\n")
+ if "error" in r:
+ lines.append(f"**שגיאה ב-Mistral OCR:** `{r['error']}`\n")
+ continue
+
+ lines.append(render_metric_table(r["current_metrics"], r["mistral_metrics"]))
+ lines.append("")
+
+ cur_abbr_err = count_known_abbrev_errors(r["current_text"])
+ mis_abbr_err = count_known_abbrev_errors(r["mistral_text"])
+ cur_abbr_ok = count_correct_abbrevs(r["current_text"])
+ mis_abbr_ok = count_correct_abbrevs(r["mistral_text"])
+
+ lines.append(f"\n**קיצורים עבריים:**")
+ lines.append(f"- נוכחי: {cur_abbr_ok} נכונים, {cur_abbr_err} שגויים")
+ lines.append(f"- Mistral: {mis_abbr_ok} נכונים, {mis_abbr_err} שגויים")
+ lines.append(f"\n**זמן Mistral:** {r['mistral_elapsed']:.1f} שניות\n")
+
+ # Side-by-side first 600 chars
+ lines.append("### דוגמת טקסט — 600 תווים ראשונים\n")
+ lines.append("**מערכת נוכחית:**")
+ lines.append("```")
+ lines.append((r["current_text"] or "")[:600].replace("```", "'''"))
+ lines.append("```\n")
+ lines.append("**Mistral OCR:**")
+ lines.append("```")
+ lines.append((r["mistral_text"] or "")[:600].replace("```", "'''"))
+ lines.append("```\n")
+ lines.append("---\n")
+
+ output_path.write_text("\n".join(lines), encoding="utf-8")
+ print(f"\n✅ דוח נשמר: {output_path}")
+
+
+# ── Main ──────────────────────────────────────────────────────────────────────
+
+def main() -> None:
+ parser = argparse.ArgumentParser(description="OCR benchmark: current vs Mistral")
+ parser.add_argument("--docs", type=int, default=4,
+ help="כמה מסמכים לבדוק (ברירת מחדל: 4)")
+ parser.add_argument("--output", type=str,
+ default="/home/chaim/legal-ai/data/audit/ocr-benchmark-mistral.md",
+ help="נתיב לדוח הפלט")
+ args = parser.parse_args()
+
+ docs = DOCS_TO_BENCHMARK[: args.docs]
+ output_path = Path(args.output)
+ output_path.parent.mkdir(parents=True, exist_ok=True)
+
+ print(f"🔍 שולף טקסטים קיימים מה-DB עבור תיק 1027-04-26...")
+ db_texts = _fetch_extracted_texts("1027-04-26")
+ print(f" נמצאו {len(db_texts)} מסמכים עם טקסט מחולץ")
+
+ results = []
+
+ with tempfile.TemporaryDirectory() as tmp_dir:
+ for doc in docs:
+ print(f"\n📄 מעבד: {doc['title']} ({doc['pages']} עמודים)")
+
+ # --- Current OCR text from DB ---
+ current_text = ""
+ for title, info in db_texts.items():
+ if doc["title"].split("—")[0].strip() in title or doc["minio_key"].split("/")[-1] in (info.get("file_path") or ""):
+ current_text = info["text"] or ""
+ break
+ if not current_text:
+ # Try by file_path suffix match
+ key_name = doc["minio_key"].split("/")[-1]
+ for title, info in db_texts.items():
+ if key_name in (info.get("file_path") or ""):
+ current_text = info["text"] or ""
+ break
+
+ if not current_text:
+ print(f" ⚠️ לא נמצא טקסט ב-DB, מחפש לפי שם מסמך...")
+ # fallback: match by doc_type + rough title
+ for title, info in db_texts.items():
+ if doc["doc_type"] in title.lower() or doc["title"][:8] in title:
+ current_text = info["text"] or ""
+ break
+
+ print(f" נוכחי: {len(current_text):,} תווים")
+
+ # --- Download PDF from MinIO ---
+ pdf_name = doc["minio_key"].split("/")[-1]
+ pdf_path = Path(tmp_dir) / pdf_name
+ print(f" מוריד מ-MinIO...")
+ try:
+ download_from_minio(doc["minio_key"], pdf_path)
+ print(f" הורד: {pdf_path.stat().st_size:,} bytes")
+ except Exception as e:
+ print(f" ❌ שגיאה בהורדה: {e}")
+ results.append({"title": doc["title"], "pages": doc["pages"], "error": str(e)})
+ continue
+
+ # --- Mistral OCR ---
+ print(f" קורא Mistral OCR API...")
+ try:
+ mistral_text, elapsed = call_mistral_ocr(pdf_path)
+ print(f" Mistral: {len(mistral_text):,} תווים ({elapsed:.1f}s)")
+ except Exception as e:
+ print(f" ❌ שגיאת Mistral API: {e}")
+ results.append({
+ "title": doc["title"], "pages": doc["pages"],
+ "current_text": current_text,
+ "current_metrics": compute_metrics(current_text),
+ "mistral_text": "", "mistral_metrics": compute_metrics(""),
+ "mistral_elapsed": 0, "error": str(e),
+ })
+ continue
+
+ results.append({
+ "title": doc["title"],
+ "pages": doc["pages"],
+ "current_text": current_text,
+ "current_metrics": compute_metrics(current_text),
+ "mistral_text": mistral_text,
+ "mistral_metrics": compute_metrics(mistral_text),
+ "mistral_elapsed": elapsed,
+ })
+
+ print("\n📊 בונה דוח...")
+ build_report(results, output_path)
+
+ # Also save raw texts for manual inspection
+ raw_dir = output_path.parent / "ocr-benchmark-raw"
+ raw_dir.mkdir(exist_ok=True)
+ for r in results:
+ safe = r["title"].replace("/", "-").replace(" ", "_")[:40]
+ if "current_text" in r:
+ (raw_dir / f"{safe}_current.txt").write_text(r["current_text"], encoding="utf-8")
+ if "mistral_text" in r:
+ (raw_dir / f"{safe}_mistral.txt").write_text(r["mistral_text"], encoding="utf-8")
+ print(f"💾 טקסטים גולמיים נשמרו: {raw_dir}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/skills/docx/references/advanced-features.md b/skills/docx/references/advanced-features.md
new file mode 100644
index 0000000..aeaef6d
--- /dev/null
+++ b/skills/docx/references/advanced-features.md
@@ -0,0 +1,170 @@
+# Advanced Features — פיצ'רים מתקדמים
+
+## הערות שוליים (Footnotes)
+
+**שימוש מרכזי:** הפניות לחקיקה ופסיקה.
+
+```javascript
+const { FootnoteReferenceRun } = require('docx');
+
+const doc = new Document({
+ footnotes: {
+ 1: { children: [new Paragraph({
+ bidirectional: true, alignment: AlignmentType.START,
+ children: [new TextRun({
+ text: "חוק החוזים (חלק כללי), התשל״ג-1973, סעיף 12.",
+ font: "David", size: 20, rightToLeft: true
+ })]
+ })] },
+ },
+ // ...sections
+});
+
+// הפניה בגוף הטקסט:
+new Paragraph({
+ bidirectional: true, alignment: AlignmentType.BOTH,
+ children: [
+ new TextRun({ text: "חובת תום הלב", font: "David", size: 24, rightToLeft: true }),
+ new FootnoteReferenceRun(1),
+ new TextRun({ text: " חלה על כל שלבי המשא ומתן.", font: "David", size: 24, rightToLeft: true }),
+ ]
+})
+```
+
+### תיקון RTL בהערות שוליים (post-unpack)
+docx-js לא מגדיר RTL מלא. אחרי unpack, תקן ב-`word/footnotes.xml`:
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## תוכן עניינים (TOC)
+
+**⚠️ TOC ידני בלבד** — `TableOfContents` של docx-js מאבד הגדרות RTL בעדכון Word.
+
+```javascript
+const { Tab, TabStopType, LeaderType, LineRuleType } = require('docx');
+
+const tocEntry = (text, pageNum, opts = {}) => new Paragraph({
+ bidirectional: true,
+ spacing: { after: 60, line: 276, lineRule: LineRuleType.AUTO },
+ ...(opts.indent ? { indent: { right: opts.indent } } : {}),
+ tabStops: [{ type: TabStopType.RIGHT, position: 9026, leader: LeaderType.DOT }],
+ children: [
+ new TextRun({ text, font: "David", size: 24, rightToLeft: true, bold: opts.bold || false }),
+ new TextRun({ children: [new Tab()], font: "David", rightToLeft: true }),
+ new TextRun({ text: String(pageNum), font: "David", size: 24, rightToLeft: true }),
+ ]
+});
+
+// שימוש:
+tocEntry("פרק א׳ — הגדרות כלליות", 2, { bold: true }),
+tocEntry("1. הגדרות יסוד", 2, { indent: 400 }),
+```
+
+---
+
+## מספר סקשנים (Multiple Sections)
+
+**שימוש:** כותרות שונות לנספחים, שוליים שונים.
+
+```javascript
+const doc = new Document({
+ sections: [
+ {
+ properties: {
+ page: { size: { width: 11906, height: 16838 }, margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } },
+ bidi: true,
+ },
+ headers: {
+ default: new Header({ children: [new Paragraph({
+ bidirectional: true, alignment: AlignmentType.CENTER,
+ children: [new TextRun({ text: "הסכם שירותים", font: "David", size: 20, bold: true, rightToLeft: true })]
+ })] })
+ },
+ children: [ /* גוף ההסכם */ ]
+ },
+ {
+ properties: {
+ page: { size: { width: 11906, height: 16838 }, margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } },
+ bidi: true,
+ },
+ headers: {
+ default: new Header({ children: [new Paragraph({
+ bidirectional: true, alignment: AlignmentType.START,
+ children: [new TextRun({ text: "נספח א׳ — לוח תעריפים", font: "David", size: 20, bold: true, rightToLeft: true })]
+ })] })
+ },
+ children: [ /* הנספח */ ]
+ }
+ ]
+});
+```
+
+---
+
+## לוגו/תמונה בכותרת (Letterhead)
+
+```javascript
+const { ImageRun } = require('docx');
+const logoBuffer = fs.readFileSync('/path/to/logo.png');
+
+headers: {
+ default: new Header({
+ children: [
+ new Paragraph({
+ alignment: AlignmentType.CENTER,
+ children: [new ImageRun({ data: logoBuffer, transformation: { width: 200, height: 60 }, type: "png" })],
+ }),
+ new Paragraph({
+ bidirectional: true, alignment: AlignmentType.CENTER,
+ children: [new TextRun({ text: "משרד עורכי דין ישראלי ושות׳", font: "David", size: 20, bold: true, rightToLeft: true })],
+ }),
+ ],
+ }),
+}
+```
+
+**הערה:** תמונה חייבת להיות קובץ אמיתי — לבקש מהמשתמש אם אין.
+
+---
+
+## היפרלינקים
+
+```javascript
+const { ExternalHyperlink, UnderlineType } = require('docx');
+
+new Paragraph({
+ bidirectional: true,
+ children: [
+ new TextRun({ text: "ראה: ", font: "David", size: 24, rightToLeft: true }),
+ new ExternalHyperlink({
+ link: "https://www.nevo.co.il/law_html/law01/073_002.htm",
+ children: [new TextRun({
+ text: "חוק החוזים באתר נבו",
+ font: "David", size: 24, rightToLeft: true,
+ color: "0563C1",
+ underline: { type: UnderlineType.SINGLE },
+ })],
+ }),
+ ]
+})
+```
+
+**⚠️ אל תשתמש ב-`style: "Hyperlink"`** — מפריע ל-RTL. הגדר `color` + `underline` ידנית.
diff --git a/skills/docx/references/document-templates.md b/skills/docx/references/document-templates.md
new file mode 100644
index 0000000..ebe8434
--- /dev/null
+++ b/skills/docx/references/document-templates.md
@@ -0,0 +1,219 @@
+# Document Templates — תבניות מסמכים משפטיים
+
+## תבנית 1: כתב טענות (בקשה, תביעה, הגנה, ערעור)
+
+```javascript
+const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
+ AlignmentType, LevelFormat, BorderStyle, WidthType } = require('docx');
+
+const PAGE_WIDTH = 11906;
+const MARGINS = { top: 1134, right: 1134, bottom: 1134, left: 1134 };
+const CONTENT_WIDTH = PAGE_WIDTH - MARGINS.left - MARGINS.right;
+
+const noBorder = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" };
+const noBorders = { top: noBorder, bottom: noBorder, left: noBorder, right: noBorder };
+
+// Header בית משפט — טבלה עם שם בית המשפט (ימין) ומספר תיק (שמאל)
+function courtHeader(courtName, caseNumber) {
+ return new Table({
+ width: { size: CONTENT_WIDTH, type: WidthType.DXA },
+ columnWidths: [CONTENT_WIDTH / 2, CONTENT_WIDTH / 2],
+ visuallyRightToLeft: true,
+ rows: [
+ new TableRow({
+ children: [
+ new TableCell({
+ width: { size: CONTENT_WIDTH / 2, type: WidthType.DXA },
+ borders: noBorders,
+ children: [new Paragraph({
+ bidirectional: true, alignment: AlignmentType.START,
+ children: [new TextRun({ text: courtName, bold: true, font: "David", size: 26, rightToLeft: true })]
+ })]
+ }),
+ new TableCell({
+ width: { size: CONTENT_WIDTH / 2, type: WidthType.DXA },
+ borders: noBorders,
+ children: [new Paragraph({
+ bidirectional: true, alignment: AlignmentType.END,
+ children: [new TextRun({ text: caseNumber, bold: true, font: "David", size: 26, rightToLeft: true })]
+ })]
+ })
+ ]
+ })
+ ]
+ });
+}
+
+function mainTitle(text) {
+ return new Paragraph({
+ bidirectional: true, alignment: AlignmentType.CENTER,
+ spacing: { before: 300, after: 300 },
+ children: [new TextRun({ text, bold: true, font: "David", size: 28, rightToLeft: true, underline: {} })]
+ });
+}
+
+function subHeading(text) {
+ return new Paragraph({
+ bidirectional: true, alignment: AlignmentType.START,
+ spacing: { before: 240, after: 120 },
+ children: [new TextRun({ text, bold: true, font: "David", size: 24, rightToLeft: true, underline: {} })]
+ });
+}
+
+const doc = new Document({
+ numbering: {
+ config: [{
+ reference: "legal-clauses",
+ levels: [{
+ level: 0, format: LevelFormat.DECIMAL, text: "%1.",
+ alignment: AlignmentType.START, suffix: "tab",
+ style: { paragraph: { indent: { left: 360, hanging: 360 } } }
+ }]
+ }]
+ },
+ sections: [{
+ properties: {
+ page: { size: { width: PAGE_WIDTH, height: 16838 }, margin: MARGINS },
+ bidi: true
+ },
+ children: [
+ courtHeader("בית המשפט המחוזי בתל אביב", "ת\"א 12345-01-26"),
+ mainTitle("כתב תביעה"),
+ // ... פרטי צדדים, סעיפים, חתימה
+ ]
+ }]
+});
+```
+
+---
+
+## תבנית 2: מכתב התראה
+
+```javascript
+function letterHeader(firmName, address, phone, email) {
+ return [
+ new Paragraph({
+ bidirectional: true, alignment: AlignmentType.START,
+ children: [new TextRun({ text: firmName, bold: true, font: "David", size: 28, rightToLeft: true })]
+ }),
+ new Paragraph({
+ bidirectional: true, alignment: AlignmentType.START,
+ children: [new TextRun({ text: address, font: "David", size: 22, rightToLeft: true })]
+ }),
+ new Paragraph({
+ bidirectional: true, alignment: AlignmentType.START,
+ spacing: { after: 300 },
+ children: [new TextRun({ text: `טל': ${phone} | ${email}`, font: "David", size: 22, rightToLeft: true })]
+ }),
+ ];
+}
+
+function subjectLine(text) {
+ return new Paragraph({
+ bidirectional: true, alignment: AlignmentType.CENTER,
+ spacing: { before: 200, after: 200 },
+ children: [
+ new TextRun({ text: "הנדון: ", bold: true, font: "David", size: 24, rightToLeft: true }),
+ new TextRun({ text, bold: true, font: "David", size: 24, rightToLeft: true, underline: {} })
+ ]
+ });
+}
+
+// שימוש:
+sections: [{
+ properties: { page: { size: { width: 11906, height: 16838 }, margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } }, bidi: true },
+ children: [
+ ...letterHeader("משרד עו\"ד כהן ושות'", "רח' הרצל 1, תל אביב", "03-1234567", "office@cohen-law.co.il"),
+ new Paragraph({
+ bidirectional: true, alignment: AlignmentType.START,
+ children: [new TextRun({ text: "תאריך: 10.2.2026", font: "David", size: 24, rightToLeft: true })]
+ }),
+ new Paragraph({
+ bidirectional: true, alignment: AlignmentType.START,
+ spacing: { before: 200 },
+ children: [new TextRun({ text: "לכבוד: [שם הנמען]", font: "David", size: 24, rightToLeft: true })]
+ }),
+ subjectLine("התראה בטרם נקיטת הליכים משפטיים"),
+ ]
+}]
+```
+
+---
+
+## תבנית 3: הסכם/חוזה
+
+```javascript
+const CONTENT_WIDTH = 9638; // A4 עם שוליים 2.5 ס"מ
+const noBorders = { /* ראה תבנית 1 */ };
+
+function contractTitle(text) {
+ return new Paragraph({
+ bidirectional: true, alignment: AlignmentType.CENTER,
+ spacing: { after: 300 },
+ children: [new TextRun({ text, bold: true, font: "David", size: 32, rightToLeft: true })]
+ });
+}
+
+function partyClause(label, name, id, address, alias) {
+ return new Paragraph({
+ bidirectional: true, alignment: AlignmentType.BOTH,
+ spacing: { after: 120 },
+ children: [
+ new TextRun({ text: `${label}: `, bold: true, font: "David", size: 24, rightToLeft: true }),
+ new TextRun({ text: `${name}, ח.פ./ת.ז. ${id}, מ${address} (להלן: "`, font: "David", size: 24, rightToLeft: true }),
+ new TextRun({ text: alias, bold: true, font: "David", size: 24, rightToLeft: true }),
+ new TextRun({ text: '")', font: "David", size: 24, rightToLeft: true }),
+ ]
+ });
+}
+
+function signatureTable(contentWidth) {
+ return new Table({
+ width: { size: contentWidth, type: WidthType.DXA },
+ columnWidths: [contentWidth / 2, contentWidth / 2],
+ visuallyRightToLeft: true,
+ rows: [new TableRow({
+ children: [
+ new TableCell({
+ borders: noBorders,
+ children: [
+ new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
+ children: [new TextRun({ text: "_________________", font: "David", size: 24, rightToLeft: true })] }),
+ new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
+ children: [new TextRun({ text: "צד א'", font: "David", size: 24, rightToLeft: true })] })
+ ]
+ }),
+ new TableCell({
+ borders: noBorders,
+ children: [
+ new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
+ children: [new TextRun({ text: "_________________", font: "David", size: 24, rightToLeft: true })] }),
+ new Paragraph({ bidirectional: true, alignment: AlignmentType.CENTER,
+ children: [new TextRun({ text: "צד ב'", font: "David", size: 24, rightToLeft: true })] })
+ ]
+ })
+ ]
+ })]
+ });
+}
+
+sections: [{
+ properties: { page: { size: { width: 11906, height: 16838 }, margin: { top: 1417, right: 1417, bottom: 1417, left: 1417 } }, bidi: true },
+ children: [
+ contractTitle("הסכם שירותים"),
+ new Paragraph({
+ bidirectional: true, alignment: AlignmentType.CENTER,
+ children: [new TextRun({ text: "נערך ונחתם בתל אביב ביום __________", font: "David", size: 24, rightToLeft: true })]
+ }),
+ partyClause("מצד אחד", "[שם]", "[מספר]", "[כתובת]", "המזמין"),
+ partyClause("מצד שני", "[שם]", "[מספר]", "[כתובת]", "הספק"),
+ // הואילים + סעיפים...
+ new Paragraph({
+ bidirectional: true, alignment: AlignmentType.CENTER,
+ spacing: { before: 400, after: 300 },
+ children: [new TextRun({ text: "ולראיה באו הצדדים על החתום:", bold: true, font: "David", size: 24, rightToLeft: true })]
+ }),
+ signatureTable(CONTENT_WIDTH)
+ ]
+}]
+```
diff --git a/skills/docx/references/tracked-changes.md b/skills/docx/references/tracked-changes.md
new file mode 100644
index 0000000..7d8ade5
--- /dev/null
+++ b/skills/docx/references/tracked-changes.md
@@ -0,0 +1,57 @@
+# Tracked Changes — עקוב אחר שינויים
+
+## שם מחבר בעברית
+```xml
+
+```
+
+## שינוי ערך (סכום, תאריך, תקופה)
+פצל את הטקסט ועטוף רק את הערך שמשתנה:
+```xml
+...RTL PROPS...
+ שכר הטרחה יעמוד על סך של
+
+ ...RTL PROPS...750
+
+
+ ...RTL PROPS...850
+
+...RTL PROPS...
+ ש״ח לשעת עבודה
+```
+
+## מחיקת סעיף שלם
+```xml
+
+
+
+
+
+
+
+
+
+ ...RTL PROPS...
+ הסעיף שנמחק
+
+
+```
+
+## RTL PROPS — בלוק rPr מלא לכל run
+```xml
+
+
+
+
+
+
+```
+
+## קבלה/דחייה של שינויים
+
+| פעולה | לפני | אחרי |
+|-------|------|------|
+| קבלת הוספה | `...טקסט` | `...טקסט` |
+| דחיית הוספה | `...` | *(מחק הכל)* |
+| קבלת מחיקה | `...טקסט` | *(מחק הכל)* |
+| דחיית מחיקה | `...טקסט` | `...טקסט` |