feat(curator): switch Hermes Curator to DeepSeek V4-Pro via deepseek_local adapter #3
@@ -1,13 +1,18 @@
|
|||||||
---
|
---
|
||||||
name: hermes-curator
|
name: hermes-curator
|
||||||
description: Knowledge Curator (Hermes) — מנתח החלטות סופיות אחרי export, מציע עדכונים ל-skills/lessons. read-only על תוכן, write רק על comments.
|
description: Knowledge Curator (Hermes) — מנתח החלטות סופיות אחרי export, מציע עדכונים ל-skills/lessons. read-only על תוכן, write רק על comments.
|
||||||
adapter: hermes_local
|
adapter: deepseek_local
|
||||||
model: anthropic/claude-sonnet-4-5
|
model: deepseek-v4-pro
|
||||||
profiles:
|
profiles:
|
||||||
CMP: curator-cmp # רישוי ובניה (תיקים 1xxx)
|
CMP: curator-cmp # רישוי ובניה (תיקים 1xxx)
|
||||||
CMPA: curator-cmpa # היטל השבחה + פיצויים (תיקים 8xxx, 9xxx)
|
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
|
# מנהל ידע — Hermes Knowledge Curator
|
||||||
|
|
||||||
## רקע
|
## רקע
|
||||||
@@ -54,10 +59,11 @@ profiles:
|
|||||||
|
|
||||||
1. קורא את ה-issue body שב-`{{taskBody}}` — שם התיק + ID של ההחלטה הסופית
|
1. קורא את ה-issue body שב-`{{taskBody}}` — שם התיק + ID של ההחלטה הסופית
|
||||||
2. משתמש ב-MCP tools של legal-ai:
|
2. משתמש ב-MCP tools של legal-ai:
|
||||||
- `mcp__legal-ai__case_get` — קבלת פרטי תיק
|
- `mcp__legal-ai__case_get` — קבלת פרטי תיק (כולל `expected_outcome` — **הסמכות העובדתית** לתוצאה)
|
||||||
- `mcp__legal-ai__document_list` — רשימת מסמכים, איתור ההחלטה הסופית
|
- `mcp__legal-ai__case_get_final_text` — הטקסט המלא של ההחלטה הסופית
|
||||||
- `mcp__legal-ai__search_decisions` — השוואה לחלטות קודמות
|
- `mcp__legal-ai__document_list` — רק אם נדרש רשימת מסמכים נוספים של התיק
|
||||||
- `mcp__legal-ai__get_style_guide` — דפוסי הסגנון של דפנה
|
- `mcp__legal-ai__get_style_guide` — דפוסי הסגנון של דפנה
|
||||||
|
- **לא** להשתמש ב-`search_decisions` — השוואה ל-`SKILL.md` ו-`corpus-analysis.md` מספיקה ולא יקרה
|
||||||
3. קורא קבצים מקומיים (read-only):
|
3. קורא קבצים מקומיים (read-only):
|
||||||
- `/home/chaim/legal-ai/skills/decision/SKILL.md`
|
- `/home/chaim/legal-ai/skills/decision/SKILL.md`
|
||||||
- `/home/chaim/legal-ai/docs/legal-decision-lessons.md`
|
- `/home/chaim/legal-ai/docs/legal-decision-lessons.md`
|
||||||
@@ -74,13 +80,30 @@ profiles:
|
|||||||
|
|
||||||
## פורמט ה-comment
|
## פורמט ה-comment
|
||||||
|
|
||||||
עברית, ניטרלי. 3-5 ממצאים מובחנים. כל ממצא:
|
עברית, ניטרלי. 3-5 ממצאים מובחנים. **כל ממצא חייב להיות מתויג** באחד מ-4 הסוגים:
|
||||||
|
|
||||||
|
```
|
||||||
|
[סגנון] — מילים, ביטויי מעבר, פתיחות, סיומים
|
||||||
|
[מבנה] — סדר בלוקים, יחסי אורך, מספור
|
||||||
|
[לקסיקון משפטי] — מינוח טכני (מגישי תכנית, ריפוי פגם, וכו')
|
||||||
|
[טבלאי] — דפוסים שמופיעים פעמיים+ ב-corpus
|
||||||
|
```
|
||||||
|
|
||||||
|
לכל ממצא:
|
||||||
- **מה ראיתי** — תיאור קצר של הדפוס/הפער
|
- **מה ראיתי** — תיאור קצר של הדפוס/הפער
|
||||||
- **מה זה אומר** — למה זה חשוב
|
- **מה זה אומר** — למה זה חשוב
|
||||||
- **הצעה** — איך אפשר להוסיף ל-style guide / lessons (טקסט מוצע מילולי)
|
- **הצעה** — איך אפשר להוסיף ל-style guide / lessons (טקסט מוצע מילולי)
|
||||||
|
|
||||||
אם אין ממצאים חדשים → לציין במפורש בלי להמציא.
|
אם אין ממצאים חדשים → לציין במפורש בלי להמציא.
|
||||||
|
|
||||||
|
## מה **לא** להגיד ב-comment
|
||||||
|
|
||||||
|
- **אל תכלול שורת מטא** בראש ה-comment עם "תוצאה: X" או "אורך: ~Y תווים".
|
||||||
|
אתה לא בודק את התיק — אתה בודק את הסגנון. תוצאה מוטעית בראש ה-comment פוגעת באמינות.
|
||||||
|
- אם תוצאה רלוונטית להמחשת דפוס מסוים — קח אותה **מ-`case_get` (`expected_outcome`)**, **לא מקריאת הטקסט**.
|
||||||
|
אם השדה ריק או חסר ב-DB — סמן `[תוצאה: לא מאומתת]` או דלג עליה.
|
||||||
|
- **אל תפרש משפטית** את ההחלטה. דפנה כבר הכריעה. תפקידך זיהוי דפוסים בלבד.
|
||||||
|
|
||||||
## מה אני לא עושה
|
## מה אני לא עושה
|
||||||
|
|
||||||
- **לא מעדכן** קבצים בעצמי (skills/, lessons.py, DB) — רק מציע
|
- **לא מעדכן** קבצים בעצמי (skills/, lessons.py, DB) — רק מציע
|
||||||
@@ -93,6 +116,27 @@ profiles:
|
|||||||
אם MCP server לא נגיש או החלטה לא נמצאת, כתוב comment קצר עם הסיבה
|
אם MCP server לא נגיש או החלטה לא נמצאת, כתוב comment קצר עם הסיבה
|
||||||
ו-status=failed. אל תזייף ממצאים.
|
ו-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
|
||||||
|
```
|
||||||
|
|
||||||
## קונטקסט קבוע (לא לשכוח)
|
## קונטקסט קבוע (לא לשכוח)
|
||||||
|
|
||||||
- היו"ר: עו"ד דפנה תמיר
|
- היו"ר: עו"ד דפנה תמיר
|
||||||
|
|||||||
11
CLAUDE.md
11
CLAUDE.md
@@ -118,6 +118,8 @@
|
|||||||
├── web-ui/ ← Next.js frontend (TypeScript/React): ממשק המשתמש
|
├── web-ui/ ← Next.js frontend (TypeScript/React): ממשק המשתמש
|
||||||
│ └── next.config.ts ← proxy: /api/* → FastAPI :8000
|
│ └── next.config.ts ← proxy: /api/* → FastAPI :8000
|
||||||
├── mcp-server/ ← MCP server + services + tools
|
├── mcp-server/ ← MCP server + services + tools
|
||||||
|
├── adapters/ ← Paperclip external adapters (ראה למטה)
|
||||||
|
│ └── deepseek-paperclip-adapter/ ← `deepseek_local` (Hermes-pinned ל-DeepSeek profile)
|
||||||
└── scripts/ ← סקריפטים וכלי עזר (ראה scripts/SCRIPTS.md)
|
└── scripts/ ← סקריפטים וכלי עזר (ראה scripts/SCRIPTS.md)
|
||||||
└── .archive/ ← סקריפטים שהושלמו (לא להריץ)
|
└── .archive/ ← סקריפטים שהושלמו (לא להריץ)
|
||||||
```
|
```
|
||||||
@@ -180,6 +182,15 @@
|
|||||||
- הסקריפט מסנן local skills שלא קיימים ב-CMPA (מציג אזהרה), משתמש ב-API (לא DB ישיר), יוצר revisions, idempotent.
|
- הסקריפט מסנן local skills שלא קיימים ב-CMPA (מציג אזהרה), משתמש ב-API (לא DB ישיר), יוצר revisions, idempotent.
|
||||||
- שאלות ה-skill הרשמי של Paperclip — `paperclip` skill תחת `paperclipai/paperclip`.
|
- שאלות ה-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) כתבנית.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## עקרונות כתיבה קריטיים
|
## עקרונות כתיבה קריטיים
|
||||||
|
|||||||
99
adapters/deepseek-paperclip-adapter/dist/index.js
vendored
Normal file
99
adapters/deepseek-paperclip-adapter/dist/index.js
vendored
Normal file
@@ -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;
|
||||||
352
adapters/deepseek-paperclip-adapter/dist/server/execute.js
vendored
Normal file
352
adapters/deepseek-paperclip-adapter/dist/server/execute.js
vendored
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
/**
|
||||||
|
* Server-side execution for the DeepSeek-via-Hermes adapter.
|
||||||
|
*
|
||||||
|
* Spawns `hermes chat -q "..." -Q -m <model> --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 <HERMES_HOME>/config.yaml + <HERMES_HOME>/.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: <your summary here>"}'\`
|
||||||
|
{{/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;
|
||||||
|
}
|
||||||
29
adapters/deepseek-paperclip-adapter/dist/server/session-codec.js
vendored
Normal file
29
adapters/deepseek-paperclip-adapter/dist/server/session-codec.js
vendored
Normal file
@@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
171
adapters/deepseek-paperclip-adapter/dist/server/skills.js
vendored
Normal file
171
adapters/deepseek-paperclip-adapter/dist/server/skills.js
vendored
Normal file
@@ -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);
|
||||||
|
}
|
||||||
164
adapters/deepseek-paperclip-adapter/dist/server/test.js
vendored
Normal file
164
adapters/deepseek-paperclip-adapter/dist/server/test.js
vendored
Normal file
@@ -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 <HERMES_HOME>/.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(),
|
||||||
|
};
|
||||||
|
}
|
||||||
36
adapters/deepseek-paperclip-adapter/dist/shared/constants.js
vendored
Normal file
36
adapters/deepseek-paperclip-adapter/dist/shared/constants.js
vendored
Normal file
@@ -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;
|
||||||
25
adapters/deepseek-paperclip-adapter/package-lock.json
generated
Normal file
25
adapters/deepseek-paperclip-adapter/package-lock.json
generated
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
adapters/deepseek-paperclip-adapter/package.json
Normal file
21
adapters/deepseek-paperclip-adapter/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -400,6 +400,54 @@
|
|||||||
- **~30 תקדמים חיצוניים** ש**דפנה מצטטת באופן עקבי** (ראה precedent-network.md)
|
- **~30 תקדמים חיצוניים** ש**דפנה מצטטת באופן עקבי** (ראה precedent-network.md)
|
||||||
- **~15 תקדמים אישיים** שלה עצמה — מהווים את הקאנון האישי שלה
|
- **~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. מה עדיין לא ראינו
|
## 7. מה עדיין לא ראינו
|
||||||
|
|||||||
@@ -385,3 +385,64 @@ The draft's biggest structural error was adding the "נבאר" doctrinal paragra
|
|||||||
- [ ] Update voice-fingerprint: add new transition phrases
|
- [ ] Update voice-fingerprint: add new transition phrases
|
||||||
- [ ] Update architecture-by-outcome: add "clean acceptance" archetype
|
- [ ] Update architecture-by-outcome: add "clean acceptance" archetype
|
||||||
- [ ] Fix agent opening punctuation: "ונפרט;" not "נפרט."
|
- [ ] 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
|
||||||
|
|||||||
87
scripts/.archive/run_curator_deepseek_test.sh
Executable file
87
scripts/.archive/run_curator_deepseek_test.sh
Executable file
@@ -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
|
||||||
116
scripts/.archive/run_curator_deepseek_test_v2.sh
Executable file
116
scripts/.archive/run_curator_deepseek_test_v2.sh
Executable file
@@ -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)"
|
||||||
106
scripts/.archive/run_curator_sonnet_rerun.sh
Executable file
106
scripts/.archive/run_curator_sonnet_rerun.sh
Executable file
@@ -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)"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
|--------|------|---------|-----------|
|
|--------|------|---------|-----------|
|
||||||
| `pc.sh` | bash | **wrapper לכל קריאות Paperclip API מסוכנים** — מוסיף Authorization, X-Paperclip-Run-Id (audit trail), Content-Type, base URL. תחביר: `pc.sh <METHOD> <PATH> [BODY_JSON]`. אסור `curl` ישיר ל-`$PAPERCLIP_API_URL`. ראה `HEARTBEAT.md §0`. counterpart ב-Python: `web/paperclip_api.py`. | נקרא ע"י סוכנים |
|
| `pc.sh` | bash | **wrapper לכל קריאות Paperclip API מסוכנים** — מוסיף Authorization, X-Paperclip-Run-Id (audit trail), Content-Type, base URL. תחביר: `pc.sh <METHOD> <PATH> [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_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 חוזר. | חד-פעמי (בוצע) |
|
| `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) |
|
| `auto-sync-cases.sh` | bash | סנכרון תיקי ערר ל-Gitea — רץ כל דקה | `* * * * *` (cron) |
|
||||||
| `backup-db.sh` | bash | גיבוי PostgreSQL יומי ל-`data/backups/` (gzip) | לתזמן: `0 2 * * *` |
|
| `backup-db.sh` | bash | גיבוי PostgreSQL יומי ל-`data/backups/` (gzip) | לתזמן: `0 2 * * *` |
|
||||||
@@ -54,6 +54,9 @@
|
|||||||
| `seed-appeals.py` | seeding תיקי ערר ראשוניים ל-DB | MCP: `case_create()` |
|
| `seed-appeals.py` | seeding תיקי ערר ראשוניים ל-DB | MCP: `case_create()` |
|
||||||
| `seed-knowledge.py` | seeding לקחים, ביטויי מעבר, פסיקה | MCP: `record_chair_feedback()`, `precedent_attach()` |
|
| `seed-knowledge.py` | seeding לקחים, ביטויי מעבר, פסיקה | MCP: `record_chair_feedback()`, `precedent_attach()` |
|
||||||
| `validate-decision.py` | ולידציה מול block-schema | MCP: `validate_decision()` + `qa_validator.py` |
|
| `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 בלבד)
|
## סקריפטים שנמחקו (git history בלבד)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user