jorgex-stack 1.0.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/LICENSE +21 -0
- package/PRD.md +297 -0
- package/README.md +58 -0
- package/dist/cli.js +2894 -0
- package/package.json +51 -0
- package/stack/agents/README.md +33 -0
- package/stack/agents/backend-analyst.md +61 -0
- package/stack/agents/code-reviewer.md +66 -0
- package/stack/agents/code-simplifier.md +81 -0
- package/stack/agents/comment-fixer.md +54 -0
- package/stack/agents/docs-maintainer.md +88 -0
- package/stack/agents/engram.md +124 -0
- package/stack/agents/frontend-analyst.md +51 -0
- package/stack/agents/implementer.md +55 -0
- package/stack/agents/orchestrator.md +192 -0
- package/stack/agents/security-auditor.md +66 -0
- package/stack/agents/silent-failure-hunter.md +167 -0
- package/stack/agents/test-analyzer.md +91 -0
- package/stack/agents/tester.md +71 -0
- package/stack/agents/translator.md +101 -0
- package/stack/agents/type-design-analyzer.md +128 -0
- package/stack/commands/xreview.md +80 -0
- package/stack/config/defaults.json +37 -0
- package/stack/hooks/hooks.json +18 -0
- package/stack/mcp/servers.json +19 -0
- package/stack/plugins/opencode/engram.ts +378 -0
- package/stack/plugins/opencode/hooks.ts +766 -0
- package/stack/plugins/opencode/package.json +10 -0
- package/stack/plugins/opencode/worktree.ts +405 -0
- package/stack/scripts/post-pr-review.cjs +156 -0
- package/stack/skills/agent-browser/SKILL.md +55 -0
- package/stack/skills/agent-delegation/SKILL.md +58 -0
- package/stack/skills/deploy-to-vercel/SKILL.md +296 -0
- package/stack/skills/deploy-to-vercel/resources/deploy-codex.sh +301 -0
- package/stack/skills/deploy-to-vercel/resources/deploy.sh +301 -0
- package/stack/skills/diagnose/SKILL.md +117 -0
- package/stack/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/stack/skills/find-skills/SKILL.md +133 -0
- package/stack/skills/graphify/.graphify_version +1 -0
- package/stack/skills/graphify/SKILL.md +1319 -0
- package/stack/skills/mcp-builder/LICENSE.txt +202 -0
- package/stack/skills/mcp-builder/SKILL.md +236 -0
- package/stack/skills/mcp-builder/reference/evaluation.md +602 -0
- package/stack/skills/mcp-builder/reference/mcp_best_practices.md +249 -0
- package/stack/skills/mcp-builder/reference/node_mcp_server.md +970 -0
- package/stack/skills/mcp-builder/reference/python_mcp_server.md +719 -0
- package/stack/skills/mcp-builder/scripts/connections.py +151 -0
- package/stack/skills/mcp-builder/scripts/evaluation.py +373 -0
- package/stack/skills/mcp-builder/scripts/example_evaluation.xml +22 -0
- package/stack/skills/mcp-builder/scripts/requirements.txt +2 -0
- package/stack/skills/obsidian-cli/SKILL.md +106 -0
- package/stack/skills/obsidian-markdown/SKILL.md +196 -0
- package/stack/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
- package/stack/skills/obsidian-markdown/references/EMBEDS.md +63 -0
- package/stack/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
- package/stack/skills/react-doctor/SKILL.md +19 -0
- package/stack/skills/skill-creator/LICENSE.txt +202 -0
- package/stack/skills/skill-creator/SKILL.md +485 -0
- package/stack/skills/skill-creator/agents/analyzer.md +274 -0
- package/stack/skills/skill-creator/agents/comparator.md +202 -0
- package/stack/skills/skill-creator/agents/grader.md +223 -0
- package/stack/skills/skill-creator/assets/eval_review.html +146 -0
- package/stack/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/stack/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/stack/skills/skill-creator/references/schemas.md +430 -0
- package/stack/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/stack/skills/skill-creator/scripts/generate_report.py +326 -0
- package/stack/skills/skill-creator/scripts/improve_description.py +248 -0
- package/stack/skills/skill-creator/scripts/package_skill.py +136 -0
- package/stack/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/stack/skills/skill-creator/scripts/run_eval.py +310 -0
- package/stack/skills/skill-creator/scripts/run_loop.py +332 -0
- package/stack/skills/skill-creator/scripts/utils.py +47 -0
- package/stack/skills/supabase/SKILL.md +135 -0
- package/stack/skills/supabase/assets/feedback-issue-template.md +17 -0
- package/stack/skills/supabase/references/skill-feedback.md +17 -0
- package/stack/skills/supabase-postgres-best-practices/SKILL.md +64 -0
- package/stack/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
- package/stack/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
- package/stack/skills/supabase-postgres-best-practices/references/_template.md +34 -0
- package/stack/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
- package/stack/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
- package/stack/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
- package/stack/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
- package/stack/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
- package/stack/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
- package/stack/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
- package/stack/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
- package/stack/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
- package/stack/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
- package/stack/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
- package/stack/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
- package/stack/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
- package/stack/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
- package/stack/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
- package/stack/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
- package/stack/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
- package/stack/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
- package/stack/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
- package/stack/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
- package/stack/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
- package/stack/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
- package/stack/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
- package/stack/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
- package/stack/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
- package/stack/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
- package/stack/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
- package/stack/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
- package/stack/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
- package/stack/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
- package/stack/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
- package/stack/skills/tdd/SKILL.md +109 -0
- package/stack/skills/tdd/deep-modules.md +33 -0
- package/stack/skills/tdd/interface-design.md +31 -0
- package/stack/skills/tdd/mocking.md +59 -0
- package/stack/skills/tdd/refactoring.md +10 -0
- package/stack/skills/tdd/tests.md +61 -0
- package/stack/skills/to-issues/SKILL.md +83 -0
- package/stack/skills/to-prd/SKILL.md +72 -0
- package/stack/skills/work-lifecycle/SKILL.md +79 -0
- package/stack/skills/work-lifecycle/references/plan-template.md +185 -0
- package/stack/system-prompt/AGENTS.md +171 -0
- package/stack/system-prompt/engram-protocol.md +53 -0
- package/upstreams.json +96 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engram — OpenCode plugin adapter
|
|
3
|
+
*
|
|
4
|
+
* Thin layer that connects OpenCode's event system to the Engram Go binary.
|
|
5
|
+
* The Go binary runs as a local HTTP server and handles all persistence.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* OpenCode events → this plugin → HTTP calls → engram serve → SQLite
|
|
9
|
+
*
|
|
10
|
+
* Session resilience:
|
|
11
|
+
* Uses `ensureSession()` before any DB write. This means sessions are
|
|
12
|
+
* created on-demand — even if the plugin was loaded after the session
|
|
13
|
+
* started (restart, reconnect, etc.). The session ID comes from OpenCode's
|
|
14
|
+
* hooks (input.sessionID) rather than relying on a session.created event.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
18
|
+
|
|
19
|
+
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const ENGRAM_PORT = parseInt(process.env.ENGRAM_PORT ?? "7437")
|
|
22
|
+
const ENGRAM_URL = `http://127.0.0.1:${ENGRAM_PORT}`
|
|
23
|
+
// "{{ENGRAM_BIN}}" lo resuelve el instalador con el binario detectado (D7).
|
|
24
|
+
const ENGRAM_BIN = process.env.ENGRAM_BIN ?? Bun.which("engram") ?? "{{ENGRAM_BIN}}"
|
|
25
|
+
|
|
26
|
+
// Engram's own MCP tools — don't count these as "tool calls" for session stats
|
|
27
|
+
const ENGRAM_TOOLS = new Set([
|
|
28
|
+
"mem_search",
|
|
29
|
+
"mem_save",
|
|
30
|
+
"mem_update",
|
|
31
|
+
"mem_delete",
|
|
32
|
+
"mem_suggest_topic_key",
|
|
33
|
+
"mem_save_prompt",
|
|
34
|
+
"mem_session_summary",
|
|
35
|
+
"mem_context",
|
|
36
|
+
"mem_stats",
|
|
37
|
+
"mem_timeline",
|
|
38
|
+
"mem_get_observation",
|
|
39
|
+
"mem_session_start",
|
|
40
|
+
"mem_session_end",
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
// ─── Memory Instructions ─────────────────────────────────────────────────────
|
|
44
|
+
// These get injected into the agent's context so it knows to call mem_save.
|
|
45
|
+
// El placeholder ENGRAM_PROTOCOL lo resuelve el instalador con el contenido
|
|
46
|
+
// del protocolo canónico (stack/system-prompt/engram-protocol.md) — única
|
|
47
|
+
// fuente, alineada automáticamente con lo que reciben Claude Code y Codex.
|
|
48
|
+
|
|
49
|
+
const MEMORY_INSTRUCTIONS = "{{ENGRAM_PROTOCOL}}"
|
|
50
|
+
|
|
51
|
+
// ─── HTTP Client ─────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
async function engramFetch(
|
|
54
|
+
path: string,
|
|
55
|
+
opts: { method?: string; body?: any } = {}
|
|
56
|
+
): Promise<any> {
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(`${ENGRAM_URL}${path}`, {
|
|
59
|
+
method: opts.method ?? "GET",
|
|
60
|
+
headers: opts.body ? { "Content-Type": "application/json" } : undefined,
|
|
61
|
+
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
|
62
|
+
})
|
|
63
|
+
return await res.json()
|
|
64
|
+
} catch {
|
|
65
|
+
// Engram server not running — silently fail
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function isEngramRunning(): Promise<boolean> {
|
|
71
|
+
try {
|
|
72
|
+
const res = await fetch(`${ENGRAM_URL}/health`, {
|
|
73
|
+
signal: AbortSignal.timeout(500),
|
|
74
|
+
})
|
|
75
|
+
return res.ok
|
|
76
|
+
} catch {
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
function extractProjectName(directory: string): string {
|
|
84
|
+
// Try git remote origin URL
|
|
85
|
+
try {
|
|
86
|
+
const result = Bun.spawnSync(["git", "-C", directory, "remote", "get-url", "origin"])
|
|
87
|
+
if (result.exitCode === 0) {
|
|
88
|
+
const url = result.stdout?.toString().trim()
|
|
89
|
+
if (url) {
|
|
90
|
+
const name = url.replace(/\.git$/, "").split(/[/:]/).pop()
|
|
91
|
+
if (name) return name
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
|
|
96
|
+
// Fallback: git root directory name (works in worktrees)
|
|
97
|
+
try {
|
|
98
|
+
const result = Bun.spawnSync(["git", "-C", directory, "rev-parse", "--show-toplevel"])
|
|
99
|
+
if (result.exitCode === 0) {
|
|
100
|
+
const root = result.stdout?.toString().trim()
|
|
101
|
+
if (root) return root.split(/[\\/]/).pop() ?? "unknown"
|
|
102
|
+
}
|
|
103
|
+
} catch {}
|
|
104
|
+
|
|
105
|
+
// Final fallback: cwd basename
|
|
106
|
+
return directory.split(/[\\/]/).pop() ?? "unknown"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function truncate(str: string, max: number): string {
|
|
110
|
+
if (!str) return ""
|
|
111
|
+
return str.length > max ? str.slice(0, max) + "..." : str
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Strip <private>...</private> tags before sending to engram.
|
|
116
|
+
* Double safety: the Go binary also strips, but we strip here too
|
|
117
|
+
* so sensitive data never even hits the wire.
|
|
118
|
+
*/
|
|
119
|
+
function stripPrivateTags(str: string): string {
|
|
120
|
+
if (!str) return ""
|
|
121
|
+
return str.replace(/<private>[\s\S]*?<\/private>/gi, "[REDACTED]").trim()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Plugin Export ───────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export const Engram: Plugin = async (ctx) => {
|
|
127
|
+
const oldProject = ctx.directory.split(/[\\/]/).pop() ?? "unknown"
|
|
128
|
+
const project = extractProjectName(ctx.directory)
|
|
129
|
+
|
|
130
|
+
// Track tool counts per session (in-memory only, not critical)
|
|
131
|
+
const toolCounts = new Map<string, number>()
|
|
132
|
+
|
|
133
|
+
// Track which sessions we've already ensured exist in engram
|
|
134
|
+
const knownSessions = new Set<string>()
|
|
135
|
+
|
|
136
|
+
// Track sub-agent session IDs so we can suppress their tool-hook registrations.
|
|
137
|
+
// Sub-agents (Task() calls) have a parentID or a title ending in " subagent)".
|
|
138
|
+
// We must not register them as top-level Engram sessions — they cause session
|
|
139
|
+
// inflation (e.g. 170 sessions for 1 real conversation, issue #116).
|
|
140
|
+
const subAgentSessions = new Set<string>()
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Ensure a session exists in engram. Idempotent — calls POST /sessions
|
|
144
|
+
* which uses INSERT OR IGNORE. Safe to call multiple times.
|
|
145
|
+
*
|
|
146
|
+
* Silently skips sub-agent sessions (tracked in `subAgentSessions`).
|
|
147
|
+
*/
|
|
148
|
+
async function ensureSession(sessionId: string): Promise<void> {
|
|
149
|
+
if (!sessionId || knownSessions.has(sessionId)) return
|
|
150
|
+
// Do not register sub-agent sessions in Engram (issue #116).
|
|
151
|
+
if (subAgentSessions.has(sessionId)) return
|
|
152
|
+
knownSessions.add(sessionId)
|
|
153
|
+
await engramFetch("/sessions", {
|
|
154
|
+
method: "POST",
|
|
155
|
+
body: {
|
|
156
|
+
id: sessionId,
|
|
157
|
+
project,
|
|
158
|
+
directory: ctx.directory,
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Try to start engram server if not running
|
|
164
|
+
const running = await isEngramRunning()
|
|
165
|
+
if (!running) {
|
|
166
|
+
try {
|
|
167
|
+
Bun.spawn([ENGRAM_BIN, "serve"], {
|
|
168
|
+
stdout: "ignore",
|
|
169
|
+
stderr: "ignore",
|
|
170
|
+
stdin: "ignore",
|
|
171
|
+
})
|
|
172
|
+
await new Promise((r) => setTimeout(r, 500))
|
|
173
|
+
} catch {
|
|
174
|
+
// Binary not found or can't start — plugin will silently no-op
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Migrate project name if it changed (one-time, idempotent)
|
|
179
|
+
// Must run AFTER server startup to ensure the endpoint is available
|
|
180
|
+
if (oldProject !== project) {
|
|
181
|
+
await engramFetch("/projects/migrate", {
|
|
182
|
+
method: "POST",
|
|
183
|
+
body: { old_project: oldProject, new_project: project },
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Auto-import: if .engram/manifest.json exists in the project repo,
|
|
188
|
+
// run `engram sync --import` to load any new chunks into the local DB.
|
|
189
|
+
// This is how git-synced memories get loaded when cloning a repo or
|
|
190
|
+
// pulling changes. Each chunk is imported only once (tracked by ID).
|
|
191
|
+
try {
|
|
192
|
+
const manifestFile = `${ctx.directory}/.engram/manifest.json`
|
|
193
|
+
const file = Bun.file(manifestFile)
|
|
194
|
+
if (await file.exists()) {
|
|
195
|
+
Bun.spawn([ENGRAM_BIN, "sync", "--import"], {
|
|
196
|
+
cwd: ctx.directory,
|
|
197
|
+
stdout: "ignore",
|
|
198
|
+
stderr: "ignore",
|
|
199
|
+
stdin: "ignore",
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// Manifest doesn't exist or binary not found — silently skip
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
// ─── Event Listeners ───────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
event: async ({ event }) => {
|
|
210
|
+
// --- Session Created ---
|
|
211
|
+
if (event.type === "session.created") {
|
|
212
|
+
// Bug fix (#116): session data is nested under event.properties.info,
|
|
213
|
+
// not event.properties directly.
|
|
214
|
+
const info = (event.properties as any)?.info
|
|
215
|
+
const sessionId = info?.id
|
|
216
|
+
const parentID = info?.parentID
|
|
217
|
+
const title: string = info?.title ?? ""
|
|
218
|
+
|
|
219
|
+
// Sub-agent sessions (created via Task()) must NOT be registered as
|
|
220
|
+
// top-level Engram sessions. They cause massive session inflation
|
|
221
|
+
// (e.g. 170 sessions for 1 real conversation).
|
|
222
|
+
//
|
|
223
|
+
// Detection heuristics:
|
|
224
|
+
// - parentID is set on all Task() sub-agent sessions
|
|
225
|
+
// - title ends with " subagent)" as a secondary signal
|
|
226
|
+
const isSubAgent = !!parentID || title.endsWith(" subagent)")
|
|
227
|
+
|
|
228
|
+
if (sessionId && !isSubAgent) {
|
|
229
|
+
await ensureSession(sessionId)
|
|
230
|
+
} else if (sessionId && isSubAgent) {
|
|
231
|
+
// Remember this as a sub-agent session so tool-hook calls
|
|
232
|
+
// to ensureSession() are also suppressed for it.
|
|
233
|
+
subAgentSessions.add(sessionId)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- Session Deleted ---
|
|
238
|
+
if (event.type === "session.deleted") {
|
|
239
|
+
// Same properties.info path as session.created.
|
|
240
|
+
const info = (event.properties as any)?.info
|
|
241
|
+
const sessionId = info?.id
|
|
242
|
+
if (sessionId) {
|
|
243
|
+
toolCounts.delete(sessionId)
|
|
244
|
+
knownSessions.delete(sessionId)
|
|
245
|
+
subAgentSessions.delete(sessionId)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
// ─── User Prompt Capture ──────────────────────────────────────
|
|
252
|
+
// chat.message is called once per user message, before the LLM sees it.
|
|
253
|
+
// input.sessionID is always reliable here (no knownSessions workaround).
|
|
254
|
+
// output.message is typed as UserMessage (role:"user" already guaranteed).
|
|
255
|
+
// output.parts contains TextPart[] with the actual message text.
|
|
256
|
+
|
|
257
|
+
"chat.message": async (input, output) => {
|
|
258
|
+
// Skip sub-agent sessions — they inflate session counts (issue #116)
|
|
259
|
+
if (subAgentSessions.has(input.sessionID)) return
|
|
260
|
+
|
|
261
|
+
const sessionId = input.sessionID
|
|
262
|
+
|
|
263
|
+
// Extract text from parts (type:"text")
|
|
264
|
+
const content = output.parts
|
|
265
|
+
.filter((p) => p.type === "text")
|
|
266
|
+
.map((p) => (p as any).text ?? "")
|
|
267
|
+
.join("\n")
|
|
268
|
+
.trim()
|
|
269
|
+
|
|
270
|
+
// Also fallback to summary if parts yield nothing
|
|
271
|
+
const fallback = !content && output.message.summary
|
|
272
|
+
? `${output.message.summary.title ?? ""}\n${output.message.summary.body ?? ""}`.trim()
|
|
273
|
+
: ""
|
|
274
|
+
|
|
275
|
+
const finalContent = content || fallback
|
|
276
|
+
|
|
277
|
+
// Only capture non-trivial prompts (>10 chars)
|
|
278
|
+
if (finalContent.length > 10) {
|
|
279
|
+
await ensureSession(sessionId)
|
|
280
|
+
await engramFetch("/prompts", {
|
|
281
|
+
method: "POST",
|
|
282
|
+
body: {
|
|
283
|
+
session_id: sessionId,
|
|
284
|
+
content: stripPrivateTags(truncate(finalContent, 2000)),
|
|
285
|
+
project,
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
// ─── Tool Execution Hook ─────────────────────────────────────
|
|
292
|
+
// Count tool calls per session (for session end stats).
|
|
293
|
+
// Also ensures the session exists — handles plugin reload / reconnect.
|
|
294
|
+
// Passive capture: when a Task tool completes, POST its output to
|
|
295
|
+
// the passive capture endpoint so the server extracts learnings.
|
|
296
|
+
|
|
297
|
+
"tool.execute.after": async (input, output) => {
|
|
298
|
+
if (ENGRAM_TOOLS.has(input.tool.toLowerCase())) return
|
|
299
|
+
|
|
300
|
+
// input.sessionID comes from OpenCode — always available
|
|
301
|
+
const sessionId = input.sessionID
|
|
302
|
+
if (sessionId) {
|
|
303
|
+
await ensureSession(sessionId)
|
|
304
|
+
toolCounts.set(sessionId, (toolCounts.get(sessionId) ?? 0) + 1)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Passive capture: extract learnings from Task tool output
|
|
308
|
+
// (OpenCode reports the tool name in lowercase: "task")
|
|
309
|
+
if (input.tool.toLowerCase() === "task" && output && sessionId) {
|
|
310
|
+
const text = typeof output === "string" ? output : JSON.stringify(output)
|
|
311
|
+
if (text.length > 50) {
|
|
312
|
+
await engramFetch("/observations/passive", {
|
|
313
|
+
method: "POST",
|
|
314
|
+
body: {
|
|
315
|
+
session_id: sessionId,
|
|
316
|
+
content: stripPrivateTags(text),
|
|
317
|
+
project,
|
|
318
|
+
source: "task-complete",
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
// ─── System Prompt: Always-on memory instructions ──────────
|
|
326
|
+
// Injects MEMORY_INSTRUCTIONS into the system prompt of every message.
|
|
327
|
+
// This ensures the agent ALWAYS knows about Engram, even after compaction.
|
|
328
|
+
//
|
|
329
|
+
// We append to the last existing system entry instead of pushing a new one.
|
|
330
|
+
// Some models (Qwen3.5, Mistral/Ministral via llama.cpp) reject multiple
|
|
331
|
+
// system messages — their Jinja chat templates only allow a single system
|
|
332
|
+
// block at the beginning. By concatenating, we avoid adding extra system
|
|
333
|
+
// messages that would break these models. See: GitHub issue #23.
|
|
334
|
+
|
|
335
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
336
|
+
if (output.system.length > 0) {
|
|
337
|
+
output.system[output.system.length - 1] += "\n\n" + MEMORY_INSTRUCTIONS
|
|
338
|
+
} else {
|
|
339
|
+
output.system.push(MEMORY_INSTRUCTIONS)
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
// ─── Compaction Hook: Persist memory + inject context ──────────
|
|
344
|
+
// Compaction is triggered by the system (not the agent) when context
|
|
345
|
+
// gets too long. The old agent "dies" and a new one starts with the
|
|
346
|
+
// compacted summary. This is our chance to:
|
|
347
|
+
// 1. Auto-save a session checkpoint (the agent can't do this itself)
|
|
348
|
+
// 2. Inject context from previous sessions into the compaction prompt
|
|
349
|
+
// 3. Tell the compressor to remind the new agent to save memories
|
|
350
|
+
|
|
351
|
+
"experimental.session.compacting": async (input, output) => {
|
|
352
|
+
if (input.sessionID) {
|
|
353
|
+
await ensureSession(input.sessionID)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Inject context from previous sessions
|
|
357
|
+
const data = await engramFetch(
|
|
358
|
+
`/context?project=${encodeURIComponent(project)}`
|
|
359
|
+
)
|
|
360
|
+
if (data?.context) {
|
|
361
|
+
output.context.push(data.context)
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Tell the compressor to instruct the new agent to persist the
|
|
365
|
+
// compacted summary to Engram. The new agent reads the compacted
|
|
366
|
+
// summary and this instruction, then saves it as a session summary.
|
|
367
|
+
output.context.push(
|
|
368
|
+
`CRITICAL INSTRUCTION FOR COMPACTED SUMMARY:\n` +
|
|
369
|
+
`The agent has access to Engram persistent memory via MCP tools.\n` +
|
|
370
|
+
`You MUST include the following instruction at the TOP of the compacted summary:\n\n` +
|
|
371
|
+
`"FIRST ACTION REQUIRED: Call mem_session_summary with the content of this compacted summary. ` +
|
|
372
|
+
`Use project '${project}' if the tool accepts it. This preserves what was accomplished before compaction. ` +
|
|
373
|
+
`Do this BEFORE any other work. After that, delegate to the global engram subagent to recover only the relevant previous context."\n\n` +
|
|
374
|
+
`This is NOT optional. Without this, everything done before compaction is lost from memory.`
|
|
375
|
+
)
|
|
376
|
+
},
|
|
377
|
+
}
|
|
378
|
+
}
|