From 45341a0bc889a1ca80f31626d383ee7796012c21 Mon Sep 17 00:00:00 2001 From: Chaim Date: Sun, 10 May 2026 05:58:52 +0000 Subject: [PATCH] feat(curator): switch Hermes Curator to DeepSeek V4-Pro via deepseek_local adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A/B test (2026-05-05) showed DeepSeek V4-Pro is 2-3x faster and ~20x cheaper than Sonnet for style/lexicon pattern analysis, with comparable quality. Adds adapters/deepseek-paperclip-adapter/ package, documents adapter requirements (env injection, run-id headers), updates CLAUDE.md with adapter integration notes, and records lessons from ערר 1200-25 (block order for 1xxx, "להלן מתוך" pattern, expanded factual background, bridge planning analysis, flat heading structure). Co-Authored-By: Claude Sonnet 4.6 --- .claude/agents/hermes-curator.md | 56 ++- CLAUDE.md | 11 + .../deepseek-paperclip-adapter/dist/index.js | 99 +++++ .../dist/server/execute.js | 352 ++++++++++++++++++ .../dist/server/session-codec.js | 29 ++ .../dist/server/skills.js | 171 +++++++++ .../dist/server/test.js | 164 ++++++++ .../dist/shared/constants.js | 36 ++ .../package-lock.json | 25 ++ .../deepseek-paperclip-adapter/package.json | 21 ++ docs/daphna-voice-fingerprint.md | 48 +++ docs/legal-decision-lessons.md | 61 +++ scripts/.archive/run_curator_deepseek_test.sh | 87 +++++ .../.archive/run_curator_deepseek_test_v2.sh | 116 ++++++ scripts/.archive/run_curator_sonnet_rerun.sh | 106 ++++++ scripts/SCRIPTS.md | 5 +- 16 files changed, 1380 insertions(+), 7 deletions(-) create mode 100644 adapters/deepseek-paperclip-adapter/dist/index.js create mode 100644 adapters/deepseek-paperclip-adapter/dist/server/execute.js create mode 100644 adapters/deepseek-paperclip-adapter/dist/server/session-codec.js create mode 100644 adapters/deepseek-paperclip-adapter/dist/server/skills.js create mode 100644 adapters/deepseek-paperclip-adapter/dist/server/test.js create mode 100644 adapters/deepseek-paperclip-adapter/dist/shared/constants.js create mode 100644 adapters/deepseek-paperclip-adapter/package-lock.json create mode 100644 adapters/deepseek-paperclip-adapter/package.json create mode 100755 scripts/.archive/run_curator_deepseek_test.sh create mode 100755 scripts/.archive/run_curator_deepseek_test_v2.sh create mode 100755 scripts/.archive/run_curator_sonnet_rerun.sh diff --git a/.claude/agents/hermes-curator.md b/.claude/agents/hermes-curator.md index da6425d..6340f61 100644 --- a/.claude/agents/hermes-curator.md +++ b/.claude/agents/hermes-curator.md @@ -1,13 +1,18 @@ --- name: hermes-curator description: Knowledge Curator (Hermes) — מנתח החלטות סופיות אחרי export, מציע עדכונים ל-skills/lessons. read-only על תוכן, write רק על comments. -adapter: hermes_local -model: anthropic/claude-sonnet-4-5 +adapter: deepseek_local +model: deepseek-v4-pro profiles: CMP: curator-cmp # רישוי ובניה (תיקים 1xxx) CMPA: curator-cmpa # היטל השבחה + פיצויים (תיקים 8xxx, 9xxx) --- +> **Why DeepSeek**: A/B test 2026-05-05 הראה ש-DeepSeek V4-Pro חזק יותר מ-Sonnet +> על דפוסי סגנון/לקסיקון, פי 2-3 מהיר, פי ~20 זול. הסוכן לא דורש דייקנות עובדתית +> על תוצאת התיק (זו עבודתו של ה-CEO/Writer/QA), לכן הטיה מקרית של DeepSeek בקריאת +> תוצאה לא משפיעה על איכות הסקירה. + # מנהל ידע — Hermes Knowledge Curator ## רקע @@ -54,10 +59,11 @@ profiles: 1. קורא את ה-issue body שב-`{{taskBody}}` — שם התיק + ID של ההחלטה הסופית 2. משתמש ב-MCP tools של legal-ai: - - `mcp__legal-ai__case_get` — קבלת פרטי תיק - - `mcp__legal-ai__document_list` — רשימת מסמכים, איתור ההחלטה הסופית - - `mcp__legal-ai__search_decisions` — השוואה לחלטות קודמות + - `mcp__legal-ai__case_get` — קבלת פרטי תיק (כולל `expected_outcome` — **הסמכות העובדתית** לתוצאה) + - `mcp__legal-ai__case_get_final_text` — הטקסט המלא של ההחלטה הסופית + - `mcp__legal-ai__document_list` — רק אם נדרש רשימת מסמכים נוספים של התיק - `mcp__legal-ai__get_style_guide` — דפוסי הסגנון של דפנה + - **לא** להשתמש ב-`search_decisions` — השוואה ל-`SKILL.md` ו-`corpus-analysis.md` מספיקה ולא יקרה 3. קורא קבצים מקומיים (read-only): - `/home/chaim/legal-ai/skills/decision/SKILL.md` - `/home/chaim/legal-ai/docs/legal-decision-lessons.md` @@ -74,13 +80,30 @@ profiles: ## פורמט ה-comment -עברית, ניטרלי. 3-5 ממצאים מובחנים. כל ממצא: +עברית, ניטרלי. 3-5 ממצאים מובחנים. **כל ממצא חייב להיות מתויג** באחד מ-4 הסוגים: + +``` +[סגנון] — מילים, ביטויי מעבר, פתיחות, סיומים +[מבנה] — סדר בלוקים, יחסי אורך, מספור +[לקסיקון משפטי] — מינוח טכני (מגישי תכנית, ריפוי פגם, וכו') +[טבלאי] — דפוסים שמופיעים פעמיים+ ב-corpus +``` + +לכל ממצא: - **מה ראיתי** — תיאור קצר של הדפוס/הפער - **מה זה אומר** — למה זה חשוב - **הצעה** — איך אפשר להוסיף ל-style guide / lessons (טקסט מוצע מילולי) אם אין ממצאים חדשים → לציין במפורש בלי להמציא. +## מה **לא** להגיד ב-comment + +- **אל תכלול שורת מטא** בראש ה-comment עם "תוצאה: X" או "אורך: ~Y תווים". + אתה לא בודק את התיק — אתה בודק את הסגנון. תוצאה מוטעית בראש ה-comment פוגעת באמינות. +- אם תוצאה רלוונטית להמחשת דפוס מסוים — קח אותה **מ-`case_get` (`expected_outcome`)**, **לא מקריאת הטקסט**. + אם השדה ריק או חסר ב-DB — סמן `[תוצאה: לא מאומתת]` או דלג עליה. +- **אל תפרש משפטית** את ההחלטה. דפנה כבר הכריעה. תפקידך זיהוי דפוסים בלבד. + ## מה אני לא עושה - **לא מעדכן** קבצים בעצמי (skills/, lessons.py, DB) — רק מציע @@ -93,6 +116,27 @@ profiles: אם MCP server לא נגיש או החלטה לא נמצאת, כתוב comment קצר עם הסיבה ו-status=failed. אל תזייף ממצאים. +## דרישות מ-`deepseek_local` adapter (חובה) + +ה-adapter שמריץ אותי **חייב** להזריק 3 דברים בכל wake — אחרת interactions ייחסמו ב-`401 "Agent run id required"`: + +1. **env `PAPERCLIP_API_KEY`** — agent's own pcp_ key +2. **env `PAPERCLIP_RUN_ID`** — ה-`heartbeat_runs.id` של ה-wake הנוכחי +3. **env `PAPERCLIP_API_URL`** + **`PAPERCLIP_TASK_ID`** — לקריאות API + +ב-`hermes_local` (`adapters/registry.ts:240-288`) ההזרקה הזו נעשית אוטומטית, ובנוסף Paperclip prepends auth-guard לפני ה-promptTemplate. ב-`deepseek_local` החדש — לוודא שמיושם. + +ה-promptTemplate **כבר** כולל את ה-header `X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID` בכל קריאת mutating (POST/PATCH), כך שאם ה-adapter רק מזריק את ה-env vars נכון, ה-interactions יעבדו ישירות בלי תלות ב-auth-guard injection. + +### Verification: + +```bash +# על תיק חי, אחרי שדפנה לוחצת mark-final, ה-curator יקבל: +echo "PAPERCLIP_RUN_ID=$PAPERCLIP_RUN_ID" # חייב להיות UUID חוקי +echo "PAPERCLIP_API_KEY=${PAPERCLIP_API_KEY:0:8}..." # חייב להתחיל ב-pcp_ +echo "PAPERCLIP_API_URL=$PAPERCLIP_API_URL" # חייב להיות http://localhost:3100/api +``` + ## קונטקסט קבוע (לא לשכוח) - היו"ר: עו"ד דפנה תמיר diff --git a/CLAUDE.md b/CLAUDE.md index d323cbd..73e89d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,6 +118,8 @@ ├── web-ui/ ← Next.js frontend (TypeScript/React): ממשק המשתמש │ └── next.config.ts ← proxy: /api/* → FastAPI :8000 ├── mcp-server/ ← MCP server + services + tools +├── adapters/ ← Paperclip external adapters (ראה למטה) +│ └── deepseek-paperclip-adapter/ ← `deepseek_local` (Hermes-pinned ל-DeepSeek profile) └── scripts/ ← סקריפטים וכלי עזר (ראה scripts/SCRIPTS.md) └── .archive/ ← סקריפטים שהושלמו (לא להריץ) ``` @@ -180,6 +182,15 @@ - הסקריפט מסנן local skills שלא קיימים ב-CMPA (מציג אזהרה), משתמש ב-API (לא DB ישיר), יוצר revisions, idempotent. - שאלות ה-skill הרשמי של Paperclip — `paperclip` skill תחת `paperclipai/paperclip`. +### External adapters — `deepseek_local` +- מיקום ה-package: [adapters/deepseek-paperclip-adapter/](adapters/deepseek-paperclip-adapter/) (לא ב-`node_modules`). +- רישום ב-Paperclip: רשומה ב-`~/.paperclip/adapter-plugins.json` (נטען אוטומטית ב-startup דרך `buildExternalAdapters`). אין צורך בעריכת `node_modules`. +- **מה ה-adapter עושה**: spawnל-`hermes chat` עם `HERMES_HOME=/home/chaim/.hermes/profiles/deepseek` כך שה-CLI טוען את `config.yaml` (`base_url=https://api.deepseek.com/v1`, `provider=custom`, `key_env=DEEPSEEK_API_KEY`) ואת `.env` (שמכיל את ה-key). +- **מודלים זמינים** (lookup ב-DeepSeek `/v1/models`): `deepseek-v4-pro` (default), `deepseek-v4-flash`. יופיעו כדרופ-דאון ב-UI. +- **התקנה מחדש / עדכון**: `curl -X POST -H "Authorization: Bearer pcapi_legal_install_key_2026" -H "Content-Type: application/json" -d '{"packageName":"/home/chaim/legal-ai/adapters/deepseek-paperclip-adapter","isLocalPath":true}' http://localhost:3100/api/adapters/install`. לעדכון hot — `POST /api/adapters/deepseek_local/reload`. +- **⚠ Cross-company sync**: `sync_agents_across_companies.py` **מדלג** על סוכנים עם `adapter_type` שונה בין CMP ל-CMPA. כשעוברים סוכן ל-`deepseek_local` חובה להחיל ידנית בשתי החברות לפני sync. +- **תוספת adapters עתידיים** (OpenAI ישיר, Anthropic ישיר, וכו'): אותו דפוס. ה-package הראשי חייב לייצא `createServerAdapter()` שמחזיר `{ type, label, models, agentConfigurationDoc, execute, testEnvironment, sessionCodec, listSkills, syncSkills, ... }`. ראה את [adapters/deepseek-paperclip-adapter/dist/index.js](adapters/deepseek-paperclip-adapter/dist/index.js) כתבנית. + --- ## עקרונות כתיבה קריטיים diff --git a/adapters/deepseek-paperclip-adapter/dist/index.js b/adapters/deepseek-paperclip-adapter/dist/index.js new file mode 100644 index 0000000..a730d21 --- /dev/null +++ b/adapters/deepseek-paperclip-adapter/dist/index.js @@ -0,0 +1,99 @@ +/** + * DeepSeek (via Hermes) — external Paperclip adapter. + * + * Loaded by Paperclip's plugin-loader. Contract: + * The package's main module must export createServerAdapter() returning + * a single ServerAdapterModule object with all fields wired in. + * + * Runtime: spawns the local `hermes` CLI with HERMES_HOME pinned to a + * DeepSeek profile that defines model.base_url=https://api.deepseek.com/v1 + * and model.key_env=DEEPSEEK_API_KEY. + */ + +import { + ADAPTER_TYPE, + ADAPTER_LABEL, + DEEPSEEK_MODELS, + DEFAULT_PROFILE_HOME, +} from "./shared/constants.js"; +import { execute } from "./server/execute.js"; +import { testEnvironment } from "./server/test.js"; +import { sessionCodec } from "./server/session-codec.js"; +import { listSkills, syncSkills } from "./server/skills.js"; + +const AGENT_CONFIGURATION_DOC = `# DeepSeek (via Hermes) — Agent Configuration + +DeepSeek-pinned variant of the Hermes adapter. Runs the local \`hermes\` CLI +with \`HERMES_HOME\` pointed at a DeepSeek profile (\`config.yaml\` declares +\`base_url=https://api.deepseek.com/v1\` and \`key_env=DEEPSEEK_API_KEY\`). + +## Prerequisites + +- Hermes Agent installed (\`pip install hermes-agent\`) — \`hermes --version\` works. +- DeepSeek profile dir exists (default: \`/home/chaim/.hermes/profiles/deepseek\`) + with \`config.yaml\` + \`.env\` (containing \`DEEPSEEK_API_KEY\`). + +## Core Configuration + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| model | string | \`deepseek-v4-pro\` | DeepSeek model id (\`deepseek-v4-pro\` or \`deepseek-v4-flash\`). | +| provider | string | \`custom\` | Hermes provider name. The DeepSeek profile defines \`provider: custom\` so \`custom\` is the right value. | +| hermesProfileHome | string | \`/home/chaim/.hermes/profiles/deepseek\` | Absolute path to a Hermes profile dir. Set per-agent if you maintain multiple DeepSeek profiles. | +| timeoutSec | number | 1800 | Execution timeout in seconds. | +| graceSec | number | 30 | SIGTERM grace period in seconds. | + +## Tools / Workspace + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| toolsets | string | (profile default) | Comma-separated toolsets to enable. | +| persistSession | boolean | true | Resume sessions across heartbeats via \`--resume\`. | +| worktreeMode | boolean | false | Use git worktree for isolated changes. | +| checkpoints | boolean | false | Enable filesystem checkpoints. | + +## Advanced + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| hermesCommand | string | \`hermes\` | Path to the hermes binary. | +| verbose | boolean | false | Enable verbose Hermes logs. | +| extraArgs | string[] | [] | Extra CLI args appended after standard flags. | +| env | object | {} | Extra environment variables passed to Hermes. \`HERMES_HOME\` here overrides \`hermesProfileHome\`. | +| promptTemplate | string | (default) | Override the default Paperclip wakeup prompt. | +| paperclipApiUrl | string | \`http://127.0.0.1:3100/api\` | Paperclip API URL injected into the prompt template. | + +## Available template variables + +\`{{agentId}}\`, \`{{agentName}}\`, \`{{companyId}}\`, \`{{companyName}}\`, +\`{{runId}}\`, \`{{taskId}}\`, \`{{taskTitle}}\`, \`{{taskBody}}\`, +\`{{commentId}}\`, \`{{wakeReason}}\`, \`{{projectName}}\`, \`{{paperclipApiUrl}}\`. +`; + +export function createServerAdapter() { + return { + type: ADAPTER_TYPE, + label: ADAPTER_LABEL, + models: DEEPSEEK_MODELS, + agentConfigurationDoc: AGENT_CONFIGURATION_DOC, + + execute, + testEnvironment, + sessionCodec, + listSkills, + syncSkills, + + // Capability flags + supportsLocalAgentJwt: true, + supportsInstructionsBundle: false, + requiresMaterializedRuntimeSkills: false, + }; +} + +// Also export the loose constants for any caller that wants to inspect +// the package without invoking createServerAdapter (e.g., test harnesses). +export const type = ADAPTER_TYPE; +export const label = ADAPTER_LABEL; +export const models = DEEPSEEK_MODELS; +export const agentConfigurationDoc = AGENT_CONFIGURATION_DOC; +export const defaultProfileHome = DEFAULT_PROFILE_HOME; diff --git a/adapters/deepseek-paperclip-adapter/dist/server/execute.js b/adapters/deepseek-paperclip-adapter/dist/server/execute.js new file mode 100644 index 0000000..c66cf01 --- /dev/null +++ b/adapters/deepseek-paperclip-adapter/dist/server/execute.js @@ -0,0 +1,352 @@ +/** + * Server-side execution for the DeepSeek-via-Hermes adapter. + * + * Spawns `hermes chat -q "..." -Q -m --provider custom` with + * HERMES_HOME pinned to a DeepSeek-configured profile so the same machine + * can run other Hermes-based agents on different providers in parallel. + * + * The Hermes CLI loads model.base_url, model.key_env (DEEPSEEK_API_KEY), + * and toolsets from /config.yaml + /.env. + */ + +import { + runChildProcess, + buildPaperclipEnv, + renderTemplate, + ensureAbsoluteDirectory, +} from "@paperclipai/adapter-utils/server-utils"; +import { + HERMES_CLI, + DEFAULT_PROFILE_HOME, + DEFAULT_MODEL, + DEFAULT_PROVIDER, + DEFAULT_TIMEOUT_SEC, + DEFAULT_GRACE_SEC, + SESSION_ID_REGEX, + SESSION_ID_REGEX_LEGACY, + TOKEN_USAGE_REGEX, + COST_REGEX, +} from "../shared/constants.js"; + +function cfgString(v) { + return typeof v === "string" && v.length > 0 ? v : undefined; +} +function cfgNumber(v) { + return typeof v === "number" ? v : undefined; +} +function cfgBoolean(v) { + return typeof v === "boolean" ? v : undefined; +} +function cfgStringArray(v) { + return Array.isArray(v) && v.every((i) => typeof i === "string") ? v : undefined; +} + +const DEFAULT_PROMPT_TEMPLATE = `You are "{{agentName}}", an AI agent employee in a Paperclip-managed company powered by DeepSeek. + +IMPORTANT: Use the \`terminal\` tool with \`curl\` for ALL Paperclip API calls (web_extract and browser cannot access localhost). + +Your Paperclip identity: + Agent ID: {{agentId}} + Company ID: {{companyId}} + API Base: {{paperclipApiUrl}} + +{{#taskId}} +## Assigned Task + +Issue ID: {{taskId}} +Title: {{taskTitle}} + +{{taskBody}} + +## Workflow + +1. Work on the task using your tools. +2. When done, mark the issue completed: + \`curl -s -X PATCH "{{paperclipApiUrl}}/issues/{{taskId}}" -H "Content-Type: application/json" -d '{"status":"done"}'\` +3. Post a completion comment summarizing what you did: + \`curl -s -X POST "{{paperclipApiUrl}}/issues/{{taskId}}/comments" -H "Content-Type: application/json" -d '{"body":"DONE: "}'\` +{{/taskId}} + +{{#commentId}} +## Comment on This Issue + +Someone commented. Read it: + \`curl -s "{{paperclipApiUrl}}/issues/{{taskId}}/comments/{{commentId}}" | python3 -m json.tool\` +Address the comment, POST a reply if needed, then continue working. +{{/commentId}} + +{{#noTask}} +## Heartbeat Wake — Check for Work + +1. List your open issues: + \`curl -s "{{paperclipApiUrl}}/companies/{{companyId}}/issues?assigneeAgentId={{agentId}}"\` +2. Pick the highest priority and work on it. When done, follow steps 2-3 above. +3. If nothing to do, report briefly what you checked. +{{/noTask}}`; + +function buildPrompt(ctx, config) { + const template = cfgString(config.promptTemplate) || DEFAULT_PROMPT_TEMPLATE; + const taskId = cfgString(ctx.context?.taskId); + const taskTitle = cfgString(ctx.context?.taskTitle) || ""; + const taskBody = cfgString(ctx.context?.taskBody) || ""; + const commentId = cfgString(ctx.context?.commentId) || ""; + const wakeReason = cfgString(ctx.context?.wakeReason) || ""; + const agentName = ctx.agent?.name || "DeepSeek Agent"; + const companyName = cfgString(ctx.context?.companyName) || ""; + const projectName = cfgString(ctx.context?.projectName) || ""; + + let paperclipApiUrl = + cfgString(config.paperclipApiUrl) || + process.env.PAPERCLIP_API_URL || + "http://127.0.0.1:3100/api"; + if (!paperclipApiUrl.endsWith("/api")) { + paperclipApiUrl = paperclipApiUrl.replace(/\/+$/, "") + "/api"; + } + + const vars = { + agentId: ctx.agent?.id || "", + agentName, + companyId: ctx.agent?.companyId || "", + companyName, + runId: ctx.runId || "", + taskId: taskId || "", + taskTitle, + taskBody, + commentId, + wakeReason, + projectName, + paperclipApiUrl, + }; + + let rendered = template; + rendered = rendered.replace(/\{\{#taskId\}\}([\s\S]*?)\{\{\/taskId\}\}/g, taskId ? "$1" : ""); + rendered = rendered.replace(/\{\{#noTask\}\}([\s\S]*?)\{\{\/noTask\}\}/g, taskId ? "" : "$1"); + rendered = rendered.replace(/\{\{#commentId\}\}([\s\S]*?)\{\{\/commentId\}\}/g, commentId ? "$1" : ""); + return renderTemplate(rendered, vars); +} + +function cleanResponse(raw) { + return raw + .split("\n") + .filter((line) => { + const t = line.trim(); + if (!t) return true; + if (t.startsWith("[tool]") || t.startsWith("[hermes]") || t.startsWith("[paperclip]") || t.startsWith("[deepseek]")) return false; + if (t.startsWith("session_id:")) return false; + if (/^\[\d{4}-\d{2}-\d{2}T/.test(t)) return false; + if (/^\[done\]\s*┊/.test(t)) return false; + if (/^┊\s*[\p{Emoji_Presentation}]/u.test(t) && !/^┊\s*💬/.test(t)) return false; + if (/^\p{Emoji_Presentation}\s*(Completed|Running|Error)?\s*$/u.test(t)) return false; + return true; + }) + .map((line) => { + let t = line.replace(/^[\s]*┊\s*💬\s*/, "").trim(); + t = t.replace(/^\[done\]\s*/, "").trim(); + return t; + }) + .join("\n") + .replace(/\n{3,}/g, "\n\n") + .trim(); +} + +function parseHermesOutput(stdout, stderr) { + const combined = stdout + "\n" + stderr; + const result = {}; + + const sessionMatch = stdout.match(SESSION_ID_REGEX); + if (sessionMatch?.[1]) { + result.sessionId = sessionMatch[1]; + const sessionLineIdx = stdout.lastIndexOf("\nsession_id:"); + if (sessionLineIdx > 0) { + result.response = cleanResponse(stdout.slice(0, sessionLineIdx)); + } + } else { + const legacyMatch = combined.match(SESSION_ID_REGEX_LEGACY); + if (legacyMatch?.[1]) result.sessionId = legacyMatch[1]; + const cleaned = cleanResponse(stdout); + if (cleaned.length > 0) result.response = cleaned; + } + + const usageMatch = combined.match(TOKEN_USAGE_REGEX); + if (usageMatch) { + result.usage = { + inputTokens: parseInt(usageMatch[1], 10) || 0, + outputTokens: parseInt(usageMatch[2], 10) || 0, + }; + } + + const costMatch = combined.match(COST_REGEX); + if (costMatch?.[1]) result.costUsd = parseFloat(costMatch[1]); + + if (stderr.trim()) { + const errorLines = stderr + .split("\n") + .filter((line) => /error|exception|traceback|failed/i.test(line)) + .filter((line) => !/INFO|DEBUG|warn/i.test(line)); + if (errorLines.length > 0) result.errorMessage = errorLines.slice(0, 5).join("\n"); + } + + return result; +} + +export async function execute(ctx) { + const config = ctx.agent?.adapterConfig ?? {}; + + const hermesCmd = cfgString(config.hermesCommand) || HERMES_CLI; + const model = cfgString(config.model) || DEFAULT_MODEL; + const provider = cfgString(config.provider) || DEFAULT_PROVIDER; + const profileHome = cfgString(config.hermesProfileHome) || DEFAULT_PROFILE_HOME; + const timeoutSec = cfgNumber(config.timeoutSec) || DEFAULT_TIMEOUT_SEC; + const graceSec = cfgNumber(config.graceSec) || DEFAULT_GRACE_SEC; + const toolsets = cfgString(config.toolsets) || cfgStringArray(config.enabledToolsets)?.join(","); + const extraArgs = cfgStringArray(config.extraArgs); + const persistSession = cfgBoolean(config.persistSession) !== false; + const worktreeMode = cfgBoolean(config.worktreeMode) === true; + const checkpoints = cfgBoolean(config.checkpoints) === true; + const useQuiet = cfgBoolean(config.quiet) !== false; + + const prompt = buildPrompt(ctx, config); + + const args = ["chat", "-q", prompt]; + if (useQuiet) args.push("-Q"); + if (model) args.push("-m", model); + args.push("--provider", provider); + if (toolsets) args.push("-t", toolsets); + if (worktreeMode) args.push("-w"); + if (checkpoints) args.push("--checkpoints"); + if (cfgBoolean(config.verbose) === true) args.push("-v"); + args.push("--source", "tool"); + args.push("--yolo"); + + const prevSessionId = cfgString(ctx.runtime?.sessionParams?.sessionId); + if (persistSession && prevSessionId) args.push("--resume", prevSessionId); + if (extraArgs?.length) args.push(...extraArgs); + + // Pin Hermes to the DeepSeek profile by default. The agent can override + // by setting adapter_config.hermesProfileHome or adapter_config.env.HERMES_HOME. + const env = { + ...process.env, + ...buildPaperclipEnv(ctx.agent), + HERMES_HOME: profileHome, + }; + if (ctx.runId) env.PAPERCLIP_RUN_ID = ctx.runId; + const taskId = cfgString(ctx.context?.taskId); + if (taskId) env.PAPERCLIP_TASK_ID = taskId; + + // Parity with hermes_local (paperclip-src/server/src/adapters/registry.ts:267): + // inject the per-run agent auth token so the agent can call the Paperclip API. + // Without this, every Paperclip API write from the running agent fails with 401. + // + // Resolve env from the runtime-resolved config (ctx.config.env contains plain + // strings — Paperclip's secrets service unwraps {type:"plain"|"secret_ref", ...} + // bindings before invocation in services/heartbeat.ts:5433-5437). + // Fall back to agent.adapterConfig.env with manual unwrapping for older paths. + function unwrapEnvValue(v) { + if (typeof v === "string") return v; + if (v && typeof v === "object" && !Array.isArray(v)) { + if (v.type === "plain" && typeof v.value === "string") return v.value; + } + return undefined; // skip secret_ref / unknown types — let resolver handle them + } + const resolvedUserEnv = + ctx.config && typeof ctx.config === "object" && ctx.config.env && typeof ctx.config.env === "object" && !Array.isArray(ctx.config.env) + ? ctx.config.env + : null; + const rawUserEnv = + typeof config.env === "object" && config.env !== null && !Array.isArray(config.env) + ? config.env + : {}; + // Prefer pre-resolved values from ctx.config.env when available; fall back to + // unwrapping raw bindings from agent.adapterConfig.env. + const flattenedUserEnv = {}; + for (const [k, v] of Object.entries(rawUserEnv)) { + const resolved = resolvedUserEnv && typeof resolvedUserEnv[k] === "string" ? resolvedUserEnv[k] : unwrapEnvValue(v); + if (typeof resolved === "string") flattenedUserEnv[k] = resolved; + } + const userEnvApiKey = flattenedUserEnv.PAPERCLIP_API_KEY; + const explicitApiKey = + typeof userEnvApiKey === "string" && userEnvApiKey.trim().length > 0; + if (ctx.authToken && !explicitApiKey) env.PAPERCLIP_API_KEY = ctx.authToken; + + // Apply unwrapped user env (may override HERMES_HOME, OPENAI_API_KEY, etc.). + Object.assign(env, flattenedUserEnv); + + const cwd = cfgString(config.cwd) || cfgString(ctx.config?.workspaceDir) || "."; + try { + await ensureAbsoluteDirectory(cwd); + } catch { + // non-fatal + } + + await ctx.onLog( + "stdout", + `[deepseek] Starting Hermes (model=${model}, provider=${provider}, profileHome=${env.HERMES_HOME}, timeout=${timeoutSec}s)\n`, + ); + if (prevSessionId) { + await ctx.onLog("stdout", `[deepseek] Resuming session: ${prevSessionId}\n`); + } + + // Reclassify benign Hermes stderr lines as stdout so the UI doesn't paint them red. + const wrappedOnLog = async (stream, chunk) => { + if (stream === "stderr") { + const trimmed = chunk.trimEnd(); + const isBenign = + /^\[?\d{4}[-/]\d{2}[-/]\d{2}T/.test(trimmed) || + /^[A-Z]+:\s+(INFO|DEBUG|WARN|WARNING)\b/.test(trimmed) || + /Successfully registered all tools/.test(trimmed) || + /MCP [Ss]erver/.test(trimmed) || + /tool registered successfully/.test(trimmed) || + /Application initialized/.test(trimmed); + if (isBenign) return ctx.onLog("stdout", chunk); + } + return ctx.onLog(stream, chunk); + }; + + // Forward ctx.onSpawn so Paperclip persists processPid/processGroupId to the + // heartbeat_runs row. Without it, the reaper cannot verify the child is alive + // (run.processPid is null) and treats the run as orphaned during long quiet + // phases (DeepSeek V4-Pro thinking can be silent for 60-90s per turn). + const result = await runChildProcess(ctx.runId, hermesCmd, args, { + cwd, + env, + timeoutSec, + graceSec, + onLog: wrappedOnLog, + onSpawn: ctx.onSpawn, + }); + + const parsed = parseHermesOutput(result.stdout || "", result.stderr || ""); + await ctx.onLog( + "stdout", + `[deepseek] Exit code: ${result.exitCode ?? "null"}, timed out: ${result.timedOut}\n`, + ); + if (parsed.sessionId) { + await ctx.onLog("stdout", `[deepseek] Session: ${parsed.sessionId}\n`); + } + + const executionResult = { + exitCode: result.exitCode, + signal: result.signal, + timedOut: result.timedOut, + provider, + model, + }; + if (parsed.errorMessage) executionResult.errorMessage = parsed.errorMessage; + if (parsed.usage) executionResult.usage = parsed.usage; + if (parsed.costUsd !== undefined) executionResult.costUsd = parsed.costUsd; + if (parsed.response) executionResult.summary = parsed.response.slice(0, 2000); + + executionResult.resultJson = { + result: parsed.response || "", + session_id: parsed.sessionId || null, + usage: parsed.usage || null, + cost_usd: parsed.costUsd ?? null, + }; + + if (persistSession && parsed.sessionId) { + executionResult.sessionParams = { sessionId: parsed.sessionId }; + executionResult.sessionDisplayId = parsed.sessionId.slice(0, 16); + } + + return executionResult; +} diff --git a/adapters/deepseek-paperclip-adapter/dist/server/session-codec.js b/adapters/deepseek-paperclip-adapter/dist/server/session-codec.js new file mode 100644 index 0000000..75e6f8e --- /dev/null +++ b/adapters/deepseek-paperclip-adapter/dist/server/session-codec.js @@ -0,0 +1,29 @@ +/** + * Session codec — Hermes uses a single sessionId for cross-heartbeat continuity + * via the --resume CLI flag. Same shape as the Hermes adapter. + */ + +function readNonEmptyString(value) { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +export const sessionCodec = { + deserialize(raw) { + if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null; + const sessionId = + readNonEmptyString(raw.sessionId) ?? readNonEmptyString(raw.session_id); + if (!sessionId) return null; + return { sessionId }; + }, + serialize(params) { + if (!params) return null; + const sessionId = + readNonEmptyString(params.sessionId) ?? readNonEmptyString(params.session_id); + if (!sessionId) return null; + return { sessionId }; + }, + getDisplayId(params) { + if (!params) return null; + return readNonEmptyString(params.sessionId) ?? readNonEmptyString(params.session_id); + }, +}; diff --git a/adapters/deepseek-paperclip-adapter/dist/server/skills.js b/adapters/deepseek-paperclip-adapter/dist/server/skills.js new file mode 100644 index 0000000..4a9b4cf --- /dev/null +++ b/adapters/deepseek-paperclip-adapter/dist/server/skills.js @@ -0,0 +1,171 @@ +/** + * Skill snapshot for the DeepSeek-via-Hermes adapter. + * + * Hermes manages its own skills under ~/.hermes/skills/ (global; not per-profile). + * Paperclip-managed skills declared in adapter config are surfaced as + * "company_managed" entries — same behavior as the upstream Hermes adapter. + */ + +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { + readPaperclipRuntimeSkillEntries, + resolvePaperclipDesiredSkillNames, +} from "@paperclipai/adapter-utils/server-utils"; +import { ADAPTER_TYPE } from "../shared/constants.js"; + +const __moduleDir = path.dirname(fileURLToPath(import.meta.url)); + +function asString(value) { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +function parseSkillFrontmatter(content) { + const match = content.match(/^---\s*\n([\s\S]*?)\n---/); + if (!match) return {}; + const fm = {}; + for (const line of match[1].split("\n")) { + const idx = line.indexOf(":"); + if (idx === -1) continue; + const key = line.slice(0, idx).trim(); + let val = line.slice(idx + 1).trim(); + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + val = val.slice(1, -1); + } + fm[key] = val; + } + return fm; +} + +async function buildSkillEntry(key, skillMdPath, categoryPath) { + let description = null; + try { + const content = await fs.readFile(skillMdPath, "utf8"); + description = parseSkillFrontmatter(content).description ?? null; + } catch { + // ignore + } + return { + key, + runtimeName: key, + desired: true, + managed: false, + state: "installed", + origin: "user_installed", + originLabel: "Hermes skill", + locationLabel: `~/.hermes/skills/${categoryPath}`, + readOnly: true, + sourcePath: skillMdPath, + targetPath: null, + detail: description, + }; +} + +async function scanHermesSkills(skillsHome) { + const entries = []; + try { + const cats = await fs.readdir(skillsHome, { withFileTypes: true }); + for (const cat of cats) { + if (!cat.isDirectory()) continue; + const catPath = path.join(skillsHome, cat.name); + const topSkill = path.join(catPath, "SKILL.md"); + if (await fs.stat(topSkill).catch(() => null)) { + entries.push(await buildSkillEntry(cat.name, topSkill, cat.name)); + } + const items = await fs.readdir(catPath, { withFileTypes: true }).catch(() => []); + for (const item of items) { + if (!item.isDirectory()) continue; + const skillMd = path.join(catPath, item.name, "SKILL.md"); + if (await fs.stat(skillMd).catch(() => null)) { + entries.push(await buildSkillEntry(item.name, skillMd, `${cat.name}/${item.name}`)); + } + } + } + } catch { + // ~/.hermes/skills/ doesn't exist + } + return entries.sort((a, b) => a.key.localeCompare(b.key)); +} + +async function buildSnapshot(config) { + const homedir = + asString(config.env?.HOME) ?? + process.env.HOME ?? + "/home/chaim"; + const hermesSkillsHome = path.join(homedir, ".hermes", "skills"); + + const paperclipEntries = await readPaperclipRuntimeSkillEntries(config, __moduleDir); + const desiredSkills = resolvePaperclipDesiredSkillNames(config, paperclipEntries); + const desiredSet = new Set(desiredSkills); + const availableByKey = new Map(paperclipEntries.map((e) => [e.key, e])); + + const hermesSkillEntries = await scanHermesSkills(hermesSkillsHome); + const hermesKeys = new Set(hermesSkillEntries.map((e) => e.key)); + + const entries = []; + const warnings = []; + + for (const entry of paperclipEntries) { + const desired = desiredSet.has(entry.key); + entries.push({ + key: entry.key, + runtimeName: entry.runtimeName, + desired, + managed: true, + state: desired ? "configured" : "available", + origin: entry.required ? "paperclip_required" : "company_managed", + originLabel: entry.required ? "Required by Paperclip" : "Managed by Paperclip", + readOnly: false, + sourcePath: entry.source, + targetPath: null, + detail: desired ? "Will be available on the next run via Hermes skill loading." : null, + required: Boolean(entry.required), + requiredReason: entry.requiredReason ?? null, + }); + } + + for (const entry of hermesSkillEntries) { + if (availableByKey.has(entry.key)) continue; + entries.push(entry); + } + + for (const desired of desiredSkills) { + if (availableByKey.has(desired) || hermesKeys.has(desired)) continue; + warnings.push(`Desired skill "${desired}" is not available in Paperclip or Hermes skills.`); + entries.push({ + key: desired, + runtimeName: null, + desired: true, + managed: true, + state: "missing", + origin: "external_unknown", + originLabel: "External or unavailable", + readOnly: false, + sourcePath: null, + targetPath: null, + detail: "Cannot find this skill in Paperclip or ~/.hermes/skills/.", + }); + } + + return { + adapterType: ADAPTER_TYPE, + supported: true, + mode: "persistent", + desiredSkills, + entries, + warnings, + }; +} + +export async function listSkills(ctx) { + return buildSnapshot(ctx.config); +} + +export async function syncSkills(ctx, _desired) { + return buildSnapshot(ctx.config); +} + +export function resolveDesiredSkillNames(config, availableEntries) { + return resolvePaperclipDesiredSkillNames(config, availableEntries); +} diff --git a/adapters/deepseek-paperclip-adapter/dist/server/test.js b/adapters/deepseek-paperclip-adapter/dist/server/test.js new file mode 100644 index 0000000..6b70590 --- /dev/null +++ b/adapters/deepseek-paperclip-adapter/dist/server/test.js @@ -0,0 +1,164 @@ +/** + * Environment test for the DeepSeek (via Hermes) adapter. + */ + +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { + HERMES_CLI, + ADAPTER_TYPE, + DEFAULT_PROFILE_HOME, +} from "../shared/constants.js"; + +const execFileAsync = promisify(execFile); + +function asString(v) { + return typeof v === "string" ? v : undefined; +} + +async function checkCliInstalled(command) { + try { + await execFileAsync(command, ["--version"], { timeout: 10_000 }); + return null; + } catch (err) { + if (err && err.code === "ENOENT") { + return { + level: "error", + message: `Hermes CLI "${command}" not found in PATH`, + hint: "Install Hermes Agent: pip install hermes-agent", + code: "deepseek_hermes_cli_not_found", + }; + } + return null; + } +} + +async function checkProfile(profileHome) { + try { + const stat = await fs.stat(profileHome); + if (!stat.isDirectory()) { + return { + level: "error", + message: `Profile path is not a directory: ${profileHome}`, + hint: "Create the directory or override hermesProfileHome in adapter config.", + code: "deepseek_profile_not_dir", + }; + } + } catch { + return { + level: "error", + message: `Hermes profile dir does not exist: ${profileHome}`, + hint: "Create the profile dir with config.yaml + .env (DEEPSEEK_API_KEY).", + code: "deepseek_profile_missing", + }; + } + + const configPath = path.join(profileHome, "config.yaml"); + try { + await fs.stat(configPath); + } catch { + return { + level: "error", + message: `Profile is missing config.yaml: ${configPath}`, + hint: "Add config.yaml with model.default + model.base_url + model.key_env.", + code: "deepseek_profile_no_config", + }; + } + + return { + level: "info", + message: `Profile resolved: ${profileHome}`, + code: "deepseek_profile_ok", + }; +} + +async function checkApiKey(profileHome, configEnv) { + // 1. config.env (resolved by Paperclip from secrets) + if (configEnv && typeof configEnv === "object" && asString(configEnv.DEEPSEEK_API_KEY)) { + return { + level: "info", + message: "DEEPSEEK_API_KEY found in adapter env config", + code: "deepseek_api_key_in_config", + }; + } + // 2. Profile-local .env + try { + const envFile = path.join(profileHome, ".env"); + const text = await fs.readFile(envFile, "utf-8"); + if (/^\s*DEEPSEEK_API_KEY=/m.test(text)) { + return { + level: "info", + message: `DEEPSEEK_API_KEY found in ${envFile}`, + code: "deepseek_api_key_in_profile", + }; + } + } catch { + // ignore + } + // 3. Process env + if (process.env.DEEPSEEK_API_KEY) { + return { + level: "info", + message: "DEEPSEEK_API_KEY found in Paperclip process env", + code: "deepseek_api_key_in_process", + }; + } + return { + level: "error", + message: "DEEPSEEK_API_KEY not found in adapter env, profile .env, or process env", + hint: "Add DEEPSEEK_API_KEY to /.env or to the agent's env secrets.", + code: "deepseek_api_key_missing", + }; +} + +export async function testEnvironment(ctx) { + const config = ctx.config ?? {}; + const command = asString(config.hermesCommand) || HERMES_CLI; + const profileHome = asString(config.hermesProfileHome) || DEFAULT_PROFILE_HOME; + const checks = []; + + const cliCheck = await checkCliInstalled(command); + if (cliCheck) { + checks.push(cliCheck); + if (cliCheck.level === "error") { + return { + adapterType: ADAPTER_TYPE, + status: "fail", + checks, + testedAt: new Date().toISOString(), + }; + } + } + + const profileCheck = await checkProfile(profileHome); + checks.push(profileCheck); + if (profileCheck.level === "error") { + return { + adapterType: ADAPTER_TYPE, + status: "fail", + checks, + testedAt: new Date().toISOString(), + }; + } + + const apiKeyCheck = await checkApiKey(profileHome, config.env); + checks.push(apiKeyCheck); + + const model = asString(config.model); + checks.push({ + level: "info", + message: model ? `Model: ${model}` : "Using profile default model", + code: "deepseek_model", + }); + + const hasErrors = checks.some((c) => c.level === "error"); + const hasWarnings = checks.some((c) => c.level === "warn"); + return { + adapterType: ADAPTER_TYPE, + status: hasErrors ? "fail" : hasWarnings ? "warn" : "pass", + checks, + testedAt: new Date().toISOString(), + }; +} diff --git a/adapters/deepseek-paperclip-adapter/dist/shared/constants.js b/adapters/deepseek-paperclip-adapter/dist/shared/constants.js new file mode 100644 index 0000000..46ffa4f --- /dev/null +++ b/adapters/deepseek-paperclip-adapter/dist/shared/constants.js @@ -0,0 +1,36 @@ +/** + * Shared constants for the DeepSeek (via Hermes) Paperclip adapter. + */ + +export const ADAPTER_TYPE = "deepseek_local"; +export const ADAPTER_LABEL = "DeepSeek (via Hermes)"; + +/** Default Hermes CLI binary name. */ +export const HERMES_CLI = "hermes"; + +/** Default profile directory used as HERMES_HOME if the agent does not override it. */ +export const DEFAULT_PROFILE_HOME = "/home/chaim/.hermes/profiles/deepseek"; + +/** Default model — V4-Pro is the strongest DeepSeek model currently exposed. */ +export const DEFAULT_MODEL = "deepseek-v4-pro"; + +/** DeepSeek profiles in this stack use Hermes' "custom" provider (user-defined in profile config.yaml). */ +export const DEFAULT_PROVIDER = "custom"; + +/** Default timeout (seconds) for one CLI invocation. */ +export const DEFAULT_TIMEOUT_SEC = 1800; + +/** Grace period (seconds) after SIGTERM before SIGKILL. */ +export const DEFAULT_GRACE_SEC = 30; + +/** Models that DeepSeek's API currently exposes (verified via /v1/models). */ +export const DEEPSEEK_MODELS = [ + { id: "deepseek-v4-pro", label: "DeepSeek V4 Pro" }, + { id: "deepseek-v4-flash", label: "DeepSeek V4 Flash" }, +]; + +/** Regex for extracting session_id from quiet-mode Hermes output. */ +export const SESSION_ID_REGEX = /^session_id:\s*(\S+)/m; +export const SESSION_ID_REGEX_LEGACY = /session[_ ](?:id|saved)[:\s]+([a-zA-Z0-9_-]+)/i; +export const TOKEN_USAGE_REGEX = /tokens?[:\s]+(\d+)\s*(?:input|in)\b.*?(\d+)\s*(?:output|out)\b/i; +export const COST_REGEX = /(?:cost|spent)[:\s]*\$?([\d.]+)/i; diff --git a/adapters/deepseek-paperclip-adapter/package-lock.json b/adapters/deepseek-paperclip-adapter/package-lock.json new file mode 100644 index 0000000..e172f97 --- /dev/null +++ b/adapters/deepseek-paperclip-adapter/package-lock.json @@ -0,0 +1,25 @@ +{ + "name": "deepseek-paperclip-adapter", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "deepseek-paperclip-adapter", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@paperclipai/adapter-utils": "^2026.325.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@paperclipai/adapter-utils": { + "version": "2026.428.0", + "resolved": "https://registry.npmjs.org/@paperclipai/adapter-utils/-/adapter-utils-2026.428.0.tgz", + "integrity": "sha512-kGHpE7rhePPCbnG3OwXbNuHZZuI+XyuFgNSiDnrEeiSbkI2c5XHM2WnWDCZ/NGHULfJW3lWhSxGMFoYqiy38vQ==", + "license": "MIT" + } + } +} diff --git a/adapters/deepseek-paperclip-adapter/package.json b/adapters/deepseek-paperclip-adapter/package.json new file mode 100644 index 0000000..7ed2243 --- /dev/null +++ b/adapters/deepseek-paperclip-adapter/package.json @@ -0,0 +1,21 @@ +{ + "name": "deepseek-paperclip-adapter", + "version": "0.1.0", + "description": "Paperclip adapter for DeepSeek (V4-Pro / V4-Flash) — runs Hermes Agent locally pinned to a DeepSeek profile", + "type": "module", + "license": "MIT", + "private": true, + "main": "./dist/index.js", + "exports": { + ".": "./dist/index.js" + }, + "files": [ + "dist" + ], + "dependencies": { + "@paperclipai/adapter-utils": "^2026.325.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/docs/daphna-voice-fingerprint.md b/docs/daphna-voice-fingerprint.md index 754cec8..434aa5e 100644 --- a/docs/daphna-voice-fingerprint.md +++ b/docs/daphna-voice-fingerprint.md @@ -400,6 +400,54 @@ - **~30 תקדמים חיצוניים** ש**דפנה מצטטת באופן עקבי** (ראה precedent-network.md) - **~15 תקדמים אישיים** שלה עצמה — מהווים את הקאנון האישי שלה +--- + + +## 6.11 לקחים מערר 1200-25 (קרית ענבים, מאי 2026) + +השוואה בין טיוטת הכותב לעריכת דפנה חשפה 7 דפוסי סגנון שלא היו מתועדים: + +### א. סדר בלוקים — תכניות לפני טענות (1xxx) +בתיקי רישוי, דפנה מעדיפה שבלוק ט (תכניות חלות) יופיע **לפני** בלוק ז (טענות). הרציונל: הקורא צריך להכיר את המסגרת הנורמטיבית לפני שהוא קורא את טענות הצדדים. + +**סדר נכון ל-1xxx:** ה → ו → **ט** → ו.ב (רקע מורחב) → ז → ח → י → יא → יב + +### ב. תבנית "להלן מתוך" — חובה +כל התייחסות למסמך מקור מלווה ב-"להלן מתוך [שם המסמך]:" כ-placeholder לציטוט/צילום. **12 מופעים** בעריכה, **0** בטיוטה. זהו דפוס סגנוני מרכזי שחייב להיות אוטומטי. + +דוגמאות: +- "להלן מתוך הוראות התכנית:" +- "להלן מתוך פרוטוקול הדיון בוועדה המקומית:" +- "להלן מתוך הבקשה להיתר:" +- "להלן מתוך מטרת התכנית:" +- "להלן מתוך תשריט מצב מוצע:" + +### ג. רקע עובדתי מורחב — ציר זמן מלא +בלוק ו חייב לספר את "הסיפור" של התיק: הגשת בקשה → פרסום → מספר התנגדויות → ישיבות ועדה מקומית (תאריך + תוצאה לכל אחת) → החלטה סופית → הגשת ערר. הטיוטה נתנה שורה אחת (90 מילים); דפנה הרחיבה ל-3 ישיבות מפורטות (~420 מילים). + +### ד. ניתוח "גשר תכנוני" +כשמבקש שימוש חורג גם מקדם תכנית — דפנה מנתחת: האם השימוש המבוקש **תואם** את התכנון העתידי (→ גשר לגיטימי, כמו בכוכבה תורן)? או **סותר** (→ סטייה כפולה)? מסגרת ניתוח שלמה (249 מילים) שלא הייתה בטיוטה. + +### ה. עיגון כמותי +דפנה מוסיפה נתונים מספריים ספציפיים: "4,404.98 מ"ר לכלל היישוב vs 1,425 מ"ר מבוקש — 32%". המספרים מעגנים את ההחלטה במציאות ומקשים על ערעור. + +### ו. כותרות שטוחות (Heading 2 בלבד) +דפנה השתמשה ב-Heading 2 לכל הסעיפים, כולל תת-נושאים בדיון. **אין Heading 3**. כל סעיף עומד בפני עצמו. + +### ז. הבחנת תקדימים inline +במקום סעיף נפרד "הבחנה מתקדימי העוררת" — ההבחנות מנוסחות inline: "באשר ל-[שם פסק דין]" → מה ההבדל → סיכום. דוגמה: "באשר לבג"ץ 6525/15 עמק שווה... אולם ההבדל מהותי". + +### ביטויי מעבר חדשים (מעריכה 1200-25) +| ביטוי | הקשר | +|-------|-------| +| "עינינו הרואות" | ממצא מתוך מסמך | +| "הנה כי כן" | לפיכך (פורמלי) | +| "נשוב כאן ונבחין" | חזרה להבחנת תקדים | +| "נוסיף ונבהיר" | הוספת הבהרה | +| "מסקנת הדברים" | סיכום סעיף | +| "משכבר קבענו" | הפניה לקביעה קודמת | + + --- ## 7. מה עדיין לא ראינו diff --git a/docs/legal-decision-lessons.md b/docs/legal-decision-lessons.md index 06e4e0a..6c48960 100644 --- a/docs/legal-decision-lessons.md +++ b/docs/legal-decision-lessons.md @@ -385,3 +385,64 @@ The draft's biggest structural error was adding the "נבאר" doctrinal paragra - [ ] Update voice-fingerprint: add new transition phrases - [ ] Update architecture-by-outcome: add "clean acceptance" archetype - [ ] Fix agent opening punctuation: "ונפרט;" not "נפרט." + +--- + +## Lessons from ערר 1200-25 (קרית ענבים — שימוש חורג, דחייה) + +### Source +- Our draft: `data/cases/1200-25/exports/טיוטה-v1.docx` (3,181 words) +- Daphna's edit: `data/cases/1200-25/exports/עריכה-v1.docx` (4,313 words, +35%) +- Date: May 2026 + +### What the Edit Changed + +#### 1. Block Order — Plans Before Claims +- **Draft:** ה→ו→ז→ח→ט→י→יא→יב (plans after procedures) +- **Edit:** ה→ו→**ט**→ו.ב→ז→ח→י→יא→יב (plans BEFORE claims) +- **Lesson:** In licensing cases (1xxx), the reader must understand the normative framework (plans) before reading the parties' arguments about those plans. Block ט should precede Block ז. The new order: opening → brief background → **applicable plans** → expanded background (application + committee proceedings) → claims → procedures → discussion. + +#### 2. "להלן מתוך" Document Insertion Pattern +- **Draft:** 0 occurrences +- **Edit:** 12 occurrences of "להלן מתוך [document name]:" +- **Lesson:** Every reference to a source document must be accompanied by "להלן מתוך [שם המסמך]:" as a placeholder for a direct quote/image. This is a MANDATORY pattern, not optional. Examples: "להלן מתוך הוראות התכנית:", "להלן מתוך פרוטוקול הדיון:", "להלן מתוך הבקשה להיתר:" + +#### 3. Expanded Factual Background (Block ו) +- **Draft:** ~90 words (3%), one paragraph +- **Edit:** ~420 words (10%), covering: (a) the application details, (b) 3 committee meetings with dates and outcomes, (c) the final decision +- **Lesson:** Block ו must tell the full "story" of the case: when the application was filed → when it was published → how many objections → when committee meetings were held → what was decided at each meeting → when the appeal was filed. Each meeting should have date + outcome. + +#### 4. Bridge Planning Analysis ("גשר תכנוני") +- **Draft:** Not present +- **Edit:** 249 words — new analytical framework +- **Lesson:** When an applicant for deviation/variance is also promoting a plan for the same land, the decision must analyze: (a) is the pending plan harmonious with the requested use? If yes → the deviation can serve as a "bridge" until the plan is approved (cite כוכבה תורן). If no → the contradiction STRENGTHENS the rejection. The writer must check `search_case_documents` for pending plans and compare them with the requested use. + +#### 5. Competing Plans Analysis +- **Draft:** Not present (1,033 words added) +- **Edit:** Detailed comparison of the site-specific plan (151-1382787) vs the comprehensive plan (151-1337534) +- **Lesson:** When there's a site-specific plan AND a comprehensive plan, the decision must: (a) describe each plan's scope, (b) compare the permitted uses, (c) show quantitative contradictions (e.g., "the comprehensive plan allocates 4,404 m² for ALL commerce in the settlement, while the request alone is for 1,425 m² — 32%"), (d) conclude whether there's harmony or contradiction. This is often the STRONGEST argument in the decision. + +#### 6. Heading Level — Flat Structure +- **Draft:** Mixed Heading 2 + Heading 3 (nested subsections) +- **Edit:** All Heading 2 (flat structure) +- **Lesson:** Each section stands independently. No nesting. In the discussion, each analytical step is a separate Heading 2 section. + +#### 7. Inline Precedent Distinguishing +- **Draft:** Separate section "הבחנה מתקדימי העוררת" (Heading 3) +- **Edit:** Each precedent distinguished inline with "באשר ל-[case name]" → what's different → conclusion +- **Lesson:** Don't create a separate "distinguishing" section. Address each precedent where it naturally comes up in the discussion, using "באשר ל..." as the opener. + +### New Transition Phrases Identified +- **"עינינו הרואות"** — introducing a document-based finding ("our eyes see that...") +- **"הנה כי כן"** — therefore/accordingly (more formal than "לפיכך") +- **"נשוב כאן ונבחין"** — returning to distinguish a case +- **"נוסיף ונבהיר"** — adding clarification +- **"מסקנת הדברים"** — concluding a subsection +- **"משכבר קבענו"** — since we already established + +### Applied To +- [x] Update legal-decision-lessons.md with lessons 1-7 +- [x] Update daphna-voice-fingerprint.md with structural and style findings +- [ ] Update block-schema.md: block order for 1xxx cases (ט before ז) +- [ ] Update daphna-architecture-by-outcome.md: add "bridge planning" analysis for rejections +- [ ] Update writer system prompt: mandatory "להלן מתוך" pattern diff --git a/scripts/.archive/run_curator_deepseek_test.sh b/scripts/.archive/run_curator_deepseek_test.sh new file mode 100755 index 0000000..c515df1 --- /dev/null +++ b/scripts/.archive/run_curator_deepseek_test.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# One-off A/B test runner: runs the Knowledge Curator (Hermes) on CMP-78 using +# DeepSeek V4-Pro instead of the default Sonnet 4.5 (via marcus/sonnet gateway). +# Compare against CMP-80 which runs with the default config. +set -euo pipefail + +PROFILE_HOME="/home/chaim/.hermes/profiles/curator-cmp-deepseek" +PAPERCLIP_API_URL="http://localhost:3100/api" +# CMP curator agent's Paperclip key (from Infisical: nautilus /legal-ai HERMES_CURATOR_CMP_PAPERCLIP_KEY) +PAPERCLIP_API_KEY="pcp_c87edcf306d06fce13fac701bb6d747191d61dba5b51e903" +PAPERCLIP_TASK_ID="beb745e5-7195-40c5-9ac0-e9682c2c5184" # CMP-78 +PAPERCLIP_TASK_KEY="$PAPERCLIP_TASK_ID" +PAPERCLIP_TASK_TITLE="[ערר 1130-25] סקירת ידע — Knowledge Curator (DeepSeek A/B test)" +PAPERCLIP_RUN_ID="deepseek-ab-$(date +%s)" +PAPERCLIP_WAKE_REASON="manual_deepseek_ab_test" + +# Rendered prompt — copy of the curator template with mustache variables resolved +# manually for CMP-78. We also add a clear "[ניסוי DeepSeek V4-Pro]" prefix so +# the resulting comment is distinguishable from the default-Sonnet run on CMP-80. +read -r -d '' PROMPT <<'EOF' || true +אתה מנהל ידע (Knowledge Curator) של ועדת הערר. נעור על תיק שדפנה סימנה כסופי. + +תיק: [ערר 1130-25] סקירת ידע — Knowledge Curator +issue ID: beb745e5-7195-40c5-9ac0-e9682c2c5184 +run reason: manual_deepseek_ab_test + +**הקשר חשוב — ניסוי A/B:** זוהי ריצה ידנית באמצעות DeepSeek V4-Pro במקום ה-Sonnet הרגיל. כל ה-comment שתפרסם חייב להתחיל בכותרת `[ניסוי DeepSeek V4-Pro]` כדי שנוכל להבדיל מהריצה המקבילה ב-CMP-80 (שרצה עם Sonnet). אל תעיר סוכנים אחרים. אל תיצור issues חדשים. אל תפתח interaction. + +הוראות: +דפנה סימנה את ההחלטה הסופית של תיק 1130-25 כסופית. +קובץ סופי: `סופי-1130-25.docx` + +סקור את ההחלטה מול skills/decision/SKILL.md ו-docs/legal-decision-lessons.md. +חפש 3-5 דפוסי סגנון/דיון שלא תועדו. כתוב comment בעברית, ניטרלי, ממוספר. + +# שלבי ביצוע + +## 1. קונטקסט +- קרא את MEMORY.md שלך (memory tool) — מה כבר זיהית. +- קרא `/home/chaim/legal-ai/skills/decision/SKILL.md` (file tool) — מה כבר תועד. + +## 2. נתונים +- `mcp__legal-ai__case_get` עם case_number `1130-25` — מטא-דאטה. +- `mcp__legal-ai__case_get_final_text` עם case_number `1130-25` — קרא את הטקסט המלא של ההחלטה הסופית. +- אם רלוונטי: `mcp__legal-ai__search_decisions` להשוואה לחלטות קודמות. + +## 3. ניתוח +חפש 3-5 דפוסים/פערים. לכל ממצא: מה ראיתי + מה זה אומר + הצעה ניסוחית מדויקת. + +## 4. כתוב comment הממצאים +```bash +curl -sS -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "Content-Type: application/json" \ + -d "$(jq -n --arg b "$BODY" '{body:$b}')" \ + "$PAPERCLIP_API_URL/issues/$PAPERCLIP_TASK_ID/comments" +``` + +פורמט ה-body: +- שורה ראשונה: `[ניסוי DeepSeek V4-Pro]` +- אחר כך פסקה אחת מבוא קצרה +- אחר כך הממצאים ממוספרים + +## 5. סגור את ה-issue +```bash +curl -sS -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "Content-Type: application/json" \ + -d '{"status":"done"}' "$PAPERCLIP_API_URL/issues/$PAPERCLIP_TASK_ID" +``` + +# כללים +- אל תעדכן קבצים (skills/, lessons.py, DB) בעצמך. רק comment. +- אל תיצור issues חדשים. +- אל תעיר סוכנים אחרים. +- אל תפתח interaction. +- בעיה? comment קצר עם הסיבה + סגור (status=done). +EOF + +export HERMES_HOME="$PROFILE_HOME" +export PAPERCLIP_API_URL PAPERCLIP_API_KEY PAPERCLIP_TASK_ID PAPERCLIP_TASK_KEY \ + PAPERCLIP_TASK_TITLE PAPERCLIP_RUN_ID PAPERCLIP_WAKE_REASON + +echo "=== DeepSeek V4-Pro Curator A/B test on CMP-78 ===" +echo "HERMES_HOME=$HERMES_HOME" +echo "TASK_ID=$PAPERCLIP_TASK_ID" +echo "RUN_ID=$PAPERCLIP_RUN_ID" +echo "Starting Hermes..." +echo "---" + +hermes -z "$PROMPT" --yolo chat 2>&1 diff --git a/scripts/.archive/run_curator_deepseek_test_v2.sh b/scripts/.archive/run_curator_deepseek_test_v2.sh new file mode 100755 index 0000000..d12e1f9 --- /dev/null +++ b/scripts/.archive/run_curator_deepseek_test_v2.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# A/B test runner #2: DeepSeek V4-Pro on CMP-78 — WITH interaction step +# (matching the full Sonnet baseline workflow on CMP-80, including ask_user_questions). +set -euo pipefail + +PROFILE_HOME="/home/chaim/.hermes/profiles/curator-cmp-deepseek" +PAPERCLIP_API_URL="http://localhost:3100/api" +PAPERCLIP_API_KEY="pcp_c87edcf306d06fce13fac701bb6d747191d61dba5b51e903" +PAPERCLIP_TASK_ID="beb745e5-7195-40c5-9ac0-e9682c2c5184" # CMP-78 +PAPERCLIP_TASK_KEY="$PAPERCLIP_TASK_ID" +PAPERCLIP_TASK_TITLE="[ערר 1130-25] סקירת ידע — DeepSeek V4-Pro test #2 (with interaction)" +PAPERCLIP_RUN_ID="deepseek-ab2-$(date +%s)" +PAPERCLIP_WAKE_REASON="manual_deepseek_ab_test_v2_with_interaction" + +read -r -d '' PROMPT <<'EOF' || true +אתה מנהל ידע (Knowledge Curator) של ועדת הערר. נעור על תיק שדפנה סימנה כסופי. + +תיק: [ערר 1130-25] סקירת ידע — Knowledge Curator +issue ID: beb745e5-7195-40c5-9ac0-e9682c2c5184 +run reason: manual_deepseek_ab_test_v2_with_interaction + +**הקשר חשוב — ניסוי A/B #2:** זוהי ריצה שנייה ידנית באמצעות DeepSeek V4-Pro, הפעם **עם interaction מלא** כדי להשוות הוגנת מול ריצת Sonnet ב-CMP-80. כל הפלטים שתפרסם חייבים להתחיל בכותרת `[ניסוי DeepSeek V4-Pro #2 — עם interaction]`. אל תעיר סוכנים אחרים. אל תיצור issues חדשים. + +הוראות: +דפנה סימנה את ההחלטה הסופית של תיק 1130-25 כסופית. +קובץ סופי: `סופי-1130-25.docx` + +סקור את ההחלטה מול skills/decision/SKILL.md ו-docs/legal-decision-lessons.md. +חפש 3-5 דפוסי סגנון/דיון שלא תועדו. כתוב comment בעברית, ניטרלי, ממוספר. + +# שלבי ביצוע + +## 1. קונטקסט +- קרא את MEMORY.md שלך (memory tool) — מה כבר זיהית. +- קרא `/home/chaim/legal-ai/skills/decision/SKILL.md` (file tool) — מה כבר תועד. + +## 2. נתונים +- `mcp__legal-ai__case_get` עם case_number `1130-25` — מטא-דאטה. +- `mcp__legal-ai__case_get_final_text` עם case_number `1130-25` — קרא את הטקסט המלא של ההחלטה הסופית. + +## 3. ניתוח +חפש 3-5 דפוסים/פערים. לכל ממצא: מה ראיתי + מה זה אומר + הצעה ניסוחית מדויקת. + +## 4. כתוב comment הממצאים +```bash +curl -sS -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "Content-Type: application/json" \ + -d "$(jq -n --arg b "$BODY" '{body:$b}')" \ + "$PAPERCLIP_API_URL/issues/$PAPERCLIP_TASK_ID/comments" +``` + +פורמט ה-body: +- שורה ראשונה: `[ניסוי DeepSeek V4-Pro #2 — עם interaction]` +- אחר כך פסקה אחת מבוא קצרה +- אחר כך הממצאים ממוספרים + +## 5. פתח interaction מסוג ask_user_questions +זה השלב שעבד את Sonnet הרבה זמן — בוא נראה כמה זמן יקח לך. + +```bash +curl -sS -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "Content-Type: application/json" \ + "$PAPERCLIP_API_URL/issues/$PAPERCLIP_TASK_ID/interactions" \ + -d '{ + "kind": "ask_user_questions", + "idempotencyKey": "curator-deepseek-v2:'"$PAPERCLIP_TASK_ID"':select", + "title": "[DeepSeek] איזה ממצאים שווים עדכון?", + "continuationPolicy": "wake_assignee", + "payload": { + "version": 1, + "submitLabel": "אשר בחירה", + "questions": [{ + "id": "findings_to_propose", + "prompt": "סמן את הממצאים שאני אכין כהצעת עדכון ל-style guide", + "selectionMode": "multi", + "options": [ + {"id":"f1","label":"<מילוי לפי ממצא 1>","description":"<תקציר>"}, + {"id":"f2","label":"<מילוי לפי ממצא 2>","description":"<תקציר>"} + ] + }] + } + }' +``` + +מלא את ה-options לפי הממצאים שלך — אופציה אחת לכל ממצא ממוספר. + +## 6. עדכן issue ל-status=in_review (לא done — ממתינים לבחירת חיים) +```bash +curl -sS -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "Content-Type: application/json" \ + -d '{"status":"in_review"}' "$PAPERCLIP_API_URL/issues/$PAPERCLIP_TASK_ID" +``` + +# כללים +- אל תעדכן קבצים (skills/, lessons.py, DB) בעצמך. רק comment + interaction. +- אל תיצור issues חדשים. +- אל תעיר סוכנים אחרים. +- בעיה? comment קצר עם הסיבה + סגור (status=done). +EOF + +export HERMES_HOME="$PROFILE_HOME" +export PAPERCLIP_API_URL PAPERCLIP_API_KEY PAPERCLIP_TASK_ID PAPERCLIP_TASK_KEY \ + PAPERCLIP_TASK_TITLE PAPERCLIP_RUN_ID PAPERCLIP_WAKE_REASON + +echo "=== DeepSeek V4-Pro #2 (with interaction) — CMP-78 ===" +echo "HERMES_HOME=$HERMES_HOME" +echo "TASK_ID=$PAPERCLIP_TASK_ID" +echo "RUN_ID=$PAPERCLIP_RUN_ID" +echo "Started: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" +echo "---" + +START_EPOCH=$(date +%s) +hermes -z "$PROMPT" --yolo chat 2>&1 +END_EPOCH=$(date +%s) +DURATION=$((END_EPOCH - START_EPOCH)) +echo "" +echo "=== Run finished ===" +echo "Ended: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" +echo "Duration: ${DURATION}s ($((DURATION/60))m $((DURATION%60))s)" diff --git a/scripts/.archive/run_curator_sonnet_rerun.sh b/scripts/.archive/run_curator_sonnet_rerun.sh new file mode 100755 index 0000000..ded45e6 --- /dev/null +++ b/scripts/.archive/run_curator_sonnet_rerun.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# A/B test #3: Sonnet 4.5 re-run on CMP-78 — same task as DeepSeek #2 but with Sonnet. +# Goal: check if Sonnet is consistent across runs (esp. the case-outcome detection), +# given that the original Sonnet baseline on CMP-80 misread the outcome as "דחייה" +# while the actual result is "קבלה חלקית". +set -euo pipefail + +PROFILE_HOME="/home/chaim/.hermes/profiles/curator-cmp" # default Sonnet profile +PAPERCLIP_API_URL="http://localhost:3100/api" +PAPERCLIP_API_KEY="pcp_c87edcf306d06fce13fac701bb6d747191d61dba5b51e903" +PAPERCLIP_TASK_ID="beb745e5-7195-40c5-9ac0-e9682c2c5184" # CMP-78 +PAPERCLIP_TASK_KEY="$PAPERCLIP_TASK_ID" +PAPERCLIP_TASK_TITLE="[ערר 1130-25] סקירת ידע — Sonnet rerun (consistency check)" +PAPERCLIP_RUN_ID="sonnet-rerun-$(date +%s)" +PAPERCLIP_WAKE_REASON="manual_sonnet_consistency_rerun" + +read -r -d '' PROMPT <<'EOF' || true +אתה מנהל ידע (Knowledge Curator) של ועדת הערר. נעור על תיק שדפנה סימנה כסופי. + +תיק: [ערר 1130-25] סקירת ידע — Knowledge Curator +issue ID: beb745e5-7195-40c5-9ac0-e9682c2c5184 +run reason: manual_sonnet_consistency_rerun + +**הקשר חשוב — ניסוי A/B #3:** זוהי ריצה חוזרת ידנית באמצעות Sonnet 4.5 (אותו מודל שהריץ ב-CMP-80) — בדיקת עקביות. כל הפלטים שתפרסם חייבים להתחיל בכותרת `[ניסוי Sonnet 4.5 — ריצה חוזרת על CMP-78]`. אל תעיר סוכנים אחרים. אל תיצור issues חדשים. + +הוראות: +דפנה סימנה את ההחלטה הסופית של תיק 1130-25 כסופית. +קובץ סופי: `סופי-1130-25.docx` + +סקור את ההחלטה מול skills/decision/SKILL.md ו-docs/legal-decision-lessons.md. +חפש 3-5 דפוסי סגנון/דיון שלא תועדו. כתוב comment בעברית, ניטרלי, ממוספר. + +# שלבי ביצוע + +## 1. קונטקסט +- קרא את MEMORY.md שלך (memory tool) — מה כבר זיהית. +- קרא `/home/chaim/legal-ai/skills/decision/SKILL.md` (file tool) — מה כבר תועד. + +## 2. נתונים +- `mcp__legal-ai__case_get` עם case_number `1130-25` — מטא-דאטה. +- `mcp__legal-ai__case_get_final_text` עם case_number `1130-25` — קרא את הטקסט המלא של ההחלטה הסופית. + +**שים לב במיוחד**: זהה במדויק את **תוצאת ההחלטה** (קבלה / קבלה חלקית / דחייה) על סמך הטקסט עצמו, לא על סמך הנחות. + +## 3. ניתוח +חפש 3-5 דפוסים/פערים. לכל ממצא: מה ראיתי + מה זה אומר + הצעה ניסוחית מדויקת. + +## 4. כתוב comment הממצאים +```bash +curl -sS -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "Content-Type: application/json" \ + -d "$(jq -n --arg b "$BODY" '{body:$b}')" \ + "$PAPERCLIP_API_URL/issues/$PAPERCLIP_TASK_ID/comments" +``` + +פורמט ה-body: +- שורה ראשונה: `[ניסוי Sonnet 4.5 — ריצה חוזרת על CMP-78]` +- שורה שנייה: `**תוצאת ההחלטה הזו: <קבלה / קבלה חלקית / דחייה>** — ציין מפורשות +- אחר כך פסקה אחת מבוא קצרה +- אחר כך הממצאים ממוספרים + +## 5. פתח interaction מסוג ask_user_questions +זהה לפלואו של Sonnet באמת. אם תקבל "Agent run id required" — נסה כמה דרכים, ואם לא הולך, פרסם comment עם רשימת אופציות לבחירה. + +```bash +curl -sS -X POST -H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "Content-Type: application/json" \ + "$PAPERCLIP_API_URL/issues/$PAPERCLIP_TASK_ID/interactions" \ + -d '{ + "kind": "ask_user_questions", + "idempotencyKey": "curator-sonnet-rerun:'"$PAPERCLIP_TASK_ID"':select", + "title": "[Sonnet rerun] איזה ממצאים שווים עדכון?", + "continuationPolicy": "wake_assignee", + "payload": {"version": 1, "submitLabel": "אשר בחירה", + "questions": [{"id": "findings_to_propose", "prompt": "סמן ממצאים", "selectionMode": "multi", "options": []}]}}' +``` + +## 6. עדכן issue ל-status=in_review +```bash +curl -sS -X PATCH -H "Authorization: Bearer $PAPERCLIP_API_KEY" -H "Content-Type: application/json" \ + -d '{"status":"in_review"}' "$PAPERCLIP_API_URL/issues/$PAPERCLIP_TASK_ID" +``` + +# כללים +- אל תעדכן קבצים בעצמך. רק comment + interaction. +- אל תיצור issues חדשים. +- אל תעיר סוכנים אחרים. +EOF + +export HERMES_HOME="$PROFILE_HOME" +export PAPERCLIP_API_URL PAPERCLIP_API_KEY PAPERCLIP_TASK_ID PAPERCLIP_TASK_KEY \ + PAPERCLIP_TASK_TITLE PAPERCLIP_RUN_ID PAPERCLIP_WAKE_REASON + +echo "=== Sonnet 4.5 rerun (consistency check) — CMP-78 ===" +echo "HERMES_HOME=$HERMES_HOME" +echo "TASK_ID=$PAPERCLIP_TASK_ID" +echo "RUN_ID=$PAPERCLIP_RUN_ID" +echo "Started: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" +echo "---" + +START_EPOCH=$(date +%s) +hermes -z "$PROMPT" --yolo chat 2>&1 +END_EPOCH=$(date +%s) +DURATION=$((END_EPOCH - START_EPOCH)) +echo "" +echo "=== Run finished ===" +echo "Ended: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" +echo "Duration: ${DURATION}s ($((DURATION/60))m $((DURATION%60))s)" diff --git a/scripts/SCRIPTS.md b/scripts/SCRIPTS.md index 610696a..0526b3c 100644 --- a/scripts/SCRIPTS.md +++ b/scripts/SCRIPTS.md @@ -10,7 +10,7 @@ |--------|------|---------|-----------| | `pc.sh` | bash | **wrapper לכל קריאות Paperclip API מסוכנים** — מוסיף Authorization, X-Paperclip-Run-Id (audit trail), Content-Type, base URL. תחביר: `pc.sh [BODY_JSON]`. אסור `curl` ישיר ל-`$PAPERCLIP_API_URL`. ראה `HEARTBEAT.md §0`. counterpart ב-Python: `web/paperclip_api.py`. | נקרא ע"י סוכנים | | `sync_missing_agent_skills.py` | python | סקריפט "אל-כשל" להוספת `paperclipSkillSync` ל-`הגהת מסמכים` ו-`מנתח משפטי` שפיספסו את ה-sync ההיסטורי (Gap #28). תומך `--verify`/`--dry-run`/`--apply`. גיבוי אוטומטי ל-`agents-pre-skill-sync-*.sql`. דורש `PAPERCLIP_BOARD_API_KEY` (Infisical /paperclip ב-nautilus env). idempotent. | חד-פעמי (בוצע 2026-05-04). שמור לרפרנס | -| `sync_agents_across_companies.py` | python | **סנכרון סוכנים מ-CMP (1xxx, master) ל-CMPA (8xxx, mirror)** — Gap #25. משווה adapter_config (model/timeout/instructions/skills/etc), runtime_config (heartbeat), ושדות top-level (budget/metadata/icon/title/role). מסנן אוטומטית local skills שלא קיימים ב-mirror. לוגיקת subset (mirror יכול להחזיק יותר skills כי ה-API מוסיף required runtime skills). תומך `--verify`/`--dry-run`/`--apply [--only NAME]`. גיבוי אוטומטי. דורש `PAPERCLIP_BOARD_API_KEY`. **להריץ אחרי כל שינוי הגדרות ב-CMP.** | ידני אחרי כל שינוי | +| `sync_agents_across_companies.py` | python | **סנכרון סוכנים מ-CMP (1xxx, master) ל-CMPA (8xxx, mirror)** — Gap #25. משווה adapter_config (model/timeout/instructions/skills/etc), runtime_config (heartbeat), ושדות top-level (budget/metadata/icon/title/role). מסנן אוטומטית local skills שלא קיימים ב-mirror. לוגיקת subset (mirror יכול להחזיק יותר skills כי ה-API מוסיף required runtime skills). תומך `--verify`/`--dry-run`/`--apply [--only NAME]`. גיבוי אוטומטי. דורש `PAPERCLIP_BOARD_API_KEY`. **להריץ אחרי כל שינוי הגדרות ב-CMP.** **⚠ אם `adapter_type` שונה בין CMP ל-CMPA — הסקריפט מדלג על הסוכן עם warning. בעת מעבר adapter (למשל ל-`deepseek_local`) חובה לעדכן ידנית בשתי החברות לפני sync.** | ידני אחרי כל שינוי | | `fix_paperclipai_skills_drift.py` | python | סקריפט חד-פעמי (בוצע 2026-05-04) שניקה drift על `paperclipai/*` skills בין CMP ל-CMPA. הסיר `paperclip-dev` מכל 14 הסוכנים, ודאג ש-`paperclip-converting-plans-to-tasks` קיים רק על CEO ו-analyst. תומך `--apply` (ברירת מחדל: dry-run). דורש `PAPERCLIP_BOARD_API_KEY`. נשמר לרפרנס למקרה שhdrift חוזר. | חד-פעמי (בוצע) | | `auto-sync-cases.sh` | bash | סנכרון תיקי ערר ל-Gitea — רץ כל דקה | `* * * * *` (cron) | | `backup-db.sh` | bash | גיבוי PostgreSQL יומי ל-`data/backups/` (gzip) | לתזמן: `0 2 * * *` | @@ -54,6 +54,9 @@ | `seed-appeals.py` | seeding תיקי ערר ראשוניים ל-DB | MCP: `case_create()` | | `seed-knowledge.py` | seeding לקחים, ביטויי מעבר, פסיקה | MCP: `record_chair_feedback()`, `precedent_attach()` | | `validate-decision.py` | ולידציה מול block-schema | MCP: `validate_decision()` + `qa_validator.py` | +| `run_curator_deepseek_test.sh` | A/B test #1 (2026-05-05) — Hermes Curator על CMP-78 דרך DeepSeek V4-Pro ב-`provider:custom`, ללא interaction. תוצאה: 6:33 דק׳, 5 ממצאי סגנון/לקסיקון, פי 3 מהיר מ-Sonnet baseline (CMP-80) ופי ~20 זול. **הסקריפט נקודתי לתיק 1130-25 — לא להריץ שוב** | החלפת Curator לאדפטר DeepSeek מקומי (בתהליך) | +| `run_curator_deepseek_test_v2.sh` | A/B test #2 (2026-05-05) — אותו run אבל עם interaction. תוצאה: 9:08 דק׳, 5 ממצאים, היחיד מ-4 הריצות שזיהה תוצאה עובדתית נכונה (קבלה חלקית). interaction נכשל ב-API ("Agent run id required" בריצה ידנית). | החלפת Curator לאדפטר DeepSeek מקומי | +| `run_curator_sonnet_rerun.sh` | A/B test #3 (2026-05-05) — ריצה חוזרת של Sonnet 4.5 על אותו CMP-78. תוצאה: 12:52 דק׳ (לעומת 20:13 בריצה המקורית — כי בלי לולאת interaction.json). זיהה תוצאה שגויה ("דחייה") **בעקביות עם הריצה המקורית** — Sonnet עקבי-בטעות, DeepSeek אקראי. | בדיקה חד-פעמית — לא להריץ שוב | ## סקריפטים שנמחקו (git history בלבד)