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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jorgex-stack",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
4
4
  "description": "Harness multi-agente portable: instala la config JorgeX (agentes, skills, hooks, Engram, MCPs) en Claude Code, Codex CLI y OpenCode",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 = process.env.ENGRAM_BIN ?? Bun.which("engram") ?? "{{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<void> {
149
- if (!sessionId || knownSessions.has(sessionId)) return
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
- knownSessions.add(sessionId)
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([ENGRAM_BIN, "serve"], {
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([ENGRAM_BIN, "sync", "--import"], {
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) => (p as any).text ?? "")
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
- await engramFetch("/prompts", {
281
- method: "POST",
282
- body: {
283
- session_id: sessionId,
284
- content: stripPrivateTags(truncate(finalContent, 2000)),
285
- project,
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
- if (sessionId) {
303
- await ensureSession(sessionId)
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
  }