pantheon-opencode 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/pantheon-audit.md +25 -0
- package/commands/pantheon-consolidate.md +11 -0
- package/commands/pantheon-deepwork.md +79 -0
- package/commands/pantheon-optimize.md +79 -0
- package/commands/pantheon.md +49 -0
- package/opencode.json +1 -34
- package/package.json +4 -4
- package/scripts/mcp_persistence_server.py +331 -1
- package/src/agents/aphrodite.md +7 -0
- package/src/agents/demeter.md +7 -0
- package/src/agents/hephaestus.md +7 -0
- package/src/agents/hermes.md +7 -0
- package/src/agents/zeus.md +31 -18
- package/src/instructions/agent-return-format.instructions.md +1 -0
- package/src/instructions/zeus-anti-stall.instructions.md +43 -5
- package/src/instructions/zeus-timeout-retry.instructions.md +47 -10
- package/src/mcp/mcp_persistence_server.py +331 -0
- package/src/routing.yml +31 -0
- package/commands/pantheon-reflect.md +0 -16
- package/commands/pantheon-verify.md +0 -17
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Audit: heuristic scan (zero LLM) + Themis review. Usage: /pantheon-audit [--light|--full|--plan]"
|
|
3
|
+
agent: "zeus"
|
|
4
|
+
---
|
|
5
|
+
# /pantheon-audit — Quality & Security Audit
|
|
6
|
+
|
|
7
|
+
**What:** Layer 1 heuristic scan (zero tokens, <2s). If BLOCKING, returns score. If APPROVED, Themis deep review.
|
|
8
|
+
|
|
9
|
+
**Usage:**
|
|
10
|
+
- `/pantheon-audit` — Full audit (Layer 1 → 2 → 3)
|
|
11
|
+
- `/pantheon-audit --light` — Layer 1 only (quick scan)
|
|
12
|
+
- `/pantheon-audit --full` — Force full review
|
|
13
|
+
- `/pantheon-audit --plan` — Verification planning only
|
|
14
|
+
|
|
15
|
+
## Layers
|
|
16
|
+
|
|
17
|
+
### Layer 1 — Heuristic Scan
|
|
18
|
+
`python3 scripts/themis_heuristic_scan.py [--path=<dir>]`
|
|
19
|
+
Zero tokens, <2s. Checks: ruff, biome, anti-pattern slop, anti-overengineering (YAGNI), hash-anchored edits. Returns score 0-100 + APPROVED/BLOCKING.
|
|
20
|
+
|
|
21
|
+
### Layer 2 — Themis Deep Review
|
|
22
|
+
~500 tokens. Confidence scoring, regression prediction, OWASP Top 10.
|
|
23
|
+
|
|
24
|
+
### Layer 3 — Verification Planning
|
|
25
|
+
For N>5 file changes. Sugere plano de verificação antes de implementar.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Merge and consolidate memory entries"
|
|
3
|
+
agent: zeus
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Merge related memory entries and consolidate duplicates:
|
|
7
|
+
|
|
8
|
+
1. Search for related memories via memory_search
|
|
9
|
+
2. Merge overlapping entries
|
|
10
|
+
3. Remove duplicates via memory_forget
|
|
11
|
+
4. Store consolidated entry via memory_store
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Start a heavy multi-phase task with persisted checkpoints, phased specialist dispatch, and Themis review gates. Progress saved to .pantheon/deepwork/ for resumability."
|
|
3
|
+
agent: "zeus"
|
|
4
|
+
---
|
|
5
|
+
# /pantheon-deepwork — Heavy Task Workflow
|
|
6
|
+
|
|
7
|
+
Structured, checkpointed workflow for complex multi-step tasks spanning multiple agents and sessions. Progress persisted to `.pantheon/deepwork/<task-slug>/`. Each phase gated by Themis.
|
|
8
|
+
|
|
9
|
+
| Use When | Don't Use When |
|
|
10
|
+
|---|---|
|
|
11
|
+
| 10+ turns or multi-session | Simple fixes (→ @talos) |
|
|
12
|
+
| Multi-agent dependencies | Single-file changes (→ /subtask) |
|
|
13
|
+
| Costly context loss | < 5 turns (→ normal delegation) |
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Phase 0: SCOPING
|
|
19
|
+
└─ Zeus + Athena define scope, phases, acceptance criteria → PLAN.md
|
|
20
|
+
|
|
21
|
+
Phase 1: DISCOVERY
|
|
22
|
+
└─ @apollo maps relevant codebase areas → DISCOVERY.md
|
|
23
|
+
|
|
24
|
+
Phase 2-N: IMPLEMENTATION (parallel per phase)
|
|
25
|
+
└─ @hermes, @aphrodite, @demeter, @hephaestus, @prometheus
|
|
26
|
+
└─ Output: IMPL-phase-<N>-<agent>.md
|
|
27
|
+
|
|
28
|
+
GATE after each phase:
|
|
29
|
+
└─ @themis reviews output → FAIL (fix + resubmit) or PASS (continue)
|
|
30
|
+
|
|
31
|
+
Phase FINAL: VERIFICATION
|
|
32
|
+
└─ @themis full integration review → REVIEW.md
|
|
33
|
+
└─ All tests pass, coverage >80%
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Checkpoints
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Usage: --resume <slug> | --status <slug> | --list
|
|
40
|
+
|
|
41
|
+
.pantheon/deepwork/<slug>/
|
|
42
|
+
├── PLAN.md DISCOVERY.md IMPL-phase-*-*.md phase-*-review.md REVIEW.md STATUS.md
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Full-Auto Mode (`--full-auto`)
|
|
46
|
+
|
|
47
|
+
> ⚠️ Bypasses human review gates. Tier 1 gates (plan, commit, deploy) still require approval.
|
|
48
|
+
|
|
49
|
+
**Themis auto-approves non-blocking reviews** — only stops on **BLOCKING** verdict. No waiting between phases. Checkpoints saved at every boundary.
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
/pantheon-deepwork --full-auto "Refactor auth service to use JWT"
|
|
53
|
+
/pantheon-deepwork --full-auto --resume auth-jwt
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Anti-Stall
|
|
57
|
+
|
|
58
|
+
- **Stall Detection** — 3 turns no progress → escalate
|
|
59
|
+
- **Phase Reminder** — continue only independent work after dispatch
|
|
60
|
+
- **Progress Checkpoint** — every 5 turns, summarize completed vs remaining
|
|
61
|
+
- **Delegate Retry** — failures retried once with rephrased prompt
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
/pantheon-deepwork "Add user authentication with OAuth2" # Start new session
|
|
67
|
+
/pantheon-deepwork --resume auth-oauth2 # Resume interrupted
|
|
68
|
+
/pantheon-deepwork --status auth-oauth2 # Show progress
|
|
69
|
+
/pantheon-deepwork --list /pantheon-deepwork --archive <slug> # List / archive
|
|
70
|
+
/pantheon-deepwork --full-auto "Refactor auth" # Themis auto-approve
|
|
71
|
+
/pantheon-deepwork --full-auto --resume auth-refactor # Resume full-auto
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Safety
|
|
75
|
+
|
|
76
|
+
- All progress persisted — work is never lost
|
|
77
|
+
- Each phase gated by Themis — quality enforced at every step
|
|
78
|
+
- Explicit resume required — no auto-continue without intent (unless `--full-auto`)
|
|
79
|
+
- `.pantheon/deepwork/` is gitignored — no accidental commits
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Scan, clean, cache, and archive Pantheon project files. Scans for bloat, archives completed deepwork, migrates flat files to MCP memory, and reports optimization opportunities. Usage: /pantheon-optimize"
|
|
3
|
+
agent: "zeus"
|
|
4
|
+
---
|
|
5
|
+
# /pantheon-optimize — Pantheon Project Optimizer
|
|
6
|
+
|
|
7
|
+
**What:** Multi-mode optimization: scans for documentation bloat, archives completed deepwork, migrates memory-bank flat files to MCP memory (ChromaDB), and reports token savings.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
/pantheon-optimize → Full scan + compress (default)
|
|
13
|
+
/pantheon-optimize --dry-run → Preview only, no changes
|
|
14
|
+
/pantheon-optimize --archive → Archive completed deepwork sessions
|
|
15
|
+
/pantheon-optimize --cache → Migrate memory-bank to MCP memory
|
|
16
|
+
/pantheon-optimize --path=<dir> → Limit scan to directory
|
|
17
|
+
/pantheon-optimize --all → Run all modes (default)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Modes
|
|
21
|
+
|
|
22
|
+
### `--archive` — Deepwork Archiving
|
|
23
|
+
Archives completed deepwork sessions from `.pantheon/deepwork/<slug>/` to `.pantheon/deepwork/archive/<slug>/`.
|
|
24
|
+
|
|
25
|
+
**Criteria:** A session is "completed" if it has a `REVIEW.md` containing "APPROVED".
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
/pantheon-optimize archive → Interactive (ask per slug)
|
|
29
|
+
/pantheon-optimize archive --dry-run → Preview only
|
|
30
|
+
/pantheon-optimize archive --auto → Archive all completed
|
|
31
|
+
/pantheon-optimize archive --auto --keep=2 → Keep only PLAN.md + REVIEW.md
|
|
32
|
+
/pantheon-optimize archive --auto --compress → Single .md per slug
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### `--cache` — Memory Bank Migration
|
|
36
|
+
Scans `.pantheon/memory-bank/` for flat `.md` files and migrates them to the MCP memory server (ChromaDB).
|
|
37
|
+
|
|
38
|
+
**What it does:**
|
|
39
|
+
1. Reads all `.md` files in `.pantheon/memory-bank/`
|
|
40
|
+
2. Chunks by heading (## sections)
|
|
41
|
+
3. Calls `memory_store()` for each chunk with appropriate namespace
|
|
42
|
+
4. Optionally archives the original files after successful migration
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
/pantheon-optimize cache → Preview candidates, ask confirm
|
|
46
|
+
/pantheon-optimize cache --dry-run → Show what would migrate
|
|
47
|
+
/pantheon-optimize cache --auto → Migrate + archive originals
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### `--dry-run` (global flag)
|
|
51
|
+
Prevents any destructive action. Shows what WOULD be done.
|
|
52
|
+
|
|
53
|
+
### Default mode (no flags)
|
|
54
|
+
Runs bloat scan on all `.md` and `.json` files + compression report.
|
|
55
|
+
|
|
56
|
+
## Output
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
🧹 Pantheon Optimize Report
|
|
60
|
+
|
|
61
|
+
📦 Bloat Scan: 47 .md + 12 .json files
|
|
62
|
+
→ 3 oversized files found
|
|
63
|
+
→ ~2.4K tokens recoverable
|
|
64
|
+
|
|
65
|
+
📁 Deepwork Archive: 15 candidates
|
|
66
|
+
→ 12 completed (REVIEW.md APPROVED)
|
|
67
|
+
→ 3 in progress (skipped)
|
|
68
|
+
→ Run with --archive --auto to archive 12 sessions
|
|
69
|
+
|
|
70
|
+
🗄️ Memory Cache: 24 files in memory-bank/
|
|
71
|
+
→ 18 already in MCP memory (dedup)
|
|
72
|
+
→ 6 candidates for migration (~3.2K tokens)
|
|
73
|
+
→ Run with --cache to migrate
|
|
74
|
+
|
|
75
|
+
📊 Summary:
|
|
76
|
+
⚡ 15 files optimizable → ~5.6K tokens
|
|
77
|
+
🗑️ 12 deepworks archivable → ~320 files cleanup
|
|
78
|
+
🗄️ 6 memory files → MCP migration ready
|
|
79
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Dispatch a question to 2-4 specialist agents via inline Zeus council"
|
|
3
|
+
agent: zeus
|
|
4
|
+
---
|
|
5
|
+
# /pantheon — Council Synthesis
|
|
6
|
+
|
|
7
|
+
**What:** Dispatches the question to 2-3 specialist agents **in parallel** via `task()`, then synthesizes their responses into a single recommendation with resolved divergences.
|
|
8
|
+
**Usage:** `/pantheon <question>`
|
|
9
|
+
**When:** Architecture trade-offs, technology selection, security assessments, multi-stakeholder concerns
|
|
10
|
+
**Returns:** Structured synthesis with recommendation, confidence level, resolved divergences
|
|
11
|
+
|
|
12
|
+
## Dispatch Selection
|
|
13
|
+
|
|
14
|
+
Choose 2-3 agents based on domain:
|
|
15
|
+
|
|
16
|
+
| Domain | Specialists |
|
|
17
|
+
|--------|-------------|
|
|
18
|
+
| Architecture | hermes, demeter, themis, athena |
|
|
19
|
+
| Security | themis, hermes, prometheus, nyx |
|
|
20
|
+
| Database | demeter, hermes, prometheus |
|
|
21
|
+
| AI/RAG | hephaestus, nyx |
|
|
22
|
+
| Infrastructure | prometheus, hermes, themis |
|
|
23
|
+
| Frontend/UX | aphrodite, themis, hermes |
|
|
24
|
+
| Observability | nyx, hermes |
|
|
25
|
+
| General | athena, themis, hermes |
|
|
26
|
+
|
|
27
|
+
Send ALL `task()` calls in a single message. Each must return: Recommendation · Reasoning · Trade-offs · Risks · Confidence.
|
|
28
|
+
|
|
29
|
+
## Synthesis Structure
|
|
30
|
+
|
|
31
|
+
After ALL responses arrive, produce EXACTLY this structure:
|
|
32
|
+
|
|
33
|
+
### 📋 Individual Assessments
|
|
34
|
+
For each specialist: Recommendation, Reasoning, Trade-offs, Risks, Confidence
|
|
35
|
+
|
|
36
|
+
### ✅ Agreements
|
|
37
|
+
Where 2+ specialists converge
|
|
38
|
+
|
|
39
|
+
### ⚡ Divergences
|
|
40
|
+
Where they disagree — resolve each one with reasoning
|
|
41
|
+
|
|
42
|
+
### 🔥 Total Divergence
|
|
43
|
+
If ALL specialists disagree on a point, call this out explicitly
|
|
44
|
+
|
|
45
|
+
### 🏆 Recommendation
|
|
46
|
+
Decisive conclusion with confidence level
|
|
47
|
+
|
|
48
|
+
## Question:
|
|
49
|
+
$ARGUMENTS
|
package/opencode.json
CHANGED
|
@@ -40,41 +40,8 @@
|
|
|
40
40
|
"themis": {
|
|
41
41
|
"model": "opencode-go/deepseek-v4-pro"
|
|
42
42
|
},
|
|
43
|
-
"zeus": {
|
|
44
|
-
"model": "opencode-go/deepseek-v4-pro"
|
|
45
|
-
},
|
|
46
43
|
"hephaestus": {
|
|
47
44
|
"model": "opencode-go/deepseek-v4-pro"
|
|
48
|
-
},
|
|
49
|
-
"hermes": {
|
|
50
|
-
"model": "opencode-go/deepseek-v4-flash"
|
|
51
|
-
},
|
|
52
|
-
"aphrodite": {
|
|
53
|
-
"model": "opencode-go/deepseek-v4-flash"
|
|
54
|
-
},
|
|
55
|
-
"demeter": {
|
|
56
|
-
"model": "opencode-go/deepseek-v4-flash"
|
|
57
|
-
},
|
|
58
|
-
"prometheus": {
|
|
59
|
-
"model": "opencode-go/deepseek-v4-flash"
|
|
60
|
-
},
|
|
61
|
-
"apollo": {
|
|
62
|
-
"model": "opencode-go/deepseek-v4-flash"
|
|
63
|
-
},
|
|
64
|
-
"nyx": {
|
|
65
|
-
"model": "opencode-go/deepseek-v4-flash"
|
|
66
|
-
},
|
|
67
|
-
"gaia": {
|
|
68
|
-
"model": "opencode-go/deepseek-v4-flash"
|
|
69
|
-
},
|
|
70
|
-
"iris": {
|
|
71
|
-
"model": "opencode-go/deepseek-v4-flash"
|
|
72
|
-
},
|
|
73
|
-
"mnemosyne": {
|
|
74
|
-
"model": "opencode-go/deepseek-v4-flash"
|
|
75
|
-
},
|
|
76
|
-
"talos": {
|
|
77
|
-
"model": "opencode-go/deepseek-v4-flash"
|
|
78
45
|
}
|
|
79
46
|
}
|
|
80
|
-
}
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pantheon-opencode",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Multi-agent orchestration for OpenCode
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Multi-agent orchestration for OpenCode \u2014 14 specialized agents, TDD, Themis review gates.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
7
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"opencode": "OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true opencode",
|
|
16
16
|
"start": "OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true opencode",
|
|
17
17
|
"prepublishOnly": "node scripts/validate-routing.mjs",
|
|
18
|
-
"postpublish": "node -e \"console.log('
|
|
18
|
+
"postpublish": "node -e \"console.log('\u2705 Published pantheon-opencode v' + require('./package.json').version + ' to npm')\"",
|
|
19
19
|
"version:status": "node scripts/versioning.mjs status",
|
|
20
20
|
"version:apply": "node scripts/versioning.mjs apply",
|
|
21
21
|
"release:dry-run": "node scripts/versioning.mjs status && npm pack --dry-run",
|
|
@@ -80,4 +80,4 @@
|
|
|
80
80
|
"node": ">=18"
|
|
81
81
|
},
|
|
82
82
|
"preferGlobal": true
|
|
83
|
-
}
|
|
83
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# Auto-generated: resolved symlink from ../src/mcp/mcp_persistence_server.py
|
|
2
1
|
#!/usr/bin/env python3
|
|
3
2
|
"""Pantheon Persistence MCP Server.
|
|
4
3
|
|
|
@@ -36,6 +35,9 @@ CREATE TABLE IF NOT EXISTS kv_store (
|
|
|
36
35
|
UNIQUE(namespace, key)
|
|
37
36
|
);
|
|
38
37
|
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_kv_namespace_expires
|
|
39
|
+
ON kv_store(namespace, expires_at);
|
|
40
|
+
|
|
39
41
|
CREATE VIRTUAL TABLE IF NOT EXISTS kv_store_fts USING fts5(
|
|
40
42
|
namespace, key, value,
|
|
41
43
|
content='kv_store', content_rowid='id',
|
|
@@ -52,6 +54,14 @@ CREATE TRIGGER IF NOT EXISTS kv_store_ad AFTER DELETE ON kv_store BEGIN
|
|
|
52
54
|
VALUES('delete', old.id, old.namespace, old.key, old.value);
|
|
53
55
|
END;
|
|
54
56
|
|
|
57
|
+
CREATE TRIGGER IF NOT EXISTS kv_store_softdelete_ad
|
|
58
|
+
AFTER UPDATE OF deleted_at ON kv_store
|
|
59
|
+
WHEN new.deleted_at IS NOT NULL AND old.deleted_at IS NULL
|
|
60
|
+
BEGIN
|
|
61
|
+
INSERT INTO kv_store_fts(kv_store_fts, rowid)
|
|
62
|
+
VALUES('delete', old.id);
|
|
63
|
+
END;
|
|
64
|
+
|
|
55
65
|
CREATE TRIGGER IF NOT EXISTS kv_store_au AFTER UPDATE ON kv_store BEGIN
|
|
56
66
|
INSERT INTO kv_store_fts(kv_store_fts, rowid, namespace, key, value)
|
|
57
67
|
VALUES('delete', old.id, old.namespace, old.key, old.value);
|
|
@@ -220,6 +230,10 @@ async def kv_store(
|
|
|
220
230
|
(namespace, key, value, expires_at),
|
|
221
231
|
)
|
|
222
232
|
conn.commit()
|
|
233
|
+
|
|
234
|
+
# Opportunistic auto-purge: if namespace has >500 entries, purge expired
|
|
235
|
+
_opportunistic_auto_purge(conn, namespace)
|
|
236
|
+
|
|
223
237
|
return {"status": "stored", "namespace": namespace, "key": key}
|
|
224
238
|
|
|
225
239
|
|
|
@@ -254,6 +268,52 @@ async def kv_get(
|
|
|
254
268
|
return str(row[0]) if row else None
|
|
255
269
|
|
|
256
270
|
|
|
271
|
+
@mcp.tool(
|
|
272
|
+
name="kv_stats",
|
|
273
|
+
description="Return storage statistics: total entries, expired count, per-namespace breakdown, DB file size.",
|
|
274
|
+
)
|
|
275
|
+
async def kv_stats(
|
|
276
|
+
scope: str = "project",
|
|
277
|
+
) -> dict:
|
|
278
|
+
"""Return storage statistics for the given scope.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
scope: 'project' (default) or 'global'.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Dict with total_entries, expired_entries, namespaces breakdown, db_size_bytes.
|
|
285
|
+
"""
|
|
286
|
+
conn = _db(scope)
|
|
287
|
+
|
|
288
|
+
total = conn.execute("SELECT COUNT(*) FROM kv_store WHERE deleted_at IS NULL").fetchone()[0]
|
|
289
|
+
expired = conn.execute(
|
|
290
|
+
"SELECT COUNT(*) FROM kv_store "
|
|
291
|
+
"WHERE expires_at IS NOT NULL AND expires_at < datetime('now') "
|
|
292
|
+
"AND deleted_at IS NULL"
|
|
293
|
+
).fetchone()[0]
|
|
294
|
+
|
|
295
|
+
ns_rows = conn.execute(
|
|
296
|
+
"SELECT namespace, COUNT(*) as cnt, "
|
|
297
|
+
"SUM(CASE WHEN expires_at IS NOT NULL AND expires_at < datetime('now') THEN 1 ELSE 0 END) as expired_count "
|
|
298
|
+
"FROM kv_store WHERE deleted_at IS NULL GROUP BY namespace ORDER BY cnt DESC"
|
|
299
|
+
).fetchall()
|
|
300
|
+
|
|
301
|
+
namespaces = {
|
|
302
|
+
r[0]: {"count": r[1], "expired": r[2]}
|
|
303
|
+
for r in ns_rows
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
db_path = _resolve_db_path(scope)
|
|
307
|
+
db_size = db_path.stat().st_size if db_path and db_path.exists() else 0
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
"scope": scope,
|
|
311
|
+
"total_entries": total,
|
|
312
|
+
"expired_entries": expired,
|
|
313
|
+
"namespaces": namespaces,
|
|
314
|
+
"db_size_bytes": db_size,
|
|
315
|
+
}
|
|
316
|
+
|
|
257
317
|
@mcp.tool(
|
|
258
318
|
name="kv_delete",
|
|
259
319
|
description="Delete a key-value pair by namespace and key. "
|
|
@@ -353,6 +413,7 @@ async def kv_search(
|
|
|
353
413
|
|
|
354
414
|
# Sanitize: extract up to 10 word tokens, wrap each in quotes
|
|
355
415
|
terms = re.findall(r"\w+", query)[:10]
|
|
416
|
+
terms = [re.escape(t) for t in terms]
|
|
356
417
|
if not terms:
|
|
357
418
|
return []
|
|
358
419
|
fts_query = " OR ".join(f'"{t}"' for t in terms)
|
|
@@ -441,6 +502,275 @@ async def purge_expired(
|
|
|
441
502
|
return {"purged": count, "dry_run": False}
|
|
442
503
|
|
|
443
504
|
|
|
505
|
+
|
|
506
|
+
def _opportunistic_auto_purge(conn: sqlite3.Connection, namespace: str, threshold: int = 500) -> None:
|
|
507
|
+
"""Lightweight auto-purge: if namespace exceeds threshold, soft-delete expired entries."""
|
|
508
|
+
count = conn.execute(
|
|
509
|
+
"SELECT COUNT(*) FROM kv_store WHERE namespace = ? AND deleted_at IS NULL",
|
|
510
|
+
(namespace,),
|
|
511
|
+
).fetchone()[0]
|
|
512
|
+
if count <= threshold:
|
|
513
|
+
return
|
|
514
|
+
|
|
515
|
+
conn.execute(
|
|
516
|
+
"UPDATE kv_store SET deleted_at = datetime('now') "
|
|
517
|
+
"WHERE namespace = ? AND expires_at IS NOT NULL "
|
|
518
|
+
"AND expires_at < datetime('now') AND deleted_at IS NULL",
|
|
519
|
+
(namespace,),
|
|
520
|
+
)
|
|
521
|
+
conn.commit()
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@mcp.tool(
|
|
525
|
+
name="kv_delete_namespace",
|
|
526
|
+
description="Delete all entries in a namespace. Optionally limit to entries older than N days.",
|
|
527
|
+
)
|
|
528
|
+
async def kv_delete_namespace(
|
|
529
|
+
namespace: str,
|
|
530
|
+
scope: str = "project",
|
|
531
|
+
older_than_days: int | None = None,
|
|
532
|
+
) -> dict:
|
|
533
|
+
"""Delete all entries in a namespace, optionally old entries only.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
namespace: Namespace to clear.
|
|
537
|
+
scope: 'project' (default) or 'global'.
|
|
538
|
+
older_than_days: If set, only delete entries older than this many days.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
Dict with deleted count.
|
|
542
|
+
"""
|
|
543
|
+
conn = _db(scope)
|
|
544
|
+
|
|
545
|
+
if older_than_days is not None:
|
|
546
|
+
cursor = conn.execute(
|
|
547
|
+
"DELETE FROM kv_store WHERE namespace = ? "
|
|
548
|
+
"AND datetime(created_at) < datetime('now', ? || ' days')",
|
|
549
|
+
(namespace, f"-{older_than_days}"),
|
|
550
|
+
)
|
|
551
|
+
else:
|
|
552
|
+
cursor = conn.execute(
|
|
553
|
+
"DELETE FROM kv_store WHERE namespace = ?",
|
|
554
|
+
(namespace,),
|
|
555
|
+
)
|
|
556
|
+
conn.commit()
|
|
557
|
+
|
|
558
|
+
return {"deleted": cursor.rowcount}
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
# ── Context Checkpoint Tools ────────────────────────────────────────────────────
|
|
562
|
+
# Session-scoped context storage for deepwork checkpoints, phase state,
|
|
563
|
+
# tool call history, and reasoning chains. All entries use namespace
|
|
564
|
+
# "checkpoint:{slug}" with auto-TTL of 4 hours (session duration).
|
|
565
|
+
|
|
566
|
+
DEFAULT_CONTEXT_TTL: int = 14400 # 4 hours
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
@mcp.tool(
|
|
570
|
+
name="context_save",
|
|
571
|
+
description="Save a context checkpoint for a session/phase. "
|
|
572
|
+
"Stores structured JSON in persistence KV with auto-TTL of 4h.",
|
|
573
|
+
)
|
|
574
|
+
async def context_save(
|
|
575
|
+
slug: str,
|
|
576
|
+
key: str,
|
|
577
|
+
content: str,
|
|
578
|
+
session_id: str | None = None,
|
|
579
|
+
ttl: int | None = None,
|
|
580
|
+
scope: str = "project",
|
|
581
|
+
) -> dict:
|
|
582
|
+
"""Save a context checkpoint with session-level isolation.
|
|
583
|
+
|
|
584
|
+
If session_id is provided, namespace becomes "checkpoint:{slug}:{session_id}"
|
|
585
|
+
preventing cross-session reads. If omitted, a random UUID is auto-generated
|
|
586
|
+
and returned so the caller can pass it to subsequent calls.
|
|
587
|
+
|
|
588
|
+
Args:
|
|
589
|
+
slug: Session identifier (e.g. "auth-refactor").
|
|
590
|
+
key: Checkpoint key (e.g. "phase:3", "latest", "heartbeat").
|
|
591
|
+
content: JSON-serializable string or any text content.
|
|
592
|
+
session_id: Unique session ID for namespace isolation (auto-generated if None).
|
|
593
|
+
ttl: TTL in seconds (default 4h / 14400). None = session duration.
|
|
594
|
+
scope: 'project' (default) or 'global'.
|
|
595
|
+
|
|
596
|
+
Returns:
|
|
597
|
+
Status dict with namespace, key, and session_id for subsequent calls.
|
|
598
|
+
"""
|
|
599
|
+
actual_session = session_id or uuid.uuid4().hex[:12]
|
|
600
|
+
ns = f"checkpoint:{slug}:{actual_session}"
|
|
601
|
+
actual_ttl = ttl if ttl is not None else DEFAULT_CONTEXT_TTL
|
|
602
|
+
|
|
603
|
+
conn = _db(scope)
|
|
604
|
+
expires_at = (datetime.now(timezone.utc) + timedelta(seconds=actual_ttl)).isoformat()
|
|
605
|
+
|
|
606
|
+
conn.execute(
|
|
607
|
+
"INSERT INTO kv_store (namespace, key, value, expires_at, "
|
|
608
|
+
"created_at, updated_at) VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))"
|
|
609
|
+
"ON CONFLICT(namespace, key) DO UPDATE SET "
|
|
610
|
+
" value = excluded.value, "
|
|
611
|
+
" expires_at = excluded.expires_at, "
|
|
612
|
+
" updated_at = datetime('now')",
|
|
613
|
+
(ns, key, content, expires_at),
|
|
614
|
+
)
|
|
615
|
+
conn.commit()
|
|
616
|
+
|
|
617
|
+
# Also always update "latest" pointer
|
|
618
|
+
if key != "latest":
|
|
619
|
+
# Use a larger TTL for "latest" to outlive individual phase entries
|
|
620
|
+
conn.execute(
|
|
621
|
+
"INSERT INTO kv_store (namespace, key, value, expires_at, "
|
|
622
|
+
"created_at, updated_at) VALUES (?, 'latest', ?, ?, datetime('now'), datetime('now'))"
|
|
623
|
+
"ON CONFLICT(namespace, key) DO UPDATE SET "
|
|
624
|
+
" value = excluded.value, "
|
|
625
|
+
" expires_at = excluded.expires_at, "
|
|
626
|
+
" updated_at = datetime('now')",
|
|
627
|
+
(ns, content, expires_at),
|
|
628
|
+
)
|
|
629
|
+
conn.commit()
|
|
630
|
+
|
|
631
|
+
return {"status": "stored", "namespace": ns, "key": key, "session_id": actual_session, "ttl": actual_ttl}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
@mcp.tool(
|
|
635
|
+
name="context_get",
|
|
636
|
+
description="Retrieve a context checkpoint by session slug and key. "
|
|
637
|
+
"Returns the raw content string or null if expired/not found.",
|
|
638
|
+
)
|
|
639
|
+
async def context_get(
|
|
640
|
+
slug: str,
|
|
641
|
+
key: str = "latest",
|
|
642
|
+
session_id: str | None = None,
|
|
643
|
+
scope: str = "project",
|
|
644
|
+
) -> str | None:
|
|
645
|
+
"""Get a context checkpoint with session isolation.
|
|
646
|
+
|
|
647
|
+
Args:
|
|
648
|
+
slug: Session identifier.
|
|
649
|
+
key: Checkpoint key (default "latest").
|
|
650
|
+
session_id: Required for isolation. If None, searches without session scope.
|
|
651
|
+
scope: 'project' (default) or 'global'.
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
Content string or None if not found/expired.
|
|
655
|
+
"""
|
|
656
|
+
conn = _db(scope)
|
|
657
|
+
ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
|
|
658
|
+
row = conn.execute(
|
|
659
|
+
"SELECT value FROM kv_store "
|
|
660
|
+
"WHERE namespace = ? AND key = ? "
|
|
661
|
+
"AND (expires_at IS NULL OR expires_at > datetime('now')) "
|
|
662
|
+
"AND deleted_at IS NULL",
|
|
663
|
+
(ns, key),
|
|
664
|
+
).fetchone()
|
|
665
|
+
return row[0] if row else None
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
@mcp.tool(
|
|
669
|
+
name="context_list",
|
|
670
|
+
description="List all context checkpoints for a session slug. "
|
|
671
|
+
"Returns keys, timestamps, and TTL info.",
|
|
672
|
+
)
|
|
673
|
+
async def context_list(
|
|
674
|
+
slug: str,
|
|
675
|
+
session_id: str | None = None,
|
|
676
|
+
scope: str = "project",
|
|
677
|
+
) -> list[dict]:
|
|
678
|
+
"""List all checkpoints for a session.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
slug: Session identifier.
|
|
682
|
+
session_id: Optional. If provided, narrows to specific session.
|
|
683
|
+
scope: 'project' (default) or 'global'.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
List of dicts with key, created_at, expires_at.
|
|
687
|
+
"""
|
|
688
|
+
conn = _db(scope)
|
|
689
|
+
ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
|
|
690
|
+
rows = conn.execute(
|
|
691
|
+
"SELECT key, created_at, expires_at FROM kv_store "
|
|
692
|
+
"WHERE namespace = ? "
|
|
693
|
+
"AND (expires_at IS NULL OR expires_at > datetime('now')) "
|
|
694
|
+
"AND deleted_at IS NULL "
|
|
695
|
+
"ORDER BY created_at DESC LIMIT 50",
|
|
696
|
+
(ns,),
|
|
697
|
+
).fetchall()
|
|
698
|
+
return [
|
|
699
|
+
{
|
|
700
|
+
"key": r[0],
|
|
701
|
+
"created_at": r[1],
|
|
702
|
+
"expires_at": r[2],
|
|
703
|
+
}
|
|
704
|
+
for r in rows
|
|
705
|
+
]
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
@mcp.tool(
|
|
709
|
+
name="context_stats",
|
|
710
|
+
description="Return session context statistics: "
|
|
711
|
+
"checkpoint count, TTL remaining, total size per slug.",
|
|
712
|
+
)
|
|
713
|
+
async def context_stats(
|
|
714
|
+
slug: str,
|
|
715
|
+
session_id: str | None = None,
|
|
716
|
+
scope: str = "project",
|
|
717
|
+
) -> dict:
|
|
718
|
+
"""Return storage statistics for a session's context.
|
|
719
|
+
|
|
720
|
+
Args:
|
|
721
|
+
slug: Session identifier.
|
|
722
|
+
session_id: Optional. If provided, narrows to specific session.
|
|
723
|
+
scope: 'project' (default) or 'global'.
|
|
724
|
+
|
|
725
|
+
Returns:
|
|
726
|
+
Dict with entry_count, total_bytes, ttl_remaining.
|
|
727
|
+
"""
|
|
728
|
+
conn = _db(scope)
|
|
729
|
+
ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
|
|
730
|
+
|
|
731
|
+
count = conn.execute(
|
|
732
|
+
"SELECT COUNT(*) FROM kv_store "
|
|
733
|
+
"WHERE namespace = ? AND deleted_at IS NULL",
|
|
734
|
+
(ns,),
|
|
735
|
+
).fetchone()[0]
|
|
736
|
+
|
|
737
|
+
expired = conn.execute(
|
|
738
|
+
"SELECT COUNT(*) FROM kv_store "
|
|
739
|
+
"WHERE namespace = ? AND expires_at < datetime('now') "
|
|
740
|
+
"AND deleted_at IS NULL",
|
|
741
|
+
(ns,),
|
|
742
|
+
).fetchone()[0]
|
|
743
|
+
|
|
744
|
+
size = conn.execute(
|
|
745
|
+
"SELECT COALESCE(SUM(LENGTH(value)), 0) FROM kv_store "
|
|
746
|
+
"WHERE namespace = ? AND deleted_at IS NULL",
|
|
747
|
+
(ns,),
|
|
748
|
+
).fetchone()[0]
|
|
749
|
+
|
|
750
|
+
# Check TTL of latest entry
|
|
751
|
+
remaining: int | None = None
|
|
752
|
+
row = conn.execute(
|
|
753
|
+
"SELECT expires_at FROM kv_store "
|
|
754
|
+
"WHERE namespace = ? AND key = 'latest' AND deleted_at IS NULL",
|
|
755
|
+
(ns,),
|
|
756
|
+
).fetchone()
|
|
757
|
+
if row and row[0]:
|
|
758
|
+
try:
|
|
759
|
+
exp = datetime.fromisoformat(row[0])
|
|
760
|
+
now = datetime.now(timezone.utc)
|
|
761
|
+
remaining = max(0, int((exp - now).total_seconds()))
|
|
762
|
+
except (ValueError, TypeError):
|
|
763
|
+
pass
|
|
764
|
+
|
|
765
|
+
return {
|
|
766
|
+
"slug": slug,
|
|
767
|
+
"namespace": ns,
|
|
768
|
+
"entry_count": count,
|
|
769
|
+
"expired_entries": expired,
|
|
770
|
+
"total_bytes": size,
|
|
771
|
+
"ttl_remaining_seconds": remaining,
|
|
772
|
+
}
|
|
773
|
+
|
|
444
774
|
def _resolve_db_path(scope: str) -> Path | None:
|
|
445
775
|
"""Resolve the database file path for a given scope."""
|
|
446
776
|
if scope == "global":
|