ostacky 0.7.0 → 0.7.2
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 +14 -11
- package/assets/agents/ostacky.md +18 -11
- package/assets/commands/install-stack.md +20 -1
- package/assets/mcp/ostacky-controller/index.js +523 -120
- package/assets/mcp/ostacky-controller/package.json +1 -1
- package/assets/plugins/engram.ts +82 -8
- package/assets/skills/graceful-degradation/SKILL.md +4 -0
- package/dist/cli.js +370 -158
- package/manifest.json +29 -29
- package/package.json +1 -1
package/assets/plugins/engram.ts
CHANGED
|
@@ -15,12 +15,26 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
18
|
+
import { join, dirname, basename } from "path"
|
|
19
|
+
import { readFileSync, writeFileSync, renameSync, mkdirSync } from "fs"
|
|
18
20
|
|
|
19
21
|
// ─── Configuration ───────────────────────────────────────────────────────────
|
|
20
22
|
|
|
21
23
|
const ENGRAM_PORT = parseInt(process.env.ENGRAM_PORT ?? "7437")
|
|
22
24
|
const ENGRAM_URL = `http://127.0.0.1:${ENGRAM_PORT}`
|
|
23
|
-
|
|
25
|
+
// C3/H2 fix: resolve ENGRAM_BIN per ctx.directory with win32 .exe and absolute fallback
|
|
26
|
+
function resolveEngramBin(directory: string): string {
|
|
27
|
+
if (process.env.ENGRAM_BIN) {
|
|
28
|
+
const p = process.env.ENGRAM_BIN
|
|
29
|
+
const isAbs = p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p)
|
|
30
|
+
return isAbs ? p : join(directory, p)
|
|
31
|
+
}
|
|
32
|
+
const which = Bun.which("engram")
|
|
33
|
+
if (which) return which
|
|
34
|
+
const suffix = process.platform === "win32" ? ".exe" : ""
|
|
35
|
+
return join(directory, ".opencode", "tools", "engram", "bin", `engram${suffix}`)
|
|
36
|
+
}
|
|
37
|
+
// ENGRAM_BIN eliminado: reemplazado por resolveEngramBin(ctx.directory) que maneja .exe+absolutización correctamente
|
|
24
38
|
|
|
25
39
|
// Engram's own MCP tools — don't count these as "tool calls" for session stats
|
|
26
40
|
const ENGRAM_TOOLS = new Set([
|
|
@@ -171,12 +185,12 @@ function extractProjectName(directory: string): string {
|
|
|
171
185
|
const result = Bun.spawnSync(["git", "-C", directory, "rev-parse", "--show-toplevel"])
|
|
172
186
|
if (result.exitCode === 0) {
|
|
173
187
|
const root = result.stdout?.toString().trim()
|
|
174
|
-
if (root) return root.
|
|
188
|
+
if (root) return basename(root.replace(/\\/g, "/")) ?? "unknown"
|
|
175
189
|
}
|
|
176
190
|
} catch {}
|
|
177
191
|
|
|
178
|
-
// Final fallback: cwd basename
|
|
179
|
-
return directory.
|
|
192
|
+
// Final fallback: cwd basename (cross-platform)
|
|
193
|
+
return basename(directory.replace(/\\/g, "/")) ?? "unknown"
|
|
180
194
|
}
|
|
181
195
|
|
|
182
196
|
function truncate(str: string, max: number): string {
|
|
@@ -197,7 +211,8 @@ function stripPrivateTags(str: string): string {
|
|
|
197
211
|
// ─── Plugin Export ───────────────────────────────────────────────────────────
|
|
198
212
|
|
|
199
213
|
export const Engram: Plugin = async (ctx) => {
|
|
200
|
-
|
|
214
|
+
// T4: basename multiplataforma — split("/") producía keys basura con backslashes en Windows nativo
|
|
215
|
+
const oldProject = basename(ctx.directory.replace(/\\/g, "/")) ?? "unknown"
|
|
201
216
|
const project = extractProjectName(ctx.directory)
|
|
202
217
|
|
|
203
218
|
// Track tool counts per session (in-memory only, not critical)
|
|
@@ -236,11 +251,12 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
236
251
|
})
|
|
237
252
|
}
|
|
238
253
|
|
|
239
|
-
// Try to start engram server if not running
|
|
254
|
+
// Try to start engram server if not running — use per-directory resolved bin (win32 .exe + absolute)
|
|
255
|
+
const engramBin = resolveEngramBin(ctx.directory)
|
|
240
256
|
const running = await isEngramRunning()
|
|
241
257
|
if (!running) {
|
|
242
258
|
try {
|
|
243
|
-
Bun.spawn([
|
|
259
|
+
Bun.spawn([engramBin, "serve"], {
|
|
244
260
|
stdout: "ignore",
|
|
245
261
|
stderr: "ignore",
|
|
246
262
|
stdin: "ignore",
|
|
@@ -268,7 +284,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
268
284
|
const manifestFile = `${ctx.directory}/.engram/manifest.json`
|
|
269
285
|
const file = Bun.file(manifestFile)
|
|
270
286
|
if (await file.exists()) {
|
|
271
|
-
Bun.spawn([
|
|
287
|
+
Bun.spawn([engramBin, "sync", "--import"], {
|
|
272
288
|
cwd: ctx.directory,
|
|
273
289
|
stdout: "ignore",
|
|
274
290
|
stderr: "ignore",
|
|
@@ -513,6 +529,64 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
513
529
|
await ensureSession(input.sessionID)
|
|
514
530
|
}
|
|
515
531
|
|
|
532
|
+
// C3: Compaction fallback file — write directly to same anchor as controller's get_handoff
|
|
533
|
+
// Resolves statePath from opencode.json (local) or global config, default .opencode/ostacky-state.json
|
|
534
|
+
try {
|
|
535
|
+
let statePath: string | null = null
|
|
536
|
+
// 1) env var if set
|
|
537
|
+
if (process.env.OSTACKY_STATE_PATH) {
|
|
538
|
+
statePath = process.env.OSTACKY_STATE_PATH
|
|
539
|
+
}
|
|
540
|
+
// 2) try local opencode.json / jsonc in project
|
|
541
|
+
if (!statePath) {
|
|
542
|
+
const candidates = [join(ctx.directory, "opencode.json"), join(ctx.directory, "opencode.jsonc")]
|
|
543
|
+
// also try global config (XDG / APPDATA)
|
|
544
|
+
try {
|
|
545
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? ""
|
|
546
|
+
if (home) {
|
|
547
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? join(home, ".config")
|
|
548
|
+
candidates.push(join(xdg, "opencode", "opencode.json"))
|
|
549
|
+
candidates.push(join(xdg, "opencode", "opencode.jsonc"))
|
|
550
|
+
if (process.platform === "win32" && process.env.APPDATA) {
|
|
551
|
+
candidates.push(join(process.env.APPDATA, "opencode", "opencode.json"))
|
|
552
|
+
candidates.push(join(process.env.APPDATA, "opencode", "opencode.jsonc"))
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
} catch {}
|
|
556
|
+
for (const cand of candidates) {
|
|
557
|
+
try {
|
|
558
|
+
const raw = readFileSync(cand, "utf-8")
|
|
559
|
+
// strip // and /* */ comments for jsonc
|
|
560
|
+
let j = raw
|
|
561
|
+
.replace(/\/\/.*$/gm, "")
|
|
562
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
563
|
+
.replace(/,\s*([}\]])/g, "$1")
|
|
564
|
+
const cfg = JSON.parse(j)
|
|
565
|
+
const envPath = (cfg as any)?.mcp?.["ostacky-controller"]?.environment?.OSTACKY_STATE_PATH
|
|
566
|
+
if (typeof envPath === "string" && envPath) {
|
|
567
|
+
statePath = envPath
|
|
568
|
+
break
|
|
569
|
+
}
|
|
570
|
+
} catch {}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (!statePath) statePath = join(ctx.directory, ".opencode", "ostacky-state.json")
|
|
574
|
+
const fallbackPath = join(dirname(statePath), ".ostacky-handoff-compaction.json")
|
|
575
|
+
try { mkdirSync(dirname(fallbackPath), { recursive: true }) } catch {}
|
|
576
|
+
const payload = {
|
|
577
|
+
summary: `Compaction fallback for session ${input.sessionID ?? "unknown"} — project ${project}`,
|
|
578
|
+
nextSteps: [] as string[],
|
|
579
|
+
pendingTasks: [] as string[],
|
|
580
|
+
ts: Date.now(),
|
|
581
|
+
contextSnippet: output.context?.slice(0, 2).join("\n\n").slice(0, 1000) ?? "",
|
|
582
|
+
}
|
|
583
|
+
const tmp = `${fallbackPath}.tmp.${process.pid}`
|
|
584
|
+
writeFileSync(tmp, JSON.stringify(payload, null, 2), "utf-8")
|
|
585
|
+
renameSync(tmp, fallbackPath)
|
|
586
|
+
} catch {
|
|
587
|
+
// fallback is best-effort — never crash compacting
|
|
588
|
+
}
|
|
589
|
+
|
|
516
590
|
// Inject context from previous sessions
|
|
517
591
|
const data = await engramFetch(
|
|
518
592
|
`/context?project=${encodeURIComponent(project)}`
|
|
@@ -157,6 +157,10 @@ Modo básico: sin validación de edits, sin memoria persistente, sin análisis e
|
|
|
157
157
|
¿Continuar o cancelar?
|
|
158
158
|
```
|
|
159
159
|
|
|
160
|
+
## Handoff Fallback (compaction)
|
|
161
|
+
|
|
162
|
+
Si el controller hizo `set_handoff` o el plugin escribió el fallback `dirname(OSTACKY_STATE_PATH)/.ostacky-handoff-compaction.json` antes de compaction, el próximo agente **debe** llamar `get_handoff` al inicio. `get_handoff` primero chequea `lastHandoff` en memoria y si es `null` lee el archivo fallback (mismo ancla que el writer). `clear_handoff` borra ambos. `cleanupTmpFiles` solo borra ese archivo si `ts >24h`. Ver `assets/plugins/engram.ts:experimental.session.compacting` y `assets/mcp/ostacky-controller/index.js:get_handoff`.
|
|
163
|
+
|
|
160
164
|
## Recovery After Degradation
|
|
161
165
|
|
|
162
166
|
When a tool becomes available again during the session:
|