In an RTL paragraph the bidi algorithm puts the *first* logical token
on the right, so "פתח דאשבורד Paperclip" rendered visually as
"Paperclip" on the LEFT — which reads as the *last* word in Hebrew
and looks like an afterthought rather than the brand name the menu
opens. Reorders to "Paperclip פתח דאשבורד" so Paperclip sits on the
right (read first) and centers the label so it sits above both items
instead of hugging the inline-start edge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous flex layout used `flex-1` on the search wrapper, which
centers the search relative to the *remaining* space — so as the brand
subtitle grows ("עוזר משפטי · ערר 8137-24 · ניסוח") or the agent
trigger label changes, the search drifts off-center.
Switches Row 1 to `grid-cols-[minmax(0,1fr)_minmax(280px,460px)_minmax(0,1fr)]`:
brand on the right, search in the middle (anchored to the viewport
midpoint), agent dropdown on the left. The side cells flex equally so
the center stays put regardless of side content width.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous layout used `justify-between` with the board name and the
prefix·hint hint on the same row. With Hebrew labels + the long hint
"תיקי 8xxx / 9xxx" the row overflowed the 220px content and wrapped the
hint into 2-3 lines, breaking visual alignment.
Stacks each item now: bold board name on top, dim prefix·hint underneath.
Adds whitespace-nowrap to both lines and bumps min-width to 240px so the
content drives the dropdown width instead of fighting it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits the AppShell header into:
Row 1 — brand: logo + dynamic context subtitle (route-aware) +
global search + agent boards dropdown
Row 2 — nav: work group (בית · ארכיון) | knowledge group (ספריית
פסיקה · אימון · מתודולוגיה) + admin dropdown (⚙) on the left
Three changes from the previous flat 8-item nav:
1. Grouping reflects intent. Daily-driver pages are in "work", corpus
pages in "knowledge"; system pages (skills · diagnostics · settings)
move into a single ⚙ dropdown so they stop competing for attention.
2. Subtitle is now dynamic. `headerSubtitle(pathname)` resolves the
current section so the user always sees where they are without
scanning the nav row. Case routes show the case number explicitly
("ערר 1234-24" / "ערר 1234-24 · ניסוח").
3. The gold-underline active state is preserved and the admin trigger
inherits it whenever any admin route is active.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures accumulated backend drift since last regeneration. Triggered
by the new /api/search/cases endpoint added for header global search,
but the diff also picks up many other endpoints that had been added
without re-running api:types.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an always-visible debounced search input in the AppShell header
that fans out to three independent sources in parallel and renders
per-source result groups with their own loading/empty/error states:
- /api/search/cases (NEW): SQL ILIKE on case_number, address, parties,
title, subject. Returns small projections, no embeddings needed.
- /api/precedent-library/search (existing): semantic over case-law
halachot + passages.
- /api/search (existing): semantic over case documents + past decisions.
Cmd/Ctrl+K focuses the input; Esc and click-outside close the panel.
This is Phase A of the header redesign — the bar layout itself is
unchanged; row grouping + dynamic context follow in Phase B.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the hardcoded CMPA link with a dropdown listing both
Paperclip boards (CMP = רישוי ובניה, CMPA = היטלי השבחה). Fixes the
mislabeling where the original link pointed to the wrong board, and
gives the user a single entry point that scales if a third board is
added later.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "ניהול סוכנים" link on the opposite side of the "עוזר משפטי"
title in the app shell header. Opens the Paperclip CMPA dashboard
(pc.nautilus.marcusgroup.org/CMPA/dashboard) in a new tab for quick
cross-tool navigation between the legal-ai workspace and agent ops.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a precedent is uploaded to the library, the FastAPI container now
fires a Paperclip wakeup so Claude (running locally as the CEO agent)
picks up the new row and runs `precedent_process_pending` for both
metadata and halacha extraction. The user no longer has to remember to
trigger it manually.
Mechanics:
- New `wake_for_precedent_extraction()` in paperclip_client.py creates
(or reuses) a per-company "ספריית פסיקה — תור חילוץ" project, opens
a fresh issue assigned to the company CEO with the case_law_id +
citation in the description, and pings the Board API wakeup endpoint
with `triggerDetail=precedent_library_upload`.
- ingest_precedent's _run() in app.py captures the returned case_law_id
and best-effort calls the wake function (failures are logged, not
surfaced — the upload itself stays clean).
- legal-ceo.md adds the precedent_process_pending tool family and a
new "חילוץ פסיקה אוטומטי" section that tells the CEO to short-circuit
past the heartbeat scan when woken with this trigger.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase A — voyage-3 migration (executed):
- VOYAGE_MODEL=voyage-3 set in Coolify (legal-ai app) and ~/.env
- scripts/reembed_voyage.py: re-embeds document_chunks (6157),
case_law_embeddings (9), precedent_chunks (385), and halachot (400)
using the new model. paragraph_embeddings was empty. 6951 rows
re-embedded in 93s, ~75 rows/sec.
- Same 1024 dim → no schema change needed.
Why voyage-3 over voyage-law-2: benchmark on 3 Hebrew legal queries
with real passages from the corpus gave voyage-3 perfect ordering on
3/3 tests AND the largest separation (+0.483 vs voyage-law-2's
+0.238). voyage-4 family had bigger separation but missed top-1 on
the hardest test.
Phase B (voyage-context-3) and Phase C (voyage-multimodal-3.5 for
scanned + appraiser docs) are designed in docs/voyage-upgrades-plan.md
but deferred — to be picked up in a fresh conversation. The plan
includes:
- Phase B: contextualized embeddings refactor (~49% recall lift on
legal docs per Anthropic's research). Same dim, but ingestion
pipeline must pass full doc context per chunk.
- Phase C: page-level image embeddings via voyage-multimodal-3.5,
stored in a parallel *_image_embeddings table. Hybrid text+image
search. Targets appraiser report tables and scanned PDFs where
current OCR loses layout.
After this commit: MCP server needs a /mcp reconnect to pick up the
new VOYAGE_MODEL env, and the legal-ai container will pick it up on
its next redeploy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
set_case_law_extraction_status and set_case_law_halacha_status now NULL
the corresponding *_requested_at timestamp when status transitions to
"completed" or "failed". Without this, completed rows kept lingering in
the local-MCP work queue (which scans by `WHERE *_requested_at IS NOT NULL`)
and the UI's isPrecedentActive check, leaving them undeletable until a
manual SQL cleanup.
The pre-existing process_pending_extractions path already called
clear_extraction_request, but other paths (re-extraction, status set
during upload) didn't — so the cleanup belongs at the status setter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The local MCP worker is supposed to NULL `*_extraction_requested_at` after
a successful run, but in practice these timestamps linger. The previous
isPrecedentActive logic treated any non-null timestamp as "still active",
which left completed rows permanently undeletable.
Now only "processing" status (or genuinely queued: pending + timestamp)
counts as active. Once a row is "completed"/"failed", stale timestamps
no longer block the delete button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The chair pointed out three UX gaps after uploading a new precedent:
1. The status said "מחלץ הלכות" but nothing was actually running — the
field only meant "halacha_extraction_status != completed", which
includes the post-upload "pending" state where the local MCP worker
hasn't been told to drain anything yet. Misleading.
2. The page didn't refresh on its own. The chair had to F5 to see new
counts after extraction completed.
3. Clicking the trash icon mid-extraction would cascade-delete the row
while the extractor was still using it (FK errors, partial writes).
Fixes:
- ingest_precedent now auto-queues both metadata and halacha extraction
on upload by stamping the request timestamps. The chair (or me) drains
the queue with one `precedent_process_pending` call from chat —
no need to click any button before that.
- StatusPill is now five-state with proper labels:
"נכשל" (extraction_status=failed) — red
"מעבד טקסט" — shimmer (extraction_status=processing)
"בתור" — neutral (chunks queued, not yet running)
"מחלץ הלכות" — shimmer (halacha_extraction_status=processing)
"ממתין לחילוץ" — neutral (queued for local MCP worker)
"לא חולץ" — neutral (pending without queue stamp — shouldn't happen)
"X/Y מאושרות" — gold (done, with halachot count)
The shimmer is a CSS-only sliding-stripe animation defined in globals.
- usePrecedents has a conditional refetchInterval — polls every 5s while
any row is mid-extraction or queued, then stops once everything settles
to completed/failed. New helper isPrecedentActive() centralises the
"is this row mid-something" check so the UI and the destructive-action
guard agree.
- Trash button is disabled (opacity 30%, tooltip explains) while the row
is active. Pencil/edit stays enabled — editing metadata fields during
extraction is safe (last write wins, low-stakes race).
Schema: list_external_case_law now exposes the two *_requested_at
timestamps so the UI can distinguish "queued" from "never asked".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The chair wanted a one-click "extract metadata" button on the edit sheet.
The constraint stays the same — claude_session needs the local CLI which
the container doesn't have, so the button can't run the extractor itself.
Compromise: button stamps a queue marker; the local MCP server drains the
queue on demand.
DB (V8): two nullable timestamps on case_law,
metadata_extraction_requested_at and halacha_extraction_requested_at,
with partial indexes for cheap "find pending" scans.
API:
POST /api/precedent-library/{id}/request-metadata → stamp the row
POST /api/precedent-library/{id}/request-halachot → same for halacha
GET /api/precedent-library/queue/pending?kind=... → read-only view
UI: Sparkles button in the edit sheet header. Click → toast tells the
chair what to run from Claude Code. The button never triggers the
extractor directly from the container.
MCP tool: precedent_process_pending(kind, limit) — runs from Claude Code
with the local CLI, picks up everything stamped, calls the extractor for
each, clears the timestamp on success. Failures keep the timestamp so the
next invocation retries them.
Architectural rule (claude_session local-only) is preserved end-to-end
and called out in the new endpoint comment + tool docstring.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups after running the metadata extractor on 403-17:
1. Library table: shadcn TableCell defaults to whitespace-nowrap and
the table wrapper has overflow-x-auto, so the long citation forced
a horizontal scrollbar inside the row. Override on the citation
cell only — whitespace-normal + break-words + min/max-w to keep the
column readable. Same for the case-name cell. Row aligns to top so
wrapping doesn't push neighbours up.
2. Extractor now also fills source_type (court_ruling /
appeals_committee). The previous round added decision_date_iso,
precedent_level, and court but left source_type empty. Same
closed-enum + merge-only-if-empty policy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first end-to-end run on 403-17 surfaced three fields the auto-fill
left blank because the chair didn't set them in the upload form: date,
precedent_level, and court. All three are right there in the ruling's
header text — there's no reason to require manual entry.
Prompt now asks for:
- decision_date_iso (YYYY-MM-DD parsed from "ניתנה היום, … 5 בספטמבר 2022"
style signatures)
- precedent_level (closed enum: עליון/מנהלי/ועדת_ערר_ארצית/ועדת_ערר_מחוזית)
- court (the full court name from the title block)
Validation is unchanged: precedent_level only accepts the four enum
values; decision_date_iso is parsed into a Python date object before
being handed to update_case_law (asyncpg doesn't coerce strings to
DATE columns); court is stored verbatim.
Merge policy is unchanged — only fills empty fields. Anything the
chair typed in the upload form survives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After running the dual-mode halacha extractor on a real appeals committee
decision (403-17), the pending-review tab surfaced 351 halachot in a
single flat list — the chair correctly pointed out that this is unusable
without grouping. Three fixes:
1. Group pending halachot by precedent (case_law_id). Each group shows
the citation, court, date, level and item count; default state is
collapsed so the chair picks one ruling at a time. Within a group,
items still sort by confidence ascending so the doubtful ones surface
first. J/K/A/R/E now scope to currently-expanded groups; toggling
open auto-focuses the first item.
2. Translate the badges that were leaking English: rule_type values
(`persuasive`, `interpretive`, `binding`, `application`, `procedural`,
`obiter`) now render as Hebrew labels, and `confidence X.XX` becomes
`ביטחון X.XX`. The card header no longer repeats the citation since
it's already in the group header.
3. Strip Unicode bidi marks (U+200E/F/202A-E/2066-9) from displayed
citations. Nevo PDFs and the upload form embed these in the
case_number; they render as zero-width but visually push the text
away from the right edge of the table cell. Also: hide the empty
court line under the case name in the list (was rendering as a
stray em-dash), and use a muted em-dash for empty date/level rather
than blank/dash inconsistency across columns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architectural correction: every claude_session caller in this project
runs through the local MCP server (~/.claude.json points at
/home/chaim/legal-ai/mcp-server/.venv/bin/python). The Coolify container
has no `claude` CLI and no claude.ai session, so any LLM call originating
from web/ FastAPI fails with "Claude CLI not found" — which is exactly
what we hit on 403-17.
The earlier Anthropic SDK fallback would have made it work, but at
direct API cost. The chair's preference is to stay on the claude.ai
session for everything. So:
- claude_session.py: removed the SDK fallback, restored CLI-only.
The error message now points the next person at the architectural
rule in the module docstring instead of papering over it.
- precedent_library.py:ingest_precedent (called from FastAPI on upload)
now does only the non-LLM half: extract → chunk → embed → store.
Sets halacha_extraction_status='pending' for the chair to act on.
- reextract_halachot / reextract_metadata kept, but lazy-import their
extractors so the FastAPI path can't accidentally pull them in. They
are reachable only via the MCP tools precedent_extract_halachot /
precedent_extract_metadata, which run locally with CLI.
- Removed POST /api/precedent-library/{id}/extract-halachot and
/extract-metadata — they were dead ends from the container.
- Dropped the `anthropic` Python dep that the SDK fallback required.
- UI: removed the "refresh halachot" and "sparkles metadata" buttons
that called those endpoints. Edit sheet now points the chair at the
MCP tool names instead.
Halacha and metadata extraction for an uploaded precedent now happen
when the chair (via Claude Code) runs:
mcp__legal-ai__precedent_extract_metadata <case_law_id>
mcp__legal-ai__precedent_extract_halachot <case_law_id>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes to the precedent library after the first end-to-end test on
403-17 surfaced runtime issues:
1. Anthropic SDK fallback in claude_session. The legal-ai Docker container
does not ship the `claude` CLI, so every halacha and metadata extraction
was failing with "Claude CLI not found." Module now tries the CLI first
(zero-cost local path) and falls back to the Anthropic SDK with
ANTHROPIC_API_KEY when the binary is absent. Default model is
claude-sonnet-4-6, overridable via CLAUDE_SDK_MODEL env. The system
message gets cache_control: ephemeral so multi-chunk runs reuse the
cached instruction prefix at ~10% read cost. Adds `anthropic` to
pyproject deps.
2. precedent_metadata_extractor crashed with KeyError because the JSON
example inside the prompt template contained literal { } characters
that str.format() interpreted as placeholders. Switched to f-string
concatenation; the prompt template no longer needs format() at all.
3. Library list query stays stale after upload because the upload
mutation's onSuccess fires when the POST returns task_id, not when
SSE reports completion. Added a second invalidate inside the SSE
watcher in PrecedentUploadSheet so the new row appears with up-to-date
chunk and halachot counts the moment processing finishes.
Halacha and metadata extractors now route the long static prompt through
the new `system=` parameter so the SDK path actually caches it; the CLI
path concatenates and behaves as before.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three improvements to the precedent library based on usage feedback:
1. Auto-fill metadata at upload time. New service
precedent_metadata_extractor reads the ruling's full_text and
suggests case_name (short), summary, headnote, key_quote,
subject_tags, appeal_subtype. The merge policy fills only empty
fields, preserving everything the chair typed in the upload form.
Wired into the ingest pipeline; also exposed as a re-run endpoint
POST /api/precedent-library/{id}/extract-metadata for existing
records.
2. Edit sheet in the UI. Pencil icon on each library row opens a
pre-populated form covering every field. A Sparkles button on the
sheet runs the metadata extractor on demand and refreshes the
form. The case_number is read-only because halachot are FK'd to
it; renaming requires delete + re-upload.
3. Halacha extractor branches on is_binding. Sources marked binding
(Supreme/Administrative) keep the strict halacha prompt. Non-binding
sources (other appeals committees, district courts on planning
matters) get a different prompt that extracts applications,
interpretive principles, and persuasive conclusions — labeled with
new rule_types 'application' and 'persuasive'. The fallback also
widens chunk selection: if the chunker labeled nothing as
legal_analysis/ruling/conclusion, we now run on all chunks rather
than returning zero halachot for a usable ruling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reduce vertical padding, number font size, and inter-element gaps so
the four counters take less vertical real estate. Width unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a third corpus of legal authority distinct from style_corpus
(Daphna's prior decisions for voice) and case_precedents (chair-attached
quotes per case). The new corpus holds chair-uploaded court rulings and
other appeals committee decisions, with binding rules (הלכות) extracted
automatically and queued for chair approval.
Pipeline (web/app.py + services/precedent_library.py):
file → extract → chunk → Voyage embed → halacha_extractor → store +
publish progress over the existing Redis SSE channel.
Schema V7 (services/db.py): extends case_law with source_kind +
extraction status fields under a CHECK constraint pinning practice_area
to the three appeals committee domains (rishuy_uvniya, betterment_levy,
compensation_197). New precedent_chunks (vector(1024)) and halachot
tables (vector(1024) over rule_statement, IVFFlat indexes, gin on
practice_areas/subject_tags). Halachot start as pending_review; only
approved/published rows are visible to search_precedent_library.
Agents: legal-writer, legal-researcher, legal-analyst, legal-ceo,
legal-qa get search_precedent_library. legal-writer prompt explains
the three-corpus distinction and CREAC use; legal-qa now verifies that
every cited halacha resolves to an approved row in the corpus.
UI: /precedents page with four tabs — library / semantic search /
pending review (J/K nav, A/R/E shortcuts, badge count) / stats.
Reuses the existing upload-sheet progress + SSE pattern.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The list's scroll container had only overflow-y:auto, which CSS computes
overflow-x to auto too. Combined with the row's -mx-2 hover-background
extension, this surfaced an unwanted horizontal scrollbar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>