ostacky 0.7.1 → 0.7.3
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 +18 -11
- package/assets/agents/ostacky.md +44 -14
- package/assets/commands/install-stack.md +20 -1
- package/assets/mcp/ostacky-controller/index.js +1294 -147
- package/assets/mcp/ostacky-controller/package.json +1 -1
- package/assets/plugins/engram.ts +118 -8
- package/assets/plugins/ostacky-guard.ts +131 -0
- package/assets/skills/execution-mode-evaluation/SKILL.md +9 -1
- package/assets/skills/graceful-degradation/SKILL.md +13 -0
- package/assets/skills/using-git-worktrees/SKILL.md +8 -0
- package/dist/cli.js +479 -138
- package/manifest.json +31 -31
- 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 {
|
|
@@ -194,10 +208,51 @@ function stripPrivateTags(str: string): string {
|
|
|
194
208
|
return str.replace(/<private>[\s\S]*?<\/private>/gi, "[REDACTED]").trim()
|
|
195
209
|
}
|
|
196
210
|
|
|
211
|
+
function stripJsoncComments(text: string): string {
|
|
212
|
+
let result = ""
|
|
213
|
+
let i = 0
|
|
214
|
+
let inString = false
|
|
215
|
+
while (i < text.length) {
|
|
216
|
+
const char = text[i]
|
|
217
|
+
const next = text[i + 1]
|
|
218
|
+
if (inString) {
|
|
219
|
+
if (char === "\\") {
|
|
220
|
+
result += char + (next ?? "")
|
|
221
|
+
i += 2
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
224
|
+
if (char === '"') inString = false
|
|
225
|
+
result += char
|
|
226
|
+
i++
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
if (char === '"') {
|
|
230
|
+
inString = true
|
|
231
|
+
result += char
|
|
232
|
+
i++
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
if (char === "/" && next === "/") {
|
|
236
|
+
while (i < text.length && text[i] !== "\n") i++
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
if (char === "/" && next === "*") {
|
|
240
|
+
i += 2
|
|
241
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++
|
|
242
|
+
i += 2
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
result += char
|
|
246
|
+
i++
|
|
247
|
+
}
|
|
248
|
+
return result.replace(/,\s*([}\]])/g, "$1")
|
|
249
|
+
}
|
|
250
|
+
|
|
197
251
|
// ─── Plugin Export ───────────────────────────────────────────────────────────
|
|
198
252
|
|
|
199
253
|
export const Engram: Plugin = async (ctx) => {
|
|
200
|
-
|
|
254
|
+
// T4: basename multiplataforma — split("/") producía keys basura con backslashes en Windows nativo
|
|
255
|
+
const oldProject = basename(ctx.directory.replace(/\\/g, "/")) ?? "unknown"
|
|
201
256
|
const project = extractProjectName(ctx.directory)
|
|
202
257
|
|
|
203
258
|
// Track tool counts per session (in-memory only, not critical)
|
|
@@ -236,11 +291,12 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
236
291
|
})
|
|
237
292
|
}
|
|
238
293
|
|
|
239
|
-
// Try to start engram server if not running
|
|
294
|
+
// Try to start engram server if not running — use per-directory resolved bin (win32 .exe + absolute)
|
|
295
|
+
const engramBin = resolveEngramBin(ctx.directory)
|
|
240
296
|
const running = await isEngramRunning()
|
|
241
297
|
if (!running) {
|
|
242
298
|
try {
|
|
243
|
-
Bun.spawn([
|
|
299
|
+
Bun.spawn([engramBin, "serve"], {
|
|
244
300
|
stdout: "ignore",
|
|
245
301
|
stderr: "ignore",
|
|
246
302
|
stdin: "ignore",
|
|
@@ -268,7 +324,7 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
268
324
|
const manifestFile = `${ctx.directory}/.engram/manifest.json`
|
|
269
325
|
const file = Bun.file(manifestFile)
|
|
270
326
|
if (await file.exists()) {
|
|
271
|
-
Bun.spawn([
|
|
327
|
+
Bun.spawn([engramBin, "sync", "--import"], {
|
|
272
328
|
cwd: ctx.directory,
|
|
273
329
|
stdout: "ignore",
|
|
274
330
|
stderr: "ignore",
|
|
@@ -513,6 +569,60 @@ export const Engram: Plugin = async (ctx) => {
|
|
|
513
569
|
await ensureSession(input.sessionID)
|
|
514
570
|
}
|
|
515
571
|
|
|
572
|
+
// C3: Compaction fallback file — write directly to same anchor as controller's get_handoff
|
|
573
|
+
// Resolves statePath from opencode.json (local) or global config, default .opencode/ostacky-state.json
|
|
574
|
+
try {
|
|
575
|
+
let statePath: string | null = null
|
|
576
|
+
// 1) env var if set
|
|
577
|
+
if (process.env.OSTACKY_STATE_PATH) {
|
|
578
|
+
statePath = process.env.OSTACKY_STATE_PATH
|
|
579
|
+
}
|
|
580
|
+
// 2) try local opencode.json / jsonc in project
|
|
581
|
+
if (!statePath) {
|
|
582
|
+
const candidates = [join(ctx.directory, "opencode.json"), join(ctx.directory, "opencode.jsonc")]
|
|
583
|
+
// also try global config (XDG / APPDATA)
|
|
584
|
+
try {
|
|
585
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? ""
|
|
586
|
+
if (home) {
|
|
587
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? join(home, ".config")
|
|
588
|
+
candidates.push(join(xdg, "opencode", "opencode.json"))
|
|
589
|
+
candidates.push(join(xdg, "opencode", "opencode.jsonc"))
|
|
590
|
+
if (process.platform === "win32" && process.env.APPDATA) {
|
|
591
|
+
candidates.push(join(process.env.APPDATA, "opencode", "opencode.json"))
|
|
592
|
+
candidates.push(join(process.env.APPDATA, "opencode", "opencode.jsonc"))
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
} catch {}
|
|
596
|
+
for (const cand of candidates) {
|
|
597
|
+
try {
|
|
598
|
+
const raw = readFileSync(cand, "utf-8")
|
|
599
|
+
const j = stripJsoncComments(raw)
|
|
600
|
+
const cfg = JSON.parse(j)
|
|
601
|
+
const envPath = (cfg as any)?.mcp?.["ostacky-controller"]?.environment?.OSTACKY_STATE_PATH
|
|
602
|
+
if (typeof envPath === "string" && envPath) {
|
|
603
|
+
statePath = envPath
|
|
604
|
+
break
|
|
605
|
+
}
|
|
606
|
+
} catch {}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (!statePath) statePath = join(ctx.directory, ".opencode", "ostacky-state.json")
|
|
610
|
+
const fallbackPath = join(dirname(statePath), ".ostacky-handoff-compaction.json")
|
|
611
|
+
try { mkdirSync(dirname(fallbackPath), { recursive: true }) } catch {}
|
|
612
|
+
const payload = {
|
|
613
|
+
summary: `Compaction fallback for session ${input.sessionID ?? "unknown"} — project ${project}`,
|
|
614
|
+
nextSteps: [] as string[],
|
|
615
|
+
pendingTasks: [] as string[],
|
|
616
|
+
ts: Date.now(),
|
|
617
|
+
contextSnippet: output.context?.slice(0, 2).join("\n\n").slice(0, 1000) ?? "",
|
|
618
|
+
}
|
|
619
|
+
const tmp = `${fallbackPath}.tmp.${process.pid}`
|
|
620
|
+
writeFileSync(tmp, JSON.stringify(payload, null, 2), "utf-8")
|
|
621
|
+
renameSync(tmp, fallbackPath)
|
|
622
|
+
} catch {
|
|
623
|
+
// fallback is best-effort — never crash compacting
|
|
624
|
+
}
|
|
625
|
+
|
|
516
626
|
// Inject context from previous sessions
|
|
517
627
|
const data = await engramFetch(
|
|
518
628
|
`/context?project=${encodeURIComponent(project)}`
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ostacky Guard — plugin hermano de Engram
|
|
3
|
+
*
|
|
4
|
+
* Bloquea lecturas sensibles sin autorización previa y bloquea cualquier tool
|
|
5
|
+
* cuando el controller está en estado PENDING.
|
|
6
|
+
*
|
|
7
|
+
* Se registra en `tool.execute.before` y lanza Error si la operación no está
|
|
8
|
+
* permitida. OpenCode aborta el tool antes de tocar disco.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
12
|
+
import { readFileSync, existsSync } from "node:fs"
|
|
13
|
+
import { join, dirname, basename, resolve, relative } from "node:path"
|
|
14
|
+
|
|
15
|
+
const SENSITIVE_DEFAULT = [
|
|
16
|
+
"**/.env*",
|
|
17
|
+
"**/.secrets/**",
|
|
18
|
+
"**/*.pem",
|
|
19
|
+
"**/*.key",
|
|
20
|
+
"**/.aws/**",
|
|
21
|
+
"**/.ssh/**",
|
|
22
|
+
"**/credentials.json",
|
|
23
|
+
"**/.npmrc",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
function getEnvPatterns(): string[] {
|
|
27
|
+
const raw = process.env.OSTACKY_SENSITIVE_PATTERNS
|
|
28
|
+
if (!raw) return SENSITIVE_DEFAULT
|
|
29
|
+
return raw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isSensitive(filePath: string, patterns: string[]): boolean {
|
|
33
|
+
if (!filePath) return false
|
|
34
|
+
const lower = filePath.toLowerCase()
|
|
35
|
+
if (lower.endsWith(".env.example") || lower.endsWith(".env.template") || lower.endsWith(".env.sample")) return false
|
|
36
|
+
for (const pat of patterns) {
|
|
37
|
+
if (pat.includes(".env") && lower.split("/").pop()!.startsWith(".env")) return true
|
|
38
|
+
if (pat.includes(".secrets") && lower.includes(".secrets")) return true
|
|
39
|
+
if (pat.includes("*.pem") && lower.endsWith(".pem")) return true
|
|
40
|
+
if (pat.includes("*.key") && lower.endsWith(".key")) return true
|
|
41
|
+
if (pat.includes(".aws") && lower.includes(".aws")) return true
|
|
42
|
+
if (pat.includes(".ssh") && lower.includes(".ssh")) return true
|
|
43
|
+
if (pat.includes("credentials.json") && lower.endsWith("credentials.json")) return true
|
|
44
|
+
if (pat.includes(".npmrc") && lower.endsWith(".npmrc")) return true
|
|
45
|
+
}
|
|
46
|
+
if (/\.(pem|key)$/i.test(filePath)) return true
|
|
47
|
+
if (filePath.includes(".env")) {
|
|
48
|
+
const base = filePath.split("/").pop() || ""
|
|
49
|
+
if (base.startsWith(".env")) return true
|
|
50
|
+
}
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getStatePath(directory: string): string {
|
|
55
|
+
if (process.env.OSTACKY_STATE_PATH) return process.env.OSTACKY_STATE_PATH
|
|
56
|
+
// try opencode.json
|
|
57
|
+
const candidates = [join(directory, "opencode.json"), join(directory, "opencode.jsonc")]
|
|
58
|
+
for (const cand of candidates) {
|
|
59
|
+
try {
|
|
60
|
+
const raw = readFileSync(cand, "utf-8")
|
|
61
|
+
// naive parse, ignore comments
|
|
62
|
+
const json = JSON.parse(raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""))
|
|
63
|
+
const envPath = (json as any)?.mcp?.["ostacky-controller"]?.environment?.OSTACKY_STATE_PATH
|
|
64
|
+
if (typeof envPath === "string" && envPath) return envPath
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
return join(directory, ".opencode", "ostacky-state.json")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readState(directory: string): any | null {
|
|
71
|
+
const p = getStatePath(directory)
|
|
72
|
+
try {
|
|
73
|
+
const raw = readFileSync(p, "utf-8")
|
|
74
|
+
return JSON.parse(raw)
|
|
75
|
+
} catch {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const OstackyGuard: Plugin = async (ctx) => {
|
|
81
|
+
const patterns = getEnvPatterns()
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
"tool.execute.before": async (input, output) => {
|
|
85
|
+
const tool = (input as any).tool as string
|
|
86
|
+
const args = (input as any).args as any
|
|
87
|
+
|
|
88
|
+
// Extraer filePath de args según tool
|
|
89
|
+
let filePath: string | undefined
|
|
90
|
+
if (tool === "read" || tool === "read_mcp_resource" || tool === "grep" || tool === "glob") {
|
|
91
|
+
filePath = args?.filePath || args?.path || args?.pattern || args?.uri || ""
|
|
92
|
+
// Para grep, el pattern puede ser contenido, no file; intentar detectar file
|
|
93
|
+
if (tool === "grep" && args?.include) filePath = args.include
|
|
94
|
+
if (tool === "read_mcp_resource" && typeof args?.uri === "string") {
|
|
95
|
+
// uri puede ser file:// o path
|
|
96
|
+
try {
|
|
97
|
+
const u = args.uri as string
|
|
98
|
+
if (u.startsWith("file://")) filePath = u.slice(7)
|
|
99
|
+
else if (u.includes("/") ) filePath = u
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 10.2: Guard genérico de PENDING — bloquear cualquier tool no-controller cuando estado es PENDING
|
|
105
|
+
const state = readState(ctx.directory)
|
|
106
|
+
const pendingStates = ["ROUTE_DECISION_PENDING", "EXECUTION_DECISION_PENDING", "CLARIFICATION_PENDING"]
|
|
107
|
+
if (state && pendingStates.includes(state.state)) {
|
|
108
|
+
// Permitir solo tools del controller que desbloquean
|
|
109
|
+
const allowedTools = ["consume_route_decision", "consume_execution_decision", "record_clarification", "abandon", "check_file_access", "consume_file_access_decision", "record_user_confirmation"]
|
|
110
|
+
const isControllerTool = tool.startsWith("ostacky-controller_") || allowedTools.some((t) => tool.includes(t))
|
|
111
|
+
if (!isControllerTool) {
|
|
112
|
+
// Bloquear cualquier tool no-controller (Read/Grep/Glob/Bash/Edit/Write) cuando está en PENDING — hard gate genérico
|
|
113
|
+
throw new Error(`BLOCKED: call consume_* first — controller is in ${state.state}. Use consume_route_decision / consume_execution_decision / record_clarification to proceed.`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 9.1: Guard de credenciales
|
|
118
|
+
if (filePath && isSensitive(filePath, patterns)) {
|
|
119
|
+
const s = readState(ctx.directory)
|
|
120
|
+
const allowed = s?.allowedFiles?.[filePath]
|
|
121
|
+
if (!allowed) {
|
|
122
|
+
const denied = s?.deniedFiles?.[filePath]
|
|
123
|
+
if (denied) {
|
|
124
|
+
throw new Error(`BLOCKED: File ${filePath} requires check_file_access (previously denied)`)
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`BLOCKED: File ${filePath} requires check_file_access`)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -112,6 +112,7 @@ Para cada fase de `tasks.md`, evaluar intra-fase:
|
|
|
112
112
|
"reasons": ["razón principal", "razón secundaria"],
|
|
113
113
|
"codegraphUsed": ["codegraph_codegraph_explore"],
|
|
114
114
|
"taskCount": <N>,
|
|
115
|
+
"expectedTaskIds": ["T1", "T2", "T3"],
|
|
115
116
|
"sharedFiles": { "src/archivo.ts": ["task1", "task2"] },
|
|
116
117
|
"fileClusters": [["task1", "task2"], ["task3"]],
|
|
117
118
|
"clusterCount": <N>,
|
|
@@ -124,6 +125,9 @@ Para cada fase de `tasks.md`, evaluar intra-fase:
|
|
|
124
125
|
}
|
|
125
126
|
```
|
|
126
127
|
|
|
128
|
+
> **Nota 6.1:** `expectedTaskIds` es **obligatorio** cuando `taskCount>0` — el controller lo exige y rechaza snapshot sin él (excepto `early-exit` con `taskCount<=2` y `codegraphUsed:[]` que es válido sin WARN).
|
|
129
|
+
> Ejemplo early-exit válido: `{"recommendation":"INLINE","reasons":["Cambio pequeño"],"codegraphUsed":[],"taskCount":2,"expectedTaskIds":["T1","T2"],"globalRuleTriggered":"early-exit"}`
|
|
130
|
+
|
|
127
131
|
**Output para el usuario (mostrar en lenguaje natural):**
|
|
128
132
|
|
|
129
133
|
```markdown
|
|
@@ -167,6 +171,8 @@ Para cada fase de `tasks.md`, evaluar intra-fase:
|
|
|
167
171
|
| `sequentialDeps` | Dependencias secuenciales entre tasks |
|
|
168
172
|
| `estLines` | Estimación conservadora |
|
|
169
173
|
| `hasExplicitContract` | `true` si design.md explicita contratos |
|
|
174
|
+
| `expectedTaskIds` | **Obligatorio** cuando `taskCount>0` — gate del controller |
|
|
175
|
+
| `taskCount` | Total tasks, debe coincidir con `expectedTaskIds.length` |
|
|
170
176
|
|
|
171
177
|
**⚠️ Este skill provee ANÁLISIS, no autorización.** El coordinador muestra el snapshot al usuario y pide confirmación en lenguaje natural, luego espera la respuesta.
|
|
172
178
|
|
|
@@ -184,6 +190,8 @@ Para cada fase de `tasks.md`, evaluar intra-fase:
|
|
|
184
190
|
- [ ] Verifiqué deps ENTRE clusters (no solo intra)?
|
|
185
191
|
- [ ] Apliqué reglas en orden (1→2a/2b→3a/3b/3c)?
|
|
186
192
|
- [ ] Anoté `globalRuleTriggered`?
|
|
193
|
+
- [ ] Incluí `expectedTaskIds` (obligatorio cuando `taskCount>0`) y verifiqué que `taskCount == expectedTaskIds.length`?
|
|
187
194
|
- [ ] Si global es inline, ejecuté Paso 3b por fase?
|
|
188
|
-
- [ ] Si global es subagent por clusters (Rule 2b), documenté dispatch?
|
|
195
|
+
- [ ] Si global es subagent por clusters (Rule 2b), documenté dispatch por clusters (máx 3 subagentes, advertir si `clusterCount>3` → oleadas)?
|
|
189
196
|
- [ ] Output es JSON válido con todos los campos del contrato?
|
|
197
|
+
- [ ] Early-exit con `codegraphUsed:[]` y `taskCount<=2` solo cuando realmente es cambio trivial (no genera WARN)?
|
|
@@ -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:
|
|
@@ -166,6 +170,15 @@ When a tool becomes available again during the session:
|
|
|
166
170
|
3. **Resume:** Switch back to normal workflow
|
|
167
171
|
4. **Catch up:** Use the tool to verify recent work
|
|
168
172
|
|
|
173
|
+
## Health via get_metrics y doctor (6.2)
|
|
174
|
+
|
|
175
|
+
- **Con Controller:** usar `get_metrics` como health — expone `degraded`, `diskFreeMB`, `auditSize`, `stateFileSize`, `codegraphBypassCount`, `stateOversizedCount`, `degradedEditsCount`, `sensitiveAccess`. Si `diskFreeMB<100` → ⚠️ Disco casi lleno; si `stateOversizedCount>0` → snapshots perdidos.
|
|
176
|
+
- **Sin Controller:** fallback a `ostacky doctor` (lee `.opencode/ostacky-state.json` sin MCP, verifica locks, tamaños, audit, binarios y `manifest.json` hashes). `doctor` es el fallback a `check:skills` cuando MCP caído.
|
|
177
|
+
- **Sin CodeGraph:** `get_metrics.codegraphBypassCount` incrementa cuando `record_discovery` sin `symbols` y no degraded; `get_audit` marca `inefficient: codegraph bypass` para review.
|
|
178
|
+
- **Sin Engram:** continuar sin memoria; `doctor` no requiere Engram.
|
|
179
|
+
|
|
180
|
+
No usar `skill("engram")` — Engram es MCP server, no skill. Usar `engram_mem_*` tools.
|
|
181
|
+
|
|
169
182
|
## Guardrails
|
|
170
183
|
|
|
171
184
|
### During Degradation
|
|
@@ -201,6 +201,14 @@ Ready to implement auth feature
|
|
|
201
201
|
- Auto-detect and run project setup
|
|
202
202
|
- Verify clean test baseline
|
|
203
203
|
|
|
204
|
+
## Ostacky Worktree Isolation
|
|
205
|
+
|
|
206
|
+
Cada worktree de git es **aislado** para Ostacky:
|
|
207
|
+
|
|
208
|
+
- **State file independiente:** `findProjectRoot()` resuelve el root del worktree vía `git rev-parse --show-toplevel` (cada worktree tiene su propio `.opencode/ostacky-state.json`). Dos worktrees no comparten `statePath`, por lo que `ostacky-controller` no pisa estados entre worktrees.
|
|
209
|
+
- **Lock independiente:** cada worktree tiene su propio `.lock` y `.backup` rotativo, por lo que 3 agentes en 3 worktrees no corrompen el lock del otro.
|
|
210
|
+
- **Verificación:** `tests` con dos tmp dirs simulando worktrees verifican que no comparten `statePath` y que `doctor` reporta OK en cada uno.
|
|
211
|
+
|
|
204
212
|
## Integration
|
|
205
213
|
|
|
206
214
|
**Called by:**
|