Inline # comments in gitignore are not supported — they were silently breaking three patterns (data/checkpoints/, data/adapter-migration-state.json, .claude/agents/.generated/). Moved comments to their own lines and added missing entries for runtime dirs (data/audit/, data/logs/, etc.) and temp files (.interaction_tmp.json, .design-build/, .taskmaster bak files). Also tracks previously untracked legitimate files: scripts, tests, docs, skills references, .env.example, taskmaster templates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
413 lines
13 KiB
Markdown
413 lines
13 KiB
Markdown
# Scheduled Background Agents Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** הוסף 2 cron jobs לplugin: (1) תזכורת על תיקים תקועים 3+ ימים, (2) ניתוח פידבק יו"ר שבועי עם עדכון `decision-lessons.md` אוטומטי.
|
||
|
||
**Architecture:** שני jobs חדשים נרשמים ב-`ctx.jobs.register()`. Job 1 קורא `/api/cases/stale?days=3` (endpoint חדש ב-legal-ai) ומוסיף תגובה לissues תקועים. Job 2 קורא `/api/chair-feedback/weekly-summary` (endpoint חדש), שולח לCEO agent שמעדכן את `decision-lessons.md`.
|
||
|
||
**Tech Stack:** TypeScript (plugin-legal-ai jobs), Python/FastAPI (legal-ai web), asyncpg, Paperclip SDK `ctx.jobs.register()`, `ctx.agents.invoke()`.
|
||
|
||
---
|
||
|
||
## File Map
|
||
|
||
| Action | File |
|
||
|--------|------|
|
||
| Modify | `plugin-legal-ai/src/worker.ts` — register 2 new jobs |
|
||
| Modify | `plugin-legal-ai/plugin.json` — declare 2 new job entries |
|
||
| Modify | `plugin-legal-ai/src/manifest.ts` — add to jobs array |
|
||
| Modify | `legal-ai/web/app.py` — add `GET /api/cases/stale` + `GET /api/chair-feedback/weekly-summary` |
|
||
| Modify | `legal-ai/web/database.py` (or equivalent DB module) — add `get_stale_cases()` + `get_weekly_chair_feedback()` |
|
||
|
||
---
|
||
|
||
## Task 1: הוסף endpoint `GET /api/cases/stale`
|
||
|
||
**Files:**
|
||
- Modify: `legal-ai/web/app.py`
|
||
- Modify: `legal-ai/web/database.py` (שם DB queries מנוהלים — בדוק עם `grep -n "async def get_cases\|from.*database\|import.*db" /home/chaim/legal-ai/web/app.py | head -10`)
|
||
|
||
- [ ] **Step 1: מצא את module ה-DB**
|
||
|
||
```bash
|
||
grep -n "^from\|^import\|db\." /home/chaim/legal-ai/web/app.py | head -20
|
||
```
|
||
|
||
זהה את שם הmodule שמכיל את DB queries (בד"כ `database.py` או `db.py`).
|
||
|
||
- [ ] **Step 2: הוסף `get_stale_cases()` לmodule ה-DB**
|
||
|
||
```python
|
||
async def get_stale_cases(days: int = 3) -> list[dict]:
|
||
"""Return cases whose status is not 'final' and haven't been updated in `days` days."""
|
||
async with get_db_connection() as conn:
|
||
rows = await conn.fetch(
|
||
"""
|
||
SELECT case_number, title, status, company_id,
|
||
updated_at,
|
||
now() - updated_at AS age
|
||
FROM cases
|
||
WHERE status NOT IN ('final', 'new')
|
||
AND updated_at < now() - ($1 || ' days')::interval
|
||
ORDER BY updated_at ASC
|
||
""",
|
||
str(days),
|
||
)
|
||
return [dict(r) for r in rows]
|
||
```
|
||
|
||
> `get_db_connection()` — השתמש בדפוס הקיים בקובץ. אם זה `asyncpg.connect()` ישיר, `asyncpg.create_pool()`, או context manager — העתק את הדפוס.
|
||
|
||
- [ ] **Step 3: הוסף endpoint ב-`app.py`**
|
||
|
||
```python
|
||
@app.get("/api/cases/stale")
|
||
async def list_stale_cases(days: int = 3):
|
||
"""Cases stuck in non-final status for more than `days` days."""
|
||
cases = await db.get_stale_cases(days=days)
|
||
return {
|
||
"cases": [
|
||
{
|
||
"case_number": c["case_number"],
|
||
"title": c["title"],
|
||
"status": c["status"],
|
||
"company_id": str(c["company_id"]),
|
||
"days_stale": c["age"].days,
|
||
}
|
||
for c in cases
|
||
],
|
||
"total": len(cases),
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: בדיקה**
|
||
|
||
```bash
|
||
curl -s "https://legal-ai.nautilus.marcusgroup.org/api/cases/stale?days=1" | jq .total
|
||
```
|
||
|
||
Expected: JSON עם רשימת תיקים.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /home/chaim/legal-ai
|
||
git add web/app.py web/database.py # או השם הנכון
|
||
git commit -m "feat: add GET /api/cases/stale endpoint"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: הוסף endpoint `GET /api/chair-feedback/weekly-summary`
|
||
|
||
**Files:**
|
||
- Modify: `legal-ai/web/app.py`
|
||
- Modify: DB module
|
||
|
||
- [ ] **Step 1: בדוק את מבנה טבלת `chair_feedback`**
|
||
|
||
```bash
|
||
sqlite3 /home/chaim/.paperclip/instances/default/data/app.db \
|
||
".schema chair_feedback" 2>/dev/null || \
|
||
psql -h localhost -p 5433 -U legal_ai -c "\d chair_feedback" legal_ai 2>/dev/null || \
|
||
grep -rn "chair_feedback" /home/chaim/legal-ai/mcp-server/src/ | head -10
|
||
```
|
||
|
||
- [ ] **Step 2: הוסף `get_weekly_chair_feedback()` לDB module**
|
||
|
||
```python
|
||
async def get_weekly_chair_feedback(days: int = 7) -> list[dict]:
|
||
"""Return chair feedback entries from the last `days` days."""
|
||
async with get_db_connection() as conn:
|
||
rows = await conn.fetch(
|
||
"""
|
||
SELECT cf.case_number, cf.feedback_text, cf.created_at,
|
||
cf.feedback_type, c.title
|
||
FROM chair_feedback cf
|
||
JOIN cases c ON c.case_number = cf.case_number
|
||
WHERE cf.created_at > now() - ($1 || ' days')::interval
|
||
ORDER BY cf.created_at DESC
|
||
""",
|
||
str(days),
|
||
)
|
||
return [dict(r) for r in rows]
|
||
```
|
||
|
||
> אם שמות השדות שונים (בדוק ב-Step 1) — התאם.
|
||
|
||
- [ ] **Step 3: הוסף endpoint**
|
||
|
||
```python
|
||
@app.get("/api/chair-feedback/weekly-summary")
|
||
async def get_chair_feedback_weekly(days: int = 7):
|
||
"""Feedback entries from the past week, formatted for the learning agent."""
|
||
entries = await db.get_weekly_chair_feedback(days=days)
|
||
if not entries:
|
||
return {"summary": "", "entry_count": 0}
|
||
|
||
lines = [
|
||
f"- תיק {e['case_number']} ({e['title']}): {e['feedback_text']}"
|
||
for e in entries
|
||
]
|
||
summary = "\n".join(lines)
|
||
return {"summary": summary, "entry_count": len(entries), "entries": entries}
|
||
```
|
||
|
||
- [ ] **Step 4: בדיקה**
|
||
|
||
```bash
|
||
curl -s "https://legal-ai.nautilus.marcusgroup.org/api/chair-feedback/weekly-summary" | jq .entry_count
|
||
```
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
cd /home/chaim/legal-ai
|
||
git add web/app.py web/database.py
|
||
git commit -m "feat: add GET /api/chair-feedback/weekly-summary endpoint"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: הוסף jobs לplugin
|
||
|
||
**Files:**
|
||
- Modify: `plugin-legal-ai/plugin.json`
|
||
- Modify: `plugin-legal-ai/src/manifest.ts`
|
||
|
||
- [ ] **Step 1: קרא את ה-`jobs` הקיים ב-`plugin.json`**
|
||
|
||
```bash
|
||
cat /home/chaim/plugin-legal-ai/plugin.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d['jobs'], indent=2))"
|
||
```
|
||
|
||
- [ ] **Step 2: הוסף 2 jobs חדשים לarray `"jobs"` ב-`plugin.json`**
|
||
|
||
```json
|
||
{
|
||
"jobKey": "stale-case-reminder",
|
||
"displayName": "תזכורת תיקים תקועים",
|
||
"description": "מזהה תיקים שלא עודכנו 3+ ימים ומוסיף תגובה לissue",
|
||
"schedule": "0 8 * * *"
|
||
},
|
||
{
|
||
"jobKey": "weekly-feedback-analysis",
|
||
"displayName": "ניתוח פידבק שבועי",
|
||
"description": "מסכם פידבק יו\"ר מהשבוע האחרון ומעדכן את decision-lessons.md",
|
||
"schedule": "0 19 * * 0"
|
||
}
|
||
```
|
||
|
||
> `"0 8 * * *"` = כל יום בשעה 08:00. `"0 19 * * 0"` = כל ראשון ב-19:00.
|
||
|
||
- [ ] **Step 3: עדכן `manifest.ts`**
|
||
|
||
```bash
|
||
grep -n "jobs\|jobKey\|schedule" /home/chaim/plugin-legal-ai/src/manifest.ts
|
||
```
|
||
|
||
הוסף את אותם 2 objects לarray `jobs` ב-`manifest.ts`.
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
cd /home/chaim/plugin-legal-ai
|
||
git add plugin.json src/manifest.ts
|
||
git commit -m "feat: declare stale-case-reminder and weekly-feedback-analysis jobs"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Implement job handlers ב-`worker.ts`
|
||
|
||
**Files:**
|
||
- Modify: `plugin-legal-ai/src/worker.ts`
|
||
|
||
- [ ] **Step 1: קרא את handler של `sync-case-status` הקיים**
|
||
|
||
```bash
|
||
grep -n "sync-case-status\|jobs.register\|jobKey" /home/chaim/plugin-legal-ai/src/worker.ts
|
||
```
|
||
|
||
העתק את הדפוס.
|
||
|
||
- [ ] **Step 2: הוסף את `stale-case-reminder` handler**
|
||
|
||
בתוך `setup(ctx)`, אחרי רישום ה-job הקיים:
|
||
|
||
```typescript
|
||
ctx.jobs.register("stale-case-reminder", async (job) => {
|
||
ctx.logger.info("stale-case-reminder: starting");
|
||
|
||
const config = await ctx.config.get();
|
||
const apiBase = (config.legalApiBaseUrl as string) ?? "http://localhost:8085";
|
||
|
||
const resp = await ctx.http.fetch(`${apiBase}/api/cases/stale?days=3`);
|
||
if (!resp.ok) {
|
||
ctx.logger.error(`stale-case-reminder: API error ${resp.status}`);
|
||
return;
|
||
}
|
||
|
||
const data = (await resp.json()) as {
|
||
cases: Array<{
|
||
case_number: string;
|
||
title: string;
|
||
status: string;
|
||
company_id: string;
|
||
days_stale: number;
|
||
}>;
|
||
};
|
||
|
||
for (const staleCase of data.cases) {
|
||
const issueId = await ctx.state.get(
|
||
{ companyId: staleCase.company_id },
|
||
`case:${staleCase.case_number}`
|
||
);
|
||
if (!issueId) continue;
|
||
|
||
await ctx.issues.createComment({
|
||
issueId: issueId as string,
|
||
body: `⚠️ **תיק תקוע** — ${staleCase.days_stale} ימים ללא עדכון (סטטוס: ${staleCase.status}). האם יש צורך בפעולה?`,
|
||
});
|
||
|
||
ctx.logger.info(
|
||
`stale-case-reminder: posted reminder for ${staleCase.case_number} (${staleCase.days_stale}d stale)`
|
||
);
|
||
}
|
||
|
||
ctx.logger.info(`stale-case-reminder: done. ${data.cases.length} cases reminded`);
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 3: הוסף את `weekly-feedback-analysis` handler**
|
||
|
||
```typescript
|
||
ctx.jobs.register("weekly-feedback-analysis", async (job) => {
|
||
ctx.logger.info("weekly-feedback-analysis: starting");
|
||
|
||
const config = await ctx.config.get();
|
||
const apiBase = (config.legalApiBaseUrl as string) ?? "http://localhost:8085";
|
||
|
||
const resp = await ctx.http.fetch(`${apiBase}/api/chair-feedback/weekly-summary`);
|
||
if (!resp.ok) {
|
||
ctx.logger.error(`weekly-feedback-analysis: API error ${resp.status}`);
|
||
return;
|
||
}
|
||
|
||
const data = (await resp.json()) as {
|
||
summary: string;
|
||
entry_count: number;
|
||
};
|
||
|
||
if (data.entry_count === 0) {
|
||
ctx.logger.info("weekly-feedback-analysis: no feedback this week, skipping");
|
||
return;
|
||
}
|
||
|
||
// Invoke the CEO agent to process feedback and update decision-lessons.md
|
||
const companies = await ctx.companies.list();
|
||
for (const company of companies) {
|
||
// CEO IDs per company
|
||
const CEO_IDS: Record<string, string> = {
|
||
"42a7acd0-30c5-4cbd-ac97-7424f65df294": "752cebdd-6748-4a04-aacd-c7ab0294ef33",
|
||
"8639e837-4c9d-47fa-a76b-95788d651896": "cdbfa8bc-3d61-41a4-a2e7-677ec7d34562",
|
||
};
|
||
const ceoId = CEO_IDS[company.id];
|
||
if (!ceoId) continue;
|
||
|
||
await ctx.agents.invoke(ceoId, company.id, {
|
||
prompt: `ניתוח פידבק שבועי יו"ר (${data.entry_count} פריטים):
|
||
|
||
${data.summary}
|
||
|
||
המשימה: עדכן את /home/chaim/legal-ai/docs/legal-decision-lessons.md עם הלקחים החדשים שעולים מהפידבק. הוסף רק לקחים חדשים שלא קיימים כבר. קבץ לפי נושא.`,
|
||
reason: "weekly-feedback-analysis scheduled job",
|
||
});
|
||
|
||
ctx.logger.info(
|
||
`weekly-feedback-analysis: invoked CEO for company ${company.id} (${data.entry_count} feedback entries)`
|
||
);
|
||
break; // One CEO is enough — lessons file is shared
|
||
}
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 4: TypeScript check**
|
||
|
||
```bash
|
||
cd /home/chaim/plugin-legal-ai && npx tsc --noEmit
|
||
```
|
||
|
||
Expected: 0 errors.
|
||
|
||
- [ ] **Step 5: Build**
|
||
|
||
```bash
|
||
cd /home/chaim/plugin-legal-ai && npm run build
|
||
```
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
cd /home/chaim/plugin-legal-ai
|
||
git add src/worker.ts
|
||
git commit -m "feat: implement stale-case-reminder and weekly-feedback-analysis jobs"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: Re-install plugin + בדיקה
|
||
|
||
- [ ] **Step 1: Deploy legal-ai**
|
||
|
||
```bash
|
||
cd /home/chaim/legal-ai && git push origin main
|
||
# המתן ~3 דקות
|
||
curl -s https://legal-ai.nautilus.marcusgroup.org/api/health | jq .status
|
||
```
|
||
|
||
- [ ] **Step 2: Re-install plugin**
|
||
|
||
```bash
|
||
cd /home/chaim/plugin-legal-ai && npm run build
|
||
npx paperclipai plugin uninstall marcusgroup.legal-ai \
|
||
--api-base http://localhost:3100 --api-key pcapi_legal_install_key_2026
|
||
npx paperclipai plugin install /home/chaim/plugin-legal-ai \
|
||
--api-base http://localhost:3100 --api-key pcapi_legal_install_key_2026
|
||
pm2 restart paperclip
|
||
```
|
||
|
||
- [ ] **Step 3: בדוק שה-jobs רשומים**
|
||
|
||
```bash
|
||
curl -s -H "Authorization: Bearer pcapi_legal_install_key_2026" \
|
||
http://localhost:3100/api/plugins/marcusgroup.legal-ai/jobs | jq .[].jobKey
|
||
```
|
||
|
||
Expected: `"sync-case-status"`, `"stale-case-reminder"`, `"weekly-feedback-analysis"`.
|
||
|
||
- [ ] **Step 4: הפעל job ידנית לבדיקה**
|
||
|
||
```bash
|
||
curl -s -X POST -H "Authorization: Bearer pcapi_legal_install_key_2026" \
|
||
http://localhost:3100/api/plugins/marcusgroup.legal-ai/jobs/stale-case-reminder/run | jq .
|
||
```
|
||
|
||
Expected: Job הופעל. בדוק logs:
|
||
```bash
|
||
pm2 logs paperclip --lines 30 | grep "stale-case-reminder"
|
||
```
|
||
|
||
---
|
||
|
||
## אימות סופי
|
||
|
||
| בדיקה | פקודה | תוצאה מצופה |
|
||
|-------|-------|-------------|
|
||
| API stale endpoint | `curl .../api/cases/stale?days=1` | JSON עם cases |
|
||
| API feedback endpoint | `curl .../api/chair-feedback/weekly-summary` | JSON עם summary |
|
||
| Jobs רשומים | `GET .../api/plugins/.../jobs` | 3 jobs רשומים |
|
||
| Stale reminder ידני | `POST .../jobs/stale-case-reminder/run` | תגובות בissues |
|
||
| Feedback analysis ידני | `POST .../jobs/weekly-feedback-analysis/run` | CEO מועיר |
|