jorgex-stack 1.0.14 → 1.0.16
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/README.md +42 -0
- package/dist/cli.js +512 -195
- package/package.json +1 -1
- package/stack/modes/programmatic/AGENTS.addendum.md +12 -0
- package/stack/modes/programmatic/agent-delegation.addendum.md +8 -0
- package/stack/modes/programmatic/final-output.schema.json +43 -0
- package/stack/modes/programmatic/orchestrator.addendum.md +15 -0
- package/stack/modes/programmatic/subagent.addendum.md +12 -0
- package/stack/plugins/opencode/engram.ts +54 -27
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!-- jorgex:programmatic-mode -->
|
|
2
|
+
## PROGRAMMATIC MODE
|
|
3
|
+
|
|
4
|
+
- This installation is meant for external orchestrators, CI, or scripts.
|
|
5
|
+
- Answer in English.
|
|
6
|
+
- Keep intermediate output compact and direct.
|
|
7
|
+
- Make the final handoff strict and machine-friendly.
|
|
8
|
+
- When a final response is required, emit one strict JSON object only.
|
|
9
|
+
- Any delegation must live in the JSON `delegations[]` array as a string in the form `agent: work — paths — inputs`; do not emit Markdown delegation lines outside JSON.
|
|
10
|
+
- Required keys: `status`, `decision`, `confidence`, `summary`, `risks`, `next_steps`, `delegations`.
|
|
11
|
+
- `status` is one of `done`, `partial`, `blocked`.
|
|
12
|
+
- `risks`, `next_steps`, and `delegations` are arrays of strings.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<!-- jorgex:programmatic-mode -->
|
|
2
|
+
## Programmatic Mode
|
|
3
|
+
|
|
4
|
+
- Use English only.
|
|
5
|
+
- Keep replies compact and direct.
|
|
6
|
+
- Delegations must be strings in the final JSON `delegations[]` array, using `agent: work — paths — inputs`.
|
|
7
|
+
- Do not emit Markdown delegation lines.
|
|
8
|
+
- Use the strict final JSON handoff.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "Programmatic final output",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": ["status", "decision", "confidence", "summary", "risks", "next_steps", "delegations"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"status": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"enum": ["done", "partial", "blocked"]
|
|
11
|
+
},
|
|
12
|
+
"decision": {
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"confidence": {
|
|
16
|
+
"type": "number",
|
|
17
|
+
"minimum": 0,
|
|
18
|
+
"maximum": 1
|
|
19
|
+
},
|
|
20
|
+
"summary": {
|
|
21
|
+
"type": "string"
|
|
22
|
+
},
|
|
23
|
+
"risks": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"next_steps": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"items": {
|
|
32
|
+
"type": "string"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"delegations": {
|
|
36
|
+
"type": "array",
|
|
37
|
+
"items": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"pattern": "^[A-Za-z0-9][^:\\n]*: [^—\\n]+ — [^—\\n]+ — [^—\\n]+$"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- jorgex:programmatic-mode -->
|
|
2
|
+
## Programmatic Mode
|
|
3
|
+
|
|
4
|
+
- Use English only.
|
|
5
|
+
- Keep reasoning compact and direct.
|
|
6
|
+
- The final assistant response must be exactly one strict JSON object.
|
|
7
|
+
- Do not wrap the final JSON in Markdown fences or prose.
|
|
8
|
+
- Process the final JSON object's `delegations[]` array; each item must be a string in the form `agent: work — paths — inputs`. Ignore Markdown delegation lines outside JSON.
|
|
9
|
+
- Required keys: `status`, `decision`, `confidence`, `summary`, `risks`, `next_steps`, `delegations`.
|
|
10
|
+
- `status` is one of `done`, `partial`, `blocked` and `decision` is a short string.
|
|
11
|
+
- `confidence` is a number between 0 and 1.
|
|
12
|
+
- `summary` is a short string.
|
|
13
|
+
- `risks`, `next_steps`, and `delegations` are arrays of strings.
|
|
14
|
+
|
|
15
|
+
{{CONCURRENCY_RULE}}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!-- jorgex:programmatic-mode -->
|
|
2
|
+
## Programmatic Mode
|
|
3
|
+
|
|
4
|
+
- Use English only.
|
|
5
|
+
- Keep replies compact and direct.
|
|
6
|
+
- Avoid long Markdown reports unless the orchestrator asks for them.
|
|
7
|
+
- Return a terse, structured handoff that is easy to parse.
|
|
8
|
+
- Use the strict final JSON handoff.
|
|
9
|
+
- Any delegation must live in the JSON `delegations[]` array as a string in the form `agent: work — paths — inputs`; do not emit Markdown delegation lines outside JSON.
|
|
10
|
+
- Required keys: `status`, `decision`, `confidence`, `summary`, `risks`, `next_steps`, `delegations`.
|
|
11
|
+
- `status` is one of `done`, `partial`, `blocked`.
|
|
12
|
+
- `risks`, `next_steps`, and `delegations` are arrays of strings.
|
|
@@ -16,12 +16,30 @@
|
|
|
16
16
|
|
|
17
17
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
18
18
|
|
|
19
|
+
declare const Bun: {
|
|
20
|
+
which?: (bin: string) => string | null
|
|
21
|
+
spawnSync: (args: string[]) => { exitCode: number; stdout?: { toString(): string } | string }
|
|
22
|
+
spawn: (args: string[], options?: Record<string, unknown>) => unknown
|
|
23
|
+
file: (path: string) => { exists: () => Promise<boolean> }
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
20
27
|
|
|
21
28
|
const ENGRAM_PORT = parseInt(process.env.ENGRAM_PORT ?? "7437")
|
|
22
29
|
const ENGRAM_URL = `http://127.0.0.1:${ENGRAM_PORT}`
|
|
23
30
|
// "{{ENGRAM_BIN}}" lo resuelve el instalador con el binario detectado (D7).
|
|
24
|
-
const ENGRAM_BIN =
|
|
31
|
+
const ENGRAM_BIN = "{{ENGRAM_BIN}}"
|
|
32
|
+
|
|
33
|
+
export function resolveEngramBin(installerBin = ENGRAM_BIN): string {
|
|
34
|
+
const envBin = process.env.ENGRAM_BIN
|
|
35
|
+
if (envBin) return envBin
|
|
36
|
+
|
|
37
|
+
const bun = globalThis as typeof globalThis & { Bun?: { which?: (bin: string) => string | null } }
|
|
38
|
+
const bunBin = bun.Bun?.which?.("engram")
|
|
39
|
+
if (bunBin) return bunBin
|
|
40
|
+
|
|
41
|
+
return installerBin !== "{{ENGRAM_BIN}}" ? installerBin : "engram"
|
|
42
|
+
}
|
|
25
43
|
|
|
26
44
|
// Engram's own MCP tools — don't count these as "tool calls" for session stats
|
|
27
45
|
const ENGRAM_TOOLS = new Set([
|
|
@@ -60,6 +78,7 @@ async function engramFetch(
|
|
|
60
78
|
headers: opts.body ? { "Content-Type": "application/json" } : undefined,
|
|
61
79
|
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
|
62
80
|
})
|
|
81
|
+
if (!res.ok) return null
|
|
63
82
|
return await res.json()
|
|
64
83
|
} catch {
|
|
65
84
|
// Engram server not running — silently fail
|
|
@@ -126,6 +145,7 @@ function stripPrivateTags(str: string): string {
|
|
|
126
145
|
export const Engram: Plugin = async (ctx) => {
|
|
127
146
|
const oldProject = ctx.directory.split(/[\\/]/).pop() ?? "unknown"
|
|
128
147
|
const project = extractProjectName(ctx.directory)
|
|
148
|
+
const engramBin = resolveEngramBin()
|
|
129
149
|
|
|
130
150
|
// Track tool counts per session (in-memory only, not critical)
|
|
131
151
|
const toolCounts = new Map<string, number>()
|
|
@@ -145,12 +165,12 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
145
165
|
*
|
|
146
166
|
* Silently skips sub-agent sessions (tracked in `subAgentSessions`).
|
|
147
167
|
*/
|
|
148
|
-
async function ensureSession(sessionId: string): Promise<
|
|
149
|
-
if (!sessionId
|
|
168
|
+
async function ensureSession(sessionId: string): Promise<boolean> {
|
|
169
|
+
if (!sessionId) return false
|
|
170
|
+
if (knownSessions.has(sessionId)) return true
|
|
150
171
|
// Do not register sub-agent sessions in Engram (issue #116).
|
|
151
|
-
if (subAgentSessions.has(sessionId)) return
|
|
152
|
-
|
|
153
|
-
await engramFetch("/sessions", {
|
|
172
|
+
if (subAgentSessions.has(sessionId)) return false
|
|
173
|
+
const session = await engramFetch("/sessions", {
|
|
154
174
|
method: "POST",
|
|
155
175
|
body: {
|
|
156
176
|
id: sessionId,
|
|
@@ -158,13 +178,18 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
158
178
|
directory: ctx.directory,
|
|
159
179
|
},
|
|
160
180
|
})
|
|
181
|
+
|
|
182
|
+
if (session === null) return false
|
|
183
|
+
|
|
184
|
+
knownSessions.add(sessionId)
|
|
185
|
+
return true
|
|
161
186
|
}
|
|
162
187
|
|
|
163
188
|
// Try to start engram server if not running
|
|
164
189
|
const running = await isEngramRunning()
|
|
165
190
|
if (!running) {
|
|
166
191
|
try {
|
|
167
|
-
Bun.spawn([
|
|
192
|
+
Bun.spawn([engramBin, "serve"], {
|
|
168
193
|
stdout: "ignore",
|
|
169
194
|
stderr: "ignore",
|
|
170
195
|
stdin: "ignore",
|
|
@@ -192,7 +217,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
192
217
|
const manifestFile = `${ctx.directory}/.engram/manifest.json`
|
|
193
218
|
const file = Bun.file(manifestFile)
|
|
194
219
|
if (await file.exists()) {
|
|
195
|
-
Bun.spawn([
|
|
220
|
+
Bun.spawn([engramBin, "sync", "--import"], {
|
|
196
221
|
cwd: ctx.directory,
|
|
197
222
|
stdout: "ignore",
|
|
198
223
|
stderr: "ignore",
|
|
@@ -206,7 +231,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
206
231
|
return {
|
|
207
232
|
// ─── Event Listeners ───────────────────────────────────────────
|
|
208
233
|
|
|
209
|
-
event: async ({ event }) => {
|
|
234
|
+
event: async ({ event }: { event: { type: string; properties?: unknown } }) => {
|
|
210
235
|
// --- Session Created ---
|
|
211
236
|
if (event.type === "session.created") {
|
|
212
237
|
// Bug fix (#116): session data is nested under event.properties.info,
|
|
@@ -254,7 +279,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
254
279
|
// output.message is typed as UserMessage (role:"user" already guaranteed).
|
|
255
280
|
// output.parts contains TextPart[] with the actual message text.
|
|
256
281
|
|
|
257
|
-
"chat.message": async (input, output) => {
|
|
282
|
+
"chat.message": async (input: any, output: any) => {
|
|
258
283
|
// Skip sub-agent sessions — they inflate session counts (issue #116)
|
|
259
284
|
if (subAgentSessions.has(input.sessionID)) return
|
|
260
285
|
|
|
@@ -262,8 +287,8 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
262
287
|
|
|
263
288
|
// Extract text from parts (type:"text")
|
|
264
289
|
const content = output.parts
|
|
265
|
-
.filter((p) => p.type === "text")
|
|
266
|
-
.map((p) =>
|
|
290
|
+
.filter((p: any) => p.type === "text")
|
|
291
|
+
.map((p: any) => p.text ?? "")
|
|
267
292
|
.join("\n")
|
|
268
293
|
.trim()
|
|
269
294
|
|
|
@@ -276,15 +301,17 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
276
301
|
|
|
277
302
|
// Only capture non-trivial prompts (>10 chars)
|
|
278
303
|
if (finalContent.length > 10) {
|
|
279
|
-
await ensureSession(sessionId)
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
304
|
+
const sessionReady = await ensureSession(sessionId)
|
|
305
|
+
if (sessionReady) {
|
|
306
|
+
await engramFetch("/prompts", {
|
|
307
|
+
method: "POST",
|
|
308
|
+
body: {
|
|
309
|
+
session_id: sessionId,
|
|
310
|
+
content: stripPrivateTags(truncate(finalContent, 2000)),
|
|
311
|
+
project,
|
|
312
|
+
},
|
|
313
|
+
})
|
|
314
|
+
}
|
|
288
315
|
}
|
|
289
316
|
},
|
|
290
317
|
|
|
@@ -294,13 +321,13 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
294
321
|
// Passive capture: when a Task tool completes, POST its output to
|
|
295
322
|
// the passive capture endpoint so the server extracts learnings.
|
|
296
323
|
|
|
297
|
-
"tool.execute.after": async (input, output) => {
|
|
324
|
+
"tool.execute.after": async (input: any, output: any) => {
|
|
298
325
|
if (ENGRAM_TOOLS.has(input.tool.toLowerCase())) return
|
|
299
326
|
|
|
300
327
|
// input.sessionID comes from OpenCode — always available
|
|
301
328
|
const sessionId = input.sessionID
|
|
302
|
-
|
|
303
|
-
|
|
329
|
+
const sessionReady = sessionId ? await ensureSession(sessionId) : false
|
|
330
|
+
if (sessionReady && sessionId) {
|
|
304
331
|
toolCounts.set(sessionId, (toolCounts.get(sessionId) ?? 0) + 1)
|
|
305
332
|
}
|
|
306
333
|
|
|
@@ -308,7 +335,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
308
335
|
// (OpenCode reports the tool name in lowercase: "task")
|
|
309
336
|
if (input.tool.toLowerCase() === "task" && output && sessionId) {
|
|
310
337
|
const text = typeof output === "string" ? output : JSON.stringify(output)
|
|
311
|
-
if (text.length > 50) {
|
|
338
|
+
if (text.length > 50 && sessionReady) {
|
|
312
339
|
await engramFetch("/observations/passive", {
|
|
313
340
|
method: "POST",
|
|
314
341
|
body: {
|
|
@@ -332,7 +359,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
332
359
|
// block at the beginning. By concatenating, we avoid adding extra system
|
|
333
360
|
// messages that would break these models. See: GitHub issue #23.
|
|
334
361
|
|
|
335
|
-
"experimental.chat.system.transform": async (_input, output) => {
|
|
362
|
+
"experimental.chat.system.transform": async (_input: any, output: any) => {
|
|
336
363
|
if (output.system.length > 0) {
|
|
337
364
|
output.system[output.system.length - 1] += "\n\n" + MEMORY_INSTRUCTIONS
|
|
338
365
|
} else {
|
|
@@ -348,7 +375,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
348
375
|
// 2. Inject context from previous sessions into the compaction prompt
|
|
349
376
|
// 3. Tell the compressor to remind the new agent to save memories
|
|
350
377
|
|
|
351
|
-
"experimental.session.compacting": async (input, output) => {
|
|
378
|
+
"experimental.session.compacting": async (input: any, output: any) => {
|
|
352
379
|
if (input.sessionID) {
|
|
353
380
|
await ensureSession(input.sessionID)
|
|
354
381
|
}
|