Backend (cases listing)
- /api/cases: also return updated_at, created_at, practice_area,
appeal_subtype, subject. The detail-mode response was previously
dropping these even though db.list_cases reads them, leaving the
UI's "תחום" and "עודכן" columns blank.
Frontend
- Split the home table into two: רישוי (1xxx) and היטל השבחה ופיצויים
(8xxx + 9xxx), bucketing on appeal_subtype with a case-number-prefix
fallback. The "תחום" column is now redundant and removed.
- New AppealTypeBars chart in the right rail next to the existing
status donut.
- Donut: switch to a vertical layout (donut on top, legend below in a
3-col grid) so labels like "חדש / בעיבוד" no longer wrap inside the
320px sidebar; counts now align in a tabular column.
- CasesTable accepts emptyText/searchPlaceholder so each split table
has its own copy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Raise HTTPException(503) when Paperclip DB is unreachable instead of
silently falling through to disk-only mode and returning [].
- Honor PAPERCLIP_SKILLS_DIR env var (falls back to ~/.paperclip/...).
In the Coolify container the host's skills dir is bind-mounted at
/paperclip-skills; without this, Path.home() resolved to /root/ and
the disk inventory was always empty.
Both bugs together silently turned a Paperclip DB outage into "no skills
installed" on the /skills page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Paperclip heartbeat staleness gate (heartbeat.js evaluateQueuedRunStaleness)
cancels queued runs when issue.assigneeAgentId !== run.agentId, with error
"issue assignee changed before the queued run could start". Older Paperclip
versions auto-assigned on wakeup; the current version does not, so issues
created with NULL assignee silently never run.
Set assignee_agent_id to the company's CEO at INSERT time. Affects both the
project setup issue and the "התחל תהליך ניסוח" workflow issue.
Default 10mb caused upload-tagged 500s on scanned PDFs in case 1027-26
(Next 16 truncates body, FastAPI sees broken multipart, socket hang up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The case repo is the user's backup, so anything in the dir must end up
on Gitea. Two layers:
1. Periodic sweep (every 30s) — git_sync.sweep_loop runs as a FastAPI
background task. It scans every case dir, runs git status --porcelain
on each, and commit_and_push's any dirty changes with an auto-built
Hebrew message ("אוטו: טיוטות (2) · מסמכים"). Catches files written
outside the API path: agent research artefacts, manual edits, etc.
2. Explicit commits at known write paths — DOCX export, interim draft,
apply_user_edit, revise_draft, mark-final, analysis DOCX export.
These give immediate feedback with descriptive messages instead of
waiting up to 30s for the sweep.
safe.directory injection added to _git_env so sweep + explicit commits
work even when the running uid differs from the case-dir owner (host
runs vs. uniform-root container).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two latent issues surfaced today while watching the case 8174-24
end-to-end run, both worth documenting and engineering around because
they will recur on every future case.
Bug 1 — issue.released flips done→todo
After an agent successfully PATCHes its issue to "done", Paperclip's
internal issue.released action reverts the status to "todo" within
~30 seconds. This triggers a fresh wakeup of the same agent on a
task that is already complete.
Reproduced on CMPA-18 (30/04/26):
18:14:57 agent PATCH → status: done
18:15:35 Paperclip → issue.released → status: todo
18:15:54 new researcher run started
The fix at the right altitude (Paperclip itself) is outside our repo.
Mitigation in HEARTBEAT.md §3 — when an agent boots and finds the
issue in `todo` while expected outputs (file, DB rows) already exist,
it must short-circuit: post a "no change" comment, PATCH back to done,
and exit. Costs ~$0.20 per false wakeup but breaks the loop.
Bug 2 — Bash backtick trap on long comment bodies
Researcher agent built a curl pipeline like:
curl ... -d "$(python3 -c "body = '''...
📁 קובץ מחקר: `/path/to/file.md`
'''")"
The backticks around the file path (markdown convention) get
evaluated by the OUTER bash $(...) as command substitution. Bash
then tries to exec /path/to/file.md, which is not executable, and
prints "Permission denied" — a misleading error since the actual
file ownership is fine. The curl itself succeeded; only the bash
prelude noised up the log.
Fix in HEARTBEAT.md §4א: long bodies must go via Write→tempfile
then `curl -d @file`. Avoids every shell quoting edge case.
Files:
• docs/paperclip-quirks.md — new. Full writeup of both bugs plus
two prior known-quirks (CEO auto-block in_progress, INSERT vs
API for wakeups). Each section: what happens, empirical evidence
from logs, impact, workaround, status.
• .claude/agents/HEARTBEAT.md — added the self-recovery section to
§3 and the temp-file pattern to §4א. The temp-file pattern is the
canonical answer for any agent posting markdown comments —
applies to all 7 agents in this skill set.
• CLAUDE.md — referenced the new doc from the docs index.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The auto-creation in case_create had two failure modes that combined to
make repos silently missing: a stale GITEA_TOKEN returning 401, and the
outer try/except in case_create that swallowed every exception with a
bare pass. Result: cases like 8174-24 ended up with a local git repo and
Paperclip project but no Gitea repo, with no signal anywhere.
_setup_gitea_remote now returns {ok, url, error} and never raises; the
result is attached to the case JSON and the FastAPI endpoint logs a
warning when ok=false. The UI gets a "צור ריפו ב-Gitea" button on the
case header that appears only when the repo or remote is missing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two structural gaps in legal-researcher's "שלב 5: דיווח" surfaced while
auditing the case 8174-24 run:
1. **No DB linkage.** The skill told the researcher to post a comment
summarizing precedents but never to call mcp__legal-ai__precedent_attach.
The MCP tool itself wasn't even in the tools frontmatter — so even
a researcher that wanted to write to case_precedents physically
couldn't. Result: 0 rows in case_precedents after a successful
research run, even with 8 precedents identified and verified in
the comment text. The writer then has to grep free-text instead
of querying a structured table.
2. **No persisted file.** Research output existed only as a Paperclip
comment. The writer/QA can't `Read` it from disk; they have to go
through Paperclip API to fetch comment bodies. Compare to the
analyst, which is required to write `analysis-and-research.md`.
Fix:
• Added precedent_attach, precedent_list, precedent_search_library
to the tools frontmatter.
• Rewrote step 5 with explicit ordering: save to disk → attach
verified precedents to DB → update status → email → post comment.
• Documented the precedent_attach call signature inline (case_number,
citation, quote, section_id) so the agent doesn't have to reverse-
engineer it. Includes guidance on which precedents to attach
(verified with quote) vs which to leave for external verification.
Effect: future research runs will populate case_precedents and
data/cases/{N}/documents/research/precedent-research.md, both of which
the writer needs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The retry loop bug we fixed in legal-analyst yesterday existed in every
single sub-agent skill. They all post a comment + wake the CEO + exit,
leaving their own issue in `in_progress`. Paperclip's "in_progress with
no live execution" watchdog then re-wakes them, repeating until something
external transitions the issue. Watched it happen on CMPA-17 (researcher)
today — 4 iterations + manual SIGTERM + manual PATCH.
Same fix applied to all 5 remaining agents:
• legal-researcher.md
• legal-writer.md
• legal-qa.md
• legal-exporter.md
• legal-proofreader.md (file was incomplete — also added the missing
שלב 5: דיווח and wake-CEO sections to bring it to parity with the
other agents)
Each gets a "סגור את ה-issue של עצמך — חובה!" section with two PATCH
templates: one for `done` after a successful run, one for `blocked` if
checks fail or output is incomplete. The section sits before the
wake-CEO block, with an explicit reference to the CMPA-17 incident so
the rule has a concrete anchor.
Result: every agent now has the same close-issue contract. No more
zombie in_progress issues, no more 4× wakeup loops.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two structural bugs surfaced while monitoring the fresh end-to-end
run on case 8174-24:
1. **No appraiser_facts extraction.** legal-analyst.md's "what to
extract" table didn't mention doc_type='appraisal' at all, and
`extract_appraiser_facts` wasn't in its tools frontmatter. The
CEO compounded this by writing in CMPA-16's body that all 3
appraisals were "reference materials, do not extract" — which
is correct for `extract_claims` but wrong for the appraisal-
specific extractor. Result: 0 appraiser_facts in DB after a
full run, even though the user had carefully tagged each
appraisal's `appraiser_side` (committee/appellant) precisely
so detect_conflicts could compare them.
2. **Issue stays in_progress, Paperclip retries forever.** Step 7
("שמירה ודיווח") instructed the analyst to update the *case*
status, post a comment, send email, and wake the CEO — but
never to PATCH the issue itself to `done`. Paperclip's
"in_progress with no live execution" watchdog then re-woke the
analyst, which posted "I'm done" again, which re-triggered
another wakeup. We saw three iterations on CMPA-16 before the
issue finally transitioned. The PATCH pattern was already
documented in HEARTBEAT.md §4ב — the analyst skill just never
referenced it.
Changes:
• legal-analyst.md
- Added mcp__legal-ai__extract_appraiser_facts to tools list.
- Rewrote the "what to extract" table to use doc_type as the
key column and added an `appraisal` row + a callout explaining
why it goes through a different extractor.
- Added explicit step 5 "חלץ עובדות שמאי" with the call.
- Step 7 now PATCHes the issue to `done` (or `blocked` on
failure) before waking the CEO. Refers to the actual incident
so the rule has a concrete anchor.
- Cleaned up the chunking guidance — phase 1 of claude_session
already handles big docs automatically; no need to manually
split.
• legal-ceo.md (analyst issue template section)
- Replaced the generic "list of docs not to extract from" with a
per-doc_type action table that explicitly says
`appraisal → extract_appraiser_facts (NOT extract_claims)`.
- Added an explicit guard: "for every appraisal in the case,
verify the issue body says to run extract_appraiser_facts —
otherwise the writer gets a numbers-free block ז".
- Added the close-the-issue-with-PATCH instruction so the CEO
knows to write that into every analyst issue.
These edits don't affect the run currently in flight (the CEO's
prompt was already cached and the analyst already ran). They take
effect on the next analyst invocation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without a primary workspace on a project, the "סביבות עבודה" tab in
Paperclip stays hidden (gate: enableIsolatedWorkspaces && S0t list
non-empty), and agents wake with cwd=`/home/chaim` instead of the
legal-ai source tree. New helper inserts a primary workspace pointing at
LEGAL_AI_WORKSPACE_CWD (default /home/chaim/legal-ai) on both new and
legacy/existing-project paths. Idempotent — skips if any workspace row
already exists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the full deletion procedure we worked out empirically while
wiping case 8174-24 for a clean rerun. Covers all four systems where
case state lives, in dependency order:
1. legal-ai DB + on-disk dir — DELETE /api/cases?remove_files=true
(now actually works after 903fb4d added the missing db.delete_case)
2. Paperclip DB — no API; raw SQL with explicit FK-blocker ordering
(issue_comments, cost_events, finance_events, feedback_votes,
issue_inbox_archives, issue_read_states must go before issues;
heartbeat_runs.wakeup_request_id must be NULLed before
agent_wakeup_requests can be deleted)
3. Gitea — DELETE /api/v1/repos/cases/{N}
4. Verification queries for each system
Two gotchas worth highlighting in the doc:
• The case directory inside /data/cases is owned by root because the
container runs as root — host-side rm needs sudo, or use the API
(rmtree happens inside the container).
• Paperclip projects are referenced via name LIKE '%{N}%' since
there's no slug column. Stricter matching is recommended if N
appears in multiple project names.
Linked from legal-ai/CLAUDE.md docs index. A future scripts/delete-case.sh
that automates the runbook with a confirmation prompt is noted as TODO
inside the runbook itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The case_delete tool in tools/cases.py and the DELETE /api/cases endpoint
in web/app.py both invoke await db.delete_case(case_id), but no such
function existed in services/db.py — every call returned 500 with an
AttributeError. Discovered while wiping case 8174-24 for a clean rerun.
Implementation is straightforward because the FK graph already does the
work: 7 dependent tables CASCADE on cases.id (documents, document_chunks,
claims, appraiser_facts, decisions, qa_results, case_precedents) and 2
SET NULL (audit_log, chair_feedback). A single DELETE FROM cases is
enough — no manual ordering needed.
Documented in the docstring that this only touches the legal-ai DB —
Paperclip projects/issues and Gitea repos for the case are separate
systems and must be cleaned up by the caller.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The claude_session bridge had two structural defects that made any
non-trivial document extraction unreliable:
1. subprocess.run() blocks the asyncio event loop in the MCP server
for the full duration of every LLM call (60-180s typical).
2. The 120-second timeout was below the cold-cache cost of any
document over ~12K Hebrew characters. Three back-to-back timeouts
on case 8174-24 dropped 43 appellant claims on the floor.
Phase 1 of the remediation plan — keeps claude_session as the engine
(no Anthropic API switch) and restructures around it:
claude_session.py
• query / query_json are now async — asyncio.create_subprocess_exec
instead of subprocess.run, so MCP server can serve other coroutines
while a call is in flight.
• DEFAULT_TIMEOUT 120 → 1800 (30 min). High enough that no realistic
document hits it; bounded so a runaway never zombifies forever.
• LONG_TIMEOUT 300 → 3600 for opus block writing on full case context.
• TimeoutError now actually kills the subprocess (asyncio.wait_for
cancellation alone leaves the child running).
claims_extractor.py
• _split_by_sections: chunks at numbered sections / Hebrew letter
headings / "פרק" markers / markdown ##, falls back to paragraph
breaks, then to hard splits. Targets 12K chars per chunk — small
enough that each chunk reliably finishes inside the timeout.
• _extract_chunk: per-chunk retry (1 attempt by default) with
structured logging on failure. Failed chunks no longer crash the
overall extraction; they're skipped with a partial-result warning.
• extract_claims_with_ai now runs chunks in parallel via
asyncio.gather bounded by a semaphore (CHUNK_CONCURRENCY=3).
For a 25K-char appeal: was sequential 150-300s, now ~70-90s.
Updated all 9 callers (claims, appraiser facts, block writer, qa
validator, brainstorm, learning loop, style analyzer × 3) to await
the now-async API.
The one-shot scripts/extract_claims_8174.py used to recover 43
appellant claims on case 8174-24 has been moved to .archive/ — phase 1
makes it obsolete. SCRIPTS.md updated.
Phase 2 (background-task wrapper around LLM-bound MCP tools, persistent
llm_tasks table, SSE progress) is the structural follow-up — separate PR.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous in-memory _progress dict + polling SSE handler had a 30s silent
tail after completion. HTTP/2 framing in the proxy chain (Traefik) buffered
the small chunks until the stream closed, so when a transient blip caused
EventSource to reconnect, the server returned 404 and the UI stuck on the
"מתחיל…" placeholder forever. Reproduced live: 445 bytes withheld 31s.
Changes:
• web/progress_store.py — ProgressStore wraps Redis with TTL (5m), atomic
GETDEL, dict-like API. Best-effort: Redis errors are logged and swallowed
so observability outages don't break uploads.
• web/app.py — _progress is now Redis-backed; every set/get/active/pop is
awaited. SSE handler emits a heartbeat each tick (forces HTTP/2 flush),
drops the 30s post-completion sleep, and returns a terminal
{"status":"unknown"} payload instead of 404 when the task is gone — so
EventSource closes cleanly instead of reconnect-looping. New _SSE_HEADERS
set X-Accel-Buffering: no.
• web-ui useProgress(taskId, caseNumber) — 10s fallback that invalidates
the case detail if no SSE message arrived; treats "unknown" as terminal
and triggers a refetch from the source of truth.
• upload-sheet wires caseNumber through and renders "unknown" as completed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hebrew was rendering LTR or in Times New Roman fallback in some Word
contexts. Root cause: incomplete RTL marking and missing font hints
on the run level.
Three layers of RTL are required (per skills/docx/SKILL.md):
1. Section: <w:bidi/> in sectPr (now inherited from template)
2. Paragraph: <w:bidi/> directly in pPr (paragraph direction)
3. Run: <w:rtl/> in rPr — tells Word to use cs (complex-script) font
Without an explicit font on the run, Hebrew renders in the ascii slot
(Times New Roman). Force David on all four slots (ascii / hAnsi / cs /
eastAsia) so every shaping path picks the correct font.
Changes:
- TEMPLATE_PATH now points to skills/docx/decision_template.docx
(carries David, RTL, margins, styles); replaces hard-coded constants.
- _mark_run_rtl: writes rFonts on all four slots, then appends <w:rtl/>.
- _mark_paragraph_rtl: places <w:bidi/> directly in pPr (not nested in
rPr — that was the bug), and adds <w:rtl/> to the paragraph-mark rPr.
- _set_paragraph_jc: forces explicit jc, overriding style-inherited.
Tests:
- test_mark_paragraph_rtl_adds_bidi_directly_in_pPr — guards against
the regression where bidi was nested inside rPr.
- test_mark_run_rtl_forces_david_on_all_font_slots — ensures all four
font slots are set, not just cs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comparison of our draft (טיוטה-v6, 2,126 words) against Dafna's final
decision (עריכה-v2, 2,299 words). 14 lessons (#20-#33) covering what
the draft got right and where she rebuilt the discussion.
Key findings:
- Lesson #20: Match doctrinal depth to legal uncertainty. In clean
acceptance the committee's OWN conditions provide the anchor — no
CREAC framework needed. The draft's 101-word "נבאר" doctrinal
paragraph was deleted entirely.
- Lesson #21: Plant analytical seeds in the background ("ודוק"
foreshadowing) for technical planning distinctions.
- Lesson #23: Concrete documentary evidence (specific permits in
buildings 5, 7, 11) beats generic statements.
- Lesson #25: Counter-factual reasoning — "approved by mistake" gives
the committee benefit of the doubt while strengthening reversal.
- Lesson #26: Engineer counter-factual — "had he known the shadow plan
was not feasible, his opposition would have been even stronger".
- Lesson #27: "אכן...אולם" / "לא נעלם מעינינו" patterns are for
rejection, NOT acceptance. Don't use prophylactically.
- Lesson #28: "ונפרט;" (ו prefix + semicolon), never "נפרט." with
period.
- Lesson #33: Full acceptance against permit applicant → no expenses
to either side.
New transition phrases catalogued: "דיון עקר", "אושרה מתוך טעות כי הרי
לא נוכל להניח כי אושרה למראית עין", "ועדת הערר אפשרה מרחב של זמן
בתקווה כי ההחלטה תתייתר", "להלן כדוגמא מתוך", "ברי כי הכוונה ל...".
Several of these lessons fed directly into daphna-acceptance-architecture.md
(template A) and daphna-decision-tree.md from the recent voice corpus
work; this file remains the case-study record.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cases failed to push silently after the Gitea token in Infisical was
rotated: the embedded credential in each case repo's origin URL was
the old token, the rotation never propagated, and capture_output=True
hid the auth failure as a logger.warning. Three cases (1033-25,
1130-25, 1194-25) accumulated unpushed commits over weeks before
this was noticed.
Fixes the root cause in two places: web/gitea_client.py for uploads
through the FastAPI endpoint, and mcp-server/services/git_sync.py
for case_update / document_upload through MCP tools (which previously
committed but never pushed at all).
The new commit_and_push helper:
- re-injects the current GITEA_ACCESS_TOKEN into the existing origin
URL on every call, so pushes survive token rotation
- logs push failures at WARNING with the actual stderr (the previous
code suppressed errors entirely)
- continues to push even when the commit was a no-op, in case earlier
commits are still unpushed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Until now only legal-writer referenced the voice corpus. Without these
references the qa agent can't validate writer output, the researcher
chooses precedents outside Daphna's canon, and the analyst's claims
classification doesn't match block-zayin rules.
- legal-qa: adds 8th check "voice_compliance" — block ז structure,
block י voice (אכן/אולם, "אנחנו" verbs, no numbered lists), correct
precedent from canon, acceptance template match.
- legal-researcher: must check daphna-precedent-network.md before
proposing any precedent; cross-reference with Daphna's own past
decisions via search_decisions.
- legal-analyst: reads block-zayin-claims.md — its output is the
writer's input for block ז.
- legal-ceo: lists all 6 voice docs and which agent reads each.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new voice docs based on deep reading of 1033-25 (full-acceptance) and
7 representative cases for block-zayin (claims summary):
- daphna-acceptance-architecture.md: 5 distinct templates for case acceptance
(A: internal flaw + voiding; B: remand to committee; C: corrections in
request; D: substantive 8xxx; E: appraiser remand). Fixes the wrong
reference in architecture-by-outcome that treated full-acceptance as a
variation of partial-acceptance.
- daphna-block-zayin-claims.md: rules for claims summary block — order by
procedural role, neutrality, sub-headings per party, anti-patterns
(numbered lists, evaluation words, premature conclusion).
- daphna-decision-tree.md: operational tool that unifies all 5 voice docs
into a short analytical process. Starts with the decisive question:
"what is the winning evidence?". Decision trees for architecture
selection, opening mode, citation choice, length by weight.
Updates legal-writer.md to read decision-tree first, then the 5 voice docs,
plus block-zayin.md before block ז.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original implementation only returned issues with a plugin_state
linkage (legal-case-number key), which was set just on the initial
setup issue. Sub-agents that created follow-up issues during the case
workflow tagged them in the title ("[ערר 1130-25] כתיבת החלטה" etc.)
but didn't write a plugin_state row, so 23 of 24 historical issues
for case 1130-25 were invisible to the agent activity feed.
Widened the lookup to UNION two paths:
(a) plugin_state.scope_id matches via the legal-case-number key
(b) issues.title LIKE '%[ערר {case_number}]%' OR '%ערר {case_number}%'
Used DISTINCT ON (i.id) + post-sort by created_at to dedupe and keep
chronological order. The widget on https://legal-ai.../cases/1130-25
will now show the full history (was 1 issue → now 16).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The AgentActivityFeed showed a spinner with "הסוכנים התחילו לעבוד,
ממתין לדיווח ראשון..." whenever the case had any issues but no
comments — including cases where all issues had ended in 'done' or
'cancelled' (like 1130-25 after archive). The widget mistook a
finished case for an in-flight workflow.
Now compute hasActiveIssue = some(issues, status !== done && cancelled)
and pick the message accordingly: spinner only while there's still
real work; otherwise a quiet "אין משימות פעילות בתיק. כל המשימות
הסתיימו או בוטלו." with the static MessageSquare icon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a case is archived, the legal-ai UI's AgentStatusWidget kept showing
"agents started working, waiting for first report" because related
Paperclip issues remained in 'todo' / 'in_progress' status. Concrete
example: case 1130-25 had two open issues (CMP-15 ניתוח תכנוני, CMP-21
כתיבת החלטה) that lingered after the case was finalized; 1194-25 had
two more (CMP-37, CMP-44).
Extended pc_archive_project to also UPDATE issues SET status='cancelled',
cancelled_at=now() WHERE project_id matches AND status IN
('backlog','todo','in_progress','blocked','in_review'). Returns the list
of cancelled issues so the toast can announce the count.
Updated cases.ts ArchiveResult.paperclip.issues_cancelled type and the
toast message in case-archive-action to surface "(N משימות פתוחות בוטלו)"
when relevant.
Restore is intentionally unchanged — we don't auto-recreate cancelled
issues; if work needs to resume, a fresh issue should be created.
Stale issues for 1130-25 / 1194-25 cancelled directly in DB as a one-off
cleanup (CMP-15, CMP-21, CMP-37, CMP-44).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a comprehensive archive flow for closed cases — separate /archive
screen in the UI, archive/restore actions on the case detail page, and
automatic two-way sync with Paperclip.
Backend (web/app.py + mcp-server/services/db.py):
- New SCHEMA_V6 migration: cases.archived_at TIMESTAMPTZ + partial index
- list_cases gains include_archived/archived_only flags; default excludes
archived rows so the main /api/cases list hides closed cases
- archive_case / restore_case helpers in db.py
- POST /api/cases/{n}/archive sets archived_at and calls
pc_archive_project (sets Paperclip projects.archived_at via direct DB)
- POST /api/cases/{n}/restore clears archived_at and calls
pc_restore_project (clears Paperclip archived_at)
- archive_project / restore_project in paperclip_client.py — name-based
match consistent with create_project's lookup
Frontend (web-ui):
- cases.ts: scope param ("active"|"archived"|"all") on useCases;
useArchiveCase / useRestoreCase mutations
- /archive page (new): table of archived cases with restore button +
search, sort, empty state matching the editorial aesthetic of /
- case-archive-action.tsx: button on case detail header. Active case →
confirm dialog → archive. Archived case → restore (no confirm).
Toast announces both legal-ai and Paperclip outcomes (synced, not
found in pc, error)
- case-header shows "בארכיון" badge when archived_at is set
- Nav: ארכיון link added to AppShell after בית
Tested end-to-end against the live DB:
- 1130-25 archive → list_cases(include_archived=False) excludes it,
list_cases(archived_only=True) includes it, restore reverses
- pc archive/restore on 1194-25 verified via direct DB lookup
- TypeScript compiles clean
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After reading all 23 1xxx decisions from style_corpus DB (in addition to
the 10 training files and 1130-25/1194-25 deep reads), synthesized two
new operational documents:
docs/daphna-precedent-network.md
- Maps each legal issue to the specific precedent Daphna cites
- 9 threshold issues (standing, השפר, סעיף 152, קנייני, פגמי פרסום,
פסילה, עבירות בנייה) with her preferred quotes for each
- 8 substantive issues (תכנון נקודתי vs כולל, חיקוק תכנית, סטייה ניכרת,
62א, חניה, תמ"א 38, תכניות ישנות, שימוש חורג)
- Lists ~30 external precedents she cites consistently + ~15 personal
precedents (her own canon — 1110/20 בעלז, 1112/22 שקופה, 1181/22 אדלר,
1130-25, etc.)
- Distinguishes precedents she cites vs. those she does NOT cite
docs/daphna-architecture-by-outcome.md
- 7 distinct block-yod architectures keyed to outcome type:
1. Pure rejection (short, 555-2000 words)
2. Rejection after complex analysis (2500-4500)
3. Threshold dismissal + merits "ועל מנת לא לצאת בחסר" (mode F)
4. Three or more distinct issues (sub-headings)
5. Partial acceptance (full funnel architecture)
6. Joined appeals
7. Remand follow-up
- Decision tree for the agent (4 questions → architecture choice)
- Internal proportions table (opening 5-10%, doctrine 15-25%, etc.)
- Costs matrix with 6 scenarios
Updated docs/daphna-voice-fingerprint.md with section 6 (additions from
23-file corpus read): 2 new opening modes (F: threshold+merits, G:
remand follow-up), nuanced sub-heading rule, self-citation of full
analytical blocks, 10 new "we" verbs, 11 traditional phrases with
sources, expanded costs matrix, transparency about petition outcomes,
warning that 1015-24 is dissent (not Daphna's voice).
Updated .claude/agents/legal-writer.md to require reading all 4 voice
docs before block-yod (the "voice quartet"), with explicit decision
tree integration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After analyzing all 24 building_permit decisions in style_corpus DB
(not just the 2 local files), refined two anti-patterns:
1. Sub-headings: actually permitted when block-yod handles 3+ distinct
legal issues (e.g., 1079-24 had "הבקשות לפסילה" / "מעמד המבקשת
וזכות עמידה" / "עותרים ציבוריים"). The earlier rule of "no
sub-headings except academic cases" was too strict — based only on
small local sample.
2. Paragraph numbering: discovered it's an evolutionary pattern, not
a static rule. Pre-2025 decisions had sequential paragraph numbers
(1, 2, 3 throughout); recent decisions (1126-25, 1128-25, 1130-25,
1194-25) abandoned it for narrative flow. The agent should NOT add
paragraph numbers — the new style.
The (1)...(2)...(3)... in-paragraph enumeration ban remains absolute —
0/33 final decisions used it. Distinction now made explicit:
in-paragraph enumeration ≠ paragraph-level numbering (former always
forbidden; latter is evolutionary).
Updated:
- docs/daphna-voice-fingerprint.md — corpus stats, refined anti-patterns
- .claude/agents/legal-writer.md — checklist with new distinctions
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Synthesized two voice documents from corpus reading:
- docs/voice-1130-25.md: deep read of case 1130-25 block-yod (5000 words),
extracting the 9-movement funnel architecture, 8 reasoning templates,
10 'we' verbs with their distinct functions, the 'akhen...ulam' pattern,
pacing/silence principles, and the deliberative meta-narrative.
- docs/daphna-voice-fingerprint.md: cross-corpus synthesis of 10 finals
(1 planning + 9 appraisal levy). Identifies 10 invariants, 5 opening
modes mapped to outcome certainty, mandatory ברמ 3644/13 preamble for
shamai cases, copy-paste templates, and 7 anti-patterns to avoid.
Updated .claude/agents/legal-writer.md:
- Added voice docs as MUST-READ before block-yod (was missing the deep
voice layer; only had surface style_guide patterns)
- Replaced the ' (1)...(2)...(3)...' enumeration template with the 5 opening
modes (the enumeration was a known anti-pattern Daphna always removes)
- Added the 'we' verbs catalog with explicit functions
- Made 'אכן...אולם' pattern mandatory for issues with substantial
counter-arguments (was vaguely 'אמנם...אולם')
- Added mandatory ברמ 3644/13 preamble for 8xxx shamai cases
- Added self-citation triple-mode (refer/defer/distinguish) — Daphna's
emerging practice of building personal jurisprudence
- Added 8-item anti-patterns checklist for post-write review
- Replaced block-yod-alef section with proper 4-paragraph closing
template (process narrative → outcome → costs → date)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "על כן" pattern for block-yod-bet was too greedy and matched mid-discussion
transitional sentences (e.g. "על כן, במקום בו..."), which caused forward-scan
to skip block-yod-alef ("סוף דבר") via the pointer advance.
Tightened to require an operative subject (אנו / הערר / הוועדה / ועדת הערר)
so terminal "על כן, אנו מחליטים" still matches but mid-block transitions don't.
Added structural_fallback for cover blocks (alef/bet/gimel/dalet) — these are
template metadata not present in user-edited DOCX bodies. Inject zero-content
anchors so apply_user_edit can still target them later. The frontend toast
distinguishes real content gaps from fallback anchors.
Also expanded heading patterns based on training corpus inspection:
- block-vav: על המקרקעין חלות / במצב התכנוני / התכניות החלות
- block-zayin: טענות העוררת
- block-chet: עיקר תגובת המשיב
- block-tet: הדיון בוועדת הערר
For case 1130-25, this raises detection from 6/12 to 11/12 blocks — only
block-yod-bet remains missing (Daphna's edit ends at "סוף דבר" + numbered
ruling, no terminal "ההחלטה" or "על כן אנו מחליטים" paragraph).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extraction is expensive (multi-minute LLM calls) and runs across every
appraisal in the case at once, so we don't kick it off silently on every
tag save. The chair tags the appraisals, then runs extraction once when
they're ready.
- New POST /api/cases/{n}/extract-appraiser-facts endpoint returns the
extractor's summary as-is: status=completed with fact counts and
conflicts, or status=sides_missing with the list of still-untagged
appraisal docs.
- DocumentTypeEditor now has a two-phase popover. After a successful
save on an appraisal doc, the body switches to a confirmation view
with a "חלץ עובדות שמאיות עכשיו" button. The result (completed /
sides_missing / no_appraisals / error) renders in the same popover
so the chair sees exactly which appraisals still need tagging
without closing and reopening anything.
- useExtractAppraiserFacts React-Query mutation invalidates the case
detail on success so downstream views (conflict rendering in
block-tet context) pick up the new facts.
Until now changing a document's doc_type required a manual SQL update.
Adds an inline editor on the document badge so the chair can retag
without leaving the case page, and threads an appraiser_side tag
(committee / appellant / deciding) through the appraisal pipeline so
betterment-levy cases — which usually have 2-3 appraisers — render
conflicts with the deciding appraiser's view marked as governing.
Backend
- New appraiser_facts.appraiser_side column (V5.1) populated from
documents.metadata.appraiser_side at extraction time.
- extract_appraiser_facts now returns status='sides_missing' with the
list of untagged appraisals instead of running with empty side
labels — chair must tag every appraisal first via the UI.
- Conflict detection orders entries committee → appellant → deciding so
the deciding appraiser appears last; block-tet's prompt instructs the
writer to phrase the deciding appraiser's view as the governing
factual finding ("ואולם, השמאי המכריע קבע...").
- New PATCH /api/cases/{n}/documents/{doc_id} (Pydantic model with
whitelist validation) and matching document_update MCP tool. Both
merge appraiser_side into metadata JSONB instead of touching the
schema.
UI
- New shared doc-types module exports the canonical 11 doc_type
options plus the 3 appraiser-side options; both upload-sheet and
the document badge now read from it instead of duplicating Hebrew
labels.
- New DocumentTypeEditor renders a Popover off the doc-type Badge
with two Selects. The save button stays disabled while doc_type is
appraisal but no side has been picked, mirroring the backend
enforcement so the user finds out before triggering extraction.
- usePatchDocument React-Query mutation invalidates the case detail
on success so the badge updates without a manual refresh.
The backend (app.py, documents.py, models.py) already maps appraisal→שומה
but the frontend DOC_TYPE_LABELS and upload DOC_TYPES dropdowns were
missing the entry, so appraisal documents rendered as the raw English
string instead of the Hebrew label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Paperclip auto-blocks any in_progress issue without a live execution path
within ~1 minute of the run finishing. When the CEO ends a run with an
@chaim question pending, the main case issue was staying in_progress and
getting auto-blocked, flooding the case timeline with "automatically
retried continuation" system comments (7 occurrences on 2026-04-16).
Add an explicit status protocol to the CEO instructions:
- in_review at the end of any run that leaves a pending @chaim question
- in_progress when resuming from user_commented (also at start of comment routing)
- done only after final export
Applied at all three @chaim waiting points (stages B/C) and at the top
of comment routing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets the chair generate a partial decision DOCX before the discussion-and-
ruling block is decided. Same template, skill and DOCX styling as the final
decision (David, RTL, bookmarks) — only the block selection and order differ:
רקע (ו) → תכניות+היתרים (ט) → טענות (ז) → הליכים (ח). The opening (ה),
ruling (י), summary (יא), and signatures (יב) are omitted.
- New appraiser_facts table + CRUD + conflict detection in db.py (V5 schema).
Conflict = same plan/permit identifier reported differently by 2+ appraisers.
- New appraiser_facts_extractor service: per-appraisal Claude extraction of
plans + permits with raw quotes and page numbers.
- block-tet prompt extended with a permits sub-section sourced from the
extracted facts, plus an explicit instruction to flag inter-appraiser
conflicts in neutral wording without resolving them (deferred to block-yod).
- block-chet prompt extended with a post-hearing materials context sourced
from documents.metadata.is_post_hearing.
- docx_exporter.export_decision now accepts mode='interim' which reorders
the blocks per the chair's mental model and writes
טיוטת-ביניים-v{N}.docx (versioned independently of regular drafts).
- 3 new MCP tools: extract_appraiser_facts, write_interim_draft,
export_interim_draft. write_interim_draft auto-runs extraction if the
appraiser_facts table is empty for the case.
- Remove "מסמכים" tab; render DocumentsPanel at the bottom of the Overview tab
- Move "פתח בעורך ההחלטה" and "עריכת פרטי תיק" into the top row, right of tabs, before "העלאת מסמכים"
- Drop the redundant document count from the quick-summary grid (list is visible below)
- Add flex-wrap to the header row so the extra buttons flow onto a second line on narrow screens
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Missing entries added:
- product-specification.md (business/product spec)
- new-company-setup-guide.md (CMPA setup)
- audit-report.md
- case-migration-tracker.md
- decision-block-mapping.md
All 14 files in docs/ are now indexed in CLAUDE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old architecture.md was out of date (mentioned n8n which isn't used,
wrong embedding dimensions, missing multi-tenancy, no edit loop).
The rewrite documents the full process end-to-end:
1. Document upload + OCR + embedding
2. Analysis (proofreader, researcher)
3. Outcome + direction decision (CEO + human)
4. Deep analysis (pass 2)
5. Drafting (writer writes 12 blocks)
6. QA
7. Initial DOCX export (with bookmarks for future revisions)
8. Edit loop with Track Changes — the new architecture:
a. User downloads + edits in Word + uploads עריכה-v{N}.docx
b. Backend auto-retrofits bookmarks + registers as active_draft
c. User asks CEO for specific change in Paperclip comment
d. CEO stage G: calls writer in revision mode → builds revisions JSON
e. docx_reviser applies <w:ins>/<w:del> preserving user's template
f. User Accept/Reject from Word Review tab
g. Repeat until marked final
Plus MCP tool reference, API endpoints, DB schema, multi-tenancy,
technology stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The analysis DOCX exporter loads skills/docx/decision_template.docx at
runtime, but .dockerignore was excluding the entire skills/ tree and
Dockerfile didn't COPY it — so the deployed container returned
'Template not found at /app/skills/docx/decision_template.docx' on
every /export-docx request.
.dockerignore Re-include the one file we need at runtime.
Dockerfile COPY that single file into /app/skills/docx/.
Documentation and SKILL.md stay outside the image.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the rules and decisions behind building DOCX files from דפנה's
decision template (טיוטת החלטה.dotx). The implementation lives in
mcp-server/src/legal_mcp/services/analysis_docx_exporter.py; this skill
captures the "why" so future improvements don't need to rediscover it.
Contents:
SKILL.md 5 critical rules, style mapping table,
export flow, line classification,
dash policy, placeholder handling,
troubleshooting, future TODOs
references/dotx-to-docx.md why python-docx can't open .dotx +
the conversion recipe
references/rtl-runs.md why <w:rtl/> is required on every run
(otherwise Hebrew falls back to
Times New Roman)
references/style-mapping.md XML dump of every template style,
with the Title-via-theme gotcha
references/line-classification.md the 7 regex categories in
_classify_line() with real examples
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes critical bug in 1033-25: user-uploaded עריכה-*.docx files were
orphaned on disk while exports kept rebuilding from stale DB blocks.
New architecture:
- User-uploaded DOCX becomes the source of truth (cases.active_draft_path)
- System edits via XML surgery with real Word <w:ins>/<w:del> revisions
- User can Accept/Reject each change from within Word
Components:
- docx_reviser.py: XML surgery for Track Changes (15 tests)
- docx_retrofit.py: retroactive bookmark injection with Hebrew marker
detection + heading heuristic (9 tests)
- docx_exporter.py: emits bookmarks around each of the 12 blocks
- 3 new MCP tools: apply_user_edit, list_bookmarks, revise_draft
- 4 new/updated endpoints: upload (auto-registers active draft),
/exports/revise, /exports/bookmarks, /exports/{filename}/retrofit,
/active-draft
- DB migration: cases.active_draft_path column
- UI: correct banner using real v-numbers, "מקור האמת" badge,
detailed upload toast with bookmarks_added/missing_blocks
- agents: legal-exporter (3 export modes), legal-ceo (stage G for
revision handling), legal-writer (revision mode)
Multi-tenancy:
- Works for both CMP (1xxx cases) and CMPA (8xxx/9xxx cases)
- New revise-draft skill added to both companies
- deploy-track-changes.sh syncs skills CMP ↔ CMPA
- retrofit_case.py: one-off retrofit of existing files
Tests: 34 passing (15 reviser + 9 retrofit + 4 exporter bookmarks + 6 e2e)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- legal-analyst: opus 4.6 → opus 4.7
- legal-proofreader: opus 4.6 → opus 4.7
- legal-writer: sonnet 4.6 → opus 4.7 (complex block writing benefits from stronger model)
- block_writer MODEL_MAP: updated opus ID to 4.7
Opus 4.7 brings: high-res images (2576px), better file-based memory,
improved DOCX generation, and task budgets for agentic loops.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New issues created by the CEO via curl were missing plugin_state records,
causing them to be invisible in the legal-ai UI. Added iron rule: after every
POST to create an issue, INSERT into plugin_state with the case number.
Also fixed 8070-25 CMPA issues directly in DB (3 records added).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Agent status widget now checks heartbeat_runs + wakeup_requests to determine
if an agent is running on *this* case. Agents running on other cases show as idle.
Added "running" to STATUS_DOT/STATUS_LABEL maps so it displays in Hebrew.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New /methodology page with 3 tabs for viewing and editing decision
writing methodology. Uses DB override pattern: hardcoded Python
constants serve as defaults, edits saved to appeal_type_rules table,
delete restores default.
Backend: 3 generic endpoints (GET/PUT/DELETE /api/methodology/{category}/{key})
with validation per category type.
Frontend: methodology.ts hooks, GoldenRatiosPanel (number inputs per
outcome/section), DiscussionRulesPanel (accordion with textarea per
rule), ContentChecklistsPanel (markdown editor with preview toggle).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add strip_nevo_preamble() to extractor.py — auto-removes Nevo database
headers (bibliography, legislation, mini-ratio) during training upload
- Add appeal_subtype column to style_patterns table — patterns are now
stored per subtype instead of globally mixed
- Update clear_style_patterns() to support subtype-scoped deletion
- Pass appeal_subtype through analyze_corpus → store → upsert pipeline
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Support ingestion of betterment levy (היטל השבחה) decisions into a
separate training corpus (CMPA). Key changes:
- Add .doc file extraction via LibreOffice conversion in extractor
- Add practice_area/appeal_subtype columns to style_corpus table
- Route training files to cmp/ or cmpa/ subdirs based on appeal subtype
- Fix derive_subtype to handle ARAR-YY-NNNN format (was matching year digit)
- Expose practice_area/appeal_subtype params in MCP upload_training tool
- Add appeal_subtype filter to analyze_style for per-type style analysis
- Update betterment levy methodology in lessons.py: checklist (from generic
to corpus-based), opening/closing strategies, and discussion rules
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
UUID and datetime objects from PostgreSQL RETURNING * were not
serializable. All other tool files already used default=str.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Created 7 agents in CMPA (betterment levy) company, mirroring
the CMP agents with same config and hierarchy. CEO_AGENTS dict
maps company_id to the correct CEO for wakeup routing.
wake_ceo_agent and post_comment now resolve the correct CEO
based on company_id. create_workflow_issue returns company_id.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Role labels: ceo→מנהל, researcher→חוקר, engineer→מהנדס, qa→בודק איכות
Issue status: in_progress→בביצוע, done→הושלם, todo→לביצוע, etc.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>