ostacky 0.10.0 → 0.10.1

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.
@@ -6,7 +6,7 @@
6
6
  * MCP queda thin solo para observabilidad (get_*).
7
7
  *
8
8
  * Single source security: mirrors generados desde src/security.ts via scripts/sync-controller-core.ts
9
- * Cache único: getDiscoverySnapshot es único entrypoint.
9
+ * Discovery: `codegraph_explore` completado; no hay cache de archivo.
10
10
  * Tiered cache-friendly: isTrivial sin reemplazar system[0], suffix hint + SKIP.
11
11
  * CodeGraph preventivo: bloquea Read/Grep masivo sin Discovery hit.
12
12
  */
@@ -228,25 +228,6 @@ function isPathInsideProject(filePath: string, directory: string): boolean {
228
228
  } catch { return false }
229
229
  }
230
230
 
231
- function getDiscoveryCacheHit(directory: string): boolean {
232
- try {
233
- const cacheDir = join(directory, ".opencode", "cache", "codegraph")
234
- if (!existsSync(cacheDir)) return false
235
- const files = readdirSync(cacheDir).filter((f: string) => f.startsWith("discovery-"))
236
- if (files.length === 0) return false
237
- // check if any file is recent (<1h) and valid
238
- const now = Date.now()
239
- for (const f of files) {
240
- try {
241
- const raw = readFileSync(join(cacheDir, f), "utf-8")
242
- const data = JSON.parse(raw)
243
- if (typeof data.ts === "number" && now - data.ts < 60*60*1000) return true
244
- } catch {}
245
- }
246
- return false
247
- } catch { return false }
248
- }
249
-
250
231
  function isCodegraphAvailable(directory: string): boolean {
251
232
  // If codegraph binary exists, assume OK. Otherwise fallback allows Read.
252
233
  try {
@@ -259,6 +240,217 @@ function isCodegraphAvailable(directory: string): boolean {
259
240
  } catch { return false }
260
241
  }
261
242
 
243
+ /**
244
+ * ¿Este read/grep/glob esta sujeto al gate de discovery?
245
+ * Exenciones: docs/json/md, openspec/changes|specs, grep de literales, y archivos ya
246
+ * trackeados (refresh honesto post-complete necesita Read fresco sin Discovery hit).
247
+ */
248
+ export function isCodeReadGated(input: {
249
+ isCodeFile: boolean
250
+ isLiteralGrep: boolean
251
+ isSpecTasksPath: boolean
252
+ isDocsPath: boolean
253
+ isTaskTracked: boolean
254
+ }): boolean {
255
+ return (
256
+ input.isCodeFile &&
257
+ !input.isLiteralGrep &&
258
+ !input.isSpecTasksPath &&
259
+ !input.isDocsPath &&
260
+ !input.isTaskTracked
261
+ )
262
+ }
263
+
264
+ /**
265
+ * Predicado puro del gate de CodeGraph: ¿bloquear esta lectura de codigo?
266
+ *
267
+ * Extraido a funcion para poder testear el INVARIANTE, no el mensaje. El test anterior hacia
268
+ * `expect(src).toContain("Usa getDiscoverySnapshot primero")`: verificaba que el texto existiera
269
+ * y pasaba verde con el gate inescapable adentro (fix-codegraph-gate).
270
+ */
271
+ export function shouldBlockCodeRead(input: {
272
+ isCodeFile: boolean
273
+ isLiteralGrep: boolean
274
+ isSpecTasksPath: boolean
275
+ isDocsPath: boolean
276
+ isTaskTracked: boolean
277
+ codegraphOk: boolean
278
+ hasDiscoveryHit: boolean
279
+ }): boolean {
280
+ return isCodeReadGated(input) && input.codegraphOk && !input.hasDiscoveryHit
281
+ }
282
+
283
+ /**
284
+ * Tools cuyo completado constituye evidencia REAL de discovery estructural en la sesion.
285
+ *
286
+ * `codegraph_explore` es la senal fuerte: llamada real, resultado inspeccionable por el plugin.
287
+ * No hay cache de discovery: `getDiscoverySnapshot` nunca existió y se retiró.
288
+ *
289
+ * Historia (fix-codegraph-gate): antes la UNICA senal era `getDiscoverySnapshot`, una tool que nunca
290
+ * se registro ni en el plugin ni en el MCP. `hasDiscoveryHit` era siempre false y el gate bloqueaba
291
+ * todo read/grep/glob de codigo sin salida posible; un `codegraph_explore` completado NO lo abria,
292
+ * contradiciendo la instruccion "CodeGraph primero" del agente.
293
+ */
294
+ export function marksDiscovery(toolName: string): boolean {
295
+ return (
296
+ toolName.includes("codegraph_explore")
297
+ )
298
+ }
299
+
300
+ /**
301
+ * Mismo criterio de "archivo de código" que el gate de read (index.ts:552),
302
+ * sin la cláusula de `pattern` que es específica de glob. Docs/JSON quedan fuera:
303
+ * un .md bajo assets/ es documentación, no código.
304
+ */
305
+ export function isCodePath(p: string): boolean {
306
+ if (!p) return false
307
+ if (/\.(md|json)$/i.test(p)) return false
308
+ return /\.(ts|js|tsx|jsx|mts|cts)$/i.test(p) || p.includes("src/") || p.includes("assets/")
309
+ }
310
+
311
+ /**
312
+ * Evidencia de una corrida de shell para las tasks `test:` (change
313
+ * test-verification-execution). Devuelve null si el evento no permite afirmar
314
+ * nada: sin comando o sin `exit` numérico.
315
+ *
316
+ * NO consulta `status`: medido en OpenCode 2.0.32, `sh -c 'exit 3'` llega con
317
+ * `status: "completed"` y `result.metadata.status: "completed"`. El único dato
318
+ * veraz es `result.metadata.exit`.
319
+ */
320
+ export function shellEvidence(event: any): { cmd: string; exit: number; ts: number } | null {
321
+ if (!event || event.tool !== "shell") return null
322
+ const cmd = event.input?.command
323
+ const exit = event.result?.metadata?.exit
324
+ if (typeof cmd !== "string" || !cmd.trim()) return null
325
+ if (typeof exit !== "number") return null
326
+ return { cmd: cmd.trim(), exit, ts: Date.now() }
327
+ }
328
+
329
+ /** Tools que mutan archivos: avanzan `lastMutationTs`. */
330
+ export function isMutatingTool(tool: string): boolean {
331
+ return tool === "edit" || tool === "write" || tool === "patch"
332
+ }
333
+
334
+ /**
335
+ * Comando shell que escribe en disco. Predicado compartido por el gate de
336
+ * mutación y por `lastMutationTs`: si no se compartiera, un `sed -i` aprobado
337
+ * por el gate mutaría el código sin avanzar el timestamp, y una corrida de test
338
+ * previa seguiría contando como evidencia.
339
+ */
340
+ export function isShellMutation(command: string): boolean {
341
+ if (!command) return false
342
+ // `2>&1`, `1>&2`, `2>/dev/null` son redirects de stderr, no mutación.
343
+ const cmdSansStderr = command.replace(/2>&1/g, "").replace(/1>&2/g, "").replace(/2>\s*\/dev\/null/g, "")
344
+ return /(^|[^0-9&])>{1,2}\s*(?!&)\S+|\bsed\b[^|;]*-i|\btruncate\b|\btee\b|\bcp\s+|\bmv\s+|\brm\s+|python.*open.*w/.test(cmdSansStderr)
345
+ }
346
+
347
+ /**
348
+ * Verbos cuyo output revela el CONTENIDO de un archivo. No incluye verbos que
349
+ * sólo mencionan paths (`bun test x.ts`, `tsc src/x.ts`, `git diff`): el gate
350
+ * distingue "revela" de "menciona" — ver change shell-code-read-gate.
351
+ */
352
+ const SHELL_READ_VERBS = new Set([
353
+ "cat", "head", "tail", "sed", "awk", "grep", "rg", "ag", "ack", "less", "more",
354
+ "nl", "bat", "tac", "strings", "xxd", "od", "base64", "diff", "cut", "paste",
355
+ "sort", "uniq", "column", "expand", "fold",
356
+ ])
357
+
358
+ const SHELL_WRAPPERS = new Set(["sudo", "env", "time", "command", "exec", "nohup"])
359
+
360
+ /**
361
+ * Paths de código que `command` REVELA (no los que menciona).
362
+ *
363
+ * Fail-open por diseño: redirección => escritura, `sed -i` => escritura, verbo
364
+ * fuera de la allowlist => []. Un gate de shell nunca es hermético; el objetivo
365
+ * es quitar el bypass casual (`cat src/x.ts`), no demostrar imposibilidad.
366
+ * Bypass residual documentado en el spec: `git show`, `python -c`, `node -e`,
367
+ * `find -exec`, `xargs`.
368
+ */
369
+ export function shellReadTargets(command: string): string[] {
370
+ if (!command) return []
371
+ const targets: string[] = []
372
+ // Split por pipes/separadores, NO por `>`: necesitamos ver la redirección.
373
+ const segments = command.replace(/&&/g, ";").replace(/\|\|/g, ";").split(/[|;\n]+/)
374
+ for (const seg of segments) {
375
+ const trimmed = seg
376
+ // Redirects de stderr no son escritura: sin esto, `cat src/x.ts 2>&1`
377
+ // pasaba el check `>` de abajo y se colaba del gate (bypass casual).
378
+ // Mismo criterio que isShellMutation, el sibling de este módulo.
379
+ .replace(/2>&1/g, "")
380
+ .replace(/1>&2/g, "")
381
+ .replace(/2>\s*\/dev\/null/g, "")
382
+ .trim()
383
+ if (!trimmed) continue
384
+ // Escritura real (cat > f, tee, heredoc): no es lectura.
385
+ if (/>/.test(trimmed)) continue
386
+ const tokens = (trimmed.match(/(?:[^\s"'`\\]+|"[^"]*"|'[^']*'|`[^`]*`)+/g) || [])
387
+ .map((t) => t.replace(/["'`]/g, "").replace(/\\/g, ""))
388
+ .filter(Boolean)
389
+ if (tokens.length === 0) continue
390
+ let i = 0
391
+ while (i < tokens.length && SHELL_WRAPPERS.has(tokens[i])) i++
392
+ const verb = tokens[i] || ""
393
+ const base = verb.split("/").pop() || verb
394
+ if (!SHELL_READ_VERBS.has(base)) continue
395
+ // sed -i es escritura in-place, no lectura.
396
+ if (base === "sed" && tokens.some((t) => t === "-i" || t.startsWith("-i") || t === "--in-place" || t.startsWith("--in-place"))) continue
397
+ // diff -q/--brief/--stat sólo compara metadatos: no revela contenido.
398
+ if (base === "diff" && tokens.some((t) => t === "-q" || t === "--quiet" || t === "--brief" || t === "--stat" || /^-[a-zA-Z]*q[a-zA-Z]*$/.test(t))) continue
399
+ for (const p of extractPathsFromBash(trimmed)) {
400
+ if (isCodePath(p)) targets.push(p)
401
+ }
402
+ }
403
+ return [...new Set(targets)]
404
+ }
405
+
406
+ /**
407
+ * Predicado puro del gate de shell: ¿leer este path por shell está EXENTO?
408
+ *
409
+ * Extraído del hook para poder testear el INVARIANTE (mismo criterio que
410
+ * shouldBlockCodeRead). Exenciones:
411
+ * 1. Paths FUERA del proyecto: no son código de este codebase (fixtures
412
+ * temporales de tests, reportes generados); bloquearlos rompe pipelines sin
413
+ * escape hatch — el falso positivo que el diseño prohibió.
414
+ * 2. Specs/docs (.md/.json, openspec, assets/docs).
415
+ * 3. Archivos ya trackeados en tasks/fileFingerprints/lastValidated (el refresh
416
+ * honesto post-complete necesita re-leerlos).
417
+ */
418
+ export function isShellPathExempt(p: string, directory: string, freshState: any): boolean {
419
+ if (!isPathInsideProject(p, directory)) return true
420
+ if (p.includes("openspec/changes") || p.includes("openspec/specs")) return true
421
+ if (p.includes("assets/docs") || p.includes("/docs/") || /\.md$/i.test(p) || /\.json$/i.test(p)) return true
422
+ try {
423
+ if (freshState) {
424
+ const fps = Object.keys(freshState.fileFingerprints || {})
425
+ const taskFiles = Object.values(freshState.tasks || {}).map((t: any) => t.filePath).filter(Boolean)
426
+ const lv: any = (freshState as any).lastValidated || null
427
+ const lvByFile = lv?.byFile && typeof lv.byFile === "object" ? lv.byFile : null
428
+ const lvPaths: string[] = [
429
+ ...(lvByFile ? Object.keys(lvByFile) : []),
430
+ ...(lv?.filePath ? [lv.filePath as string] : []),
431
+ ]
432
+ const matches = (f: string): boolean => p.endsWith(f) || f.endsWith(p) || p.includes(f) || f.includes(p)
433
+ if (fps.some(matches) || taskFiles.some(matches)) return true
434
+ if (lvPaths.some((lf) => p.endsWith(lf) || lf.endsWith(p))) return true
435
+ }
436
+ } catch {}
437
+ return false
438
+ }
439
+
440
+ /** Comandos `test:` declarados en taskVerifications, ya normalizados (sin prefijo). */
441
+ export function testVerifyCommands(state: any): Set<string> {
442
+ const out = new Set<string>()
443
+ try {
444
+ for (const v of Object.values((state as any)?.taskVerifications || {})) {
445
+ if (typeof v === "string" && v.trim().startsWith("test:")) {
446
+ const cmd = v.trim().slice("test:".length).trim()
447
+ if (cmd) out.add(cmd)
448
+ }
449
+ }
450
+ } catch {}
451
+ return out
452
+ }
453
+
262
454
  function jsonContent(value: unknown): { content: string } {
263
455
  try {
264
456
  return { content: JSON.stringify(value) ?? "null" }
@@ -343,6 +535,18 @@ export default {
343
535
  const sessionId: string = event.sessionID ?? "default"
344
536
  const text = (event.prompt.text ?? "").trim()
345
537
  const state = readState(directory)
538
+ // task-verification-integrity D4: registrar el texto ORIGINAL del usuario (antes de los
539
+ // hints de abajo) para que `implementation_complete({force:true})` se apoye en evidencia
540
+ // real y no en el audit, que lo redacta el agente. `ownedKeys` evita pisar el avance del MCP.
541
+ // Cada prompt lleva `ts`: el MCP sólo acepta confirmaciones POSTERIORES al último
542
+ // intento de force, así un "¿qué hace force:true?" viejo no abre el gate.
543
+ if (text && state) {
544
+ try {
545
+ const prev = Array.isArray((state as any).lastUserPrompts) ? (state as any).lastUserPrompts : []
546
+ ;(state as any).lastUserPrompts = [...prev, { text: text.slice(0, 500), ts: Date.now() }].slice(-5)
547
+ persistState(directory, state, ["lastUserPrompts"])
548
+ } catch {}
549
+ }
346
550
  const currentState = state?.state ?? "DONE"
347
551
  const trivial = isTrivial(text, currentState)
348
552
  trivialBySession.set(sessionId, trivial)
@@ -519,14 +723,20 @@ export default {
519
723
  (lvFile ? lvFile.some((lf) => targetPath.endsWith(lf) || lf.endsWith(targetPath)) : false)
520
724
  }
521
725
  } catch {}
522
- if (isCodeFile && !isLiteralGrep && !isSpecTasksPath && !isDocsPath && !isTaskTracked) {
523
- const hasDiscoveryHit = discoveryHitByRequest.get(sessionId) ?? getDiscoveryCacheHit(directory)
524
- const codegraphOk = isCodegraphAvailable(directory)
525
- if (codegraphOk && !hasDiscoveryHit) {
526
- // Allow if file is not indexable or trivial?
527
- // Block with suggestion
528
- throw new Error(`BLOCKED: Usá getDiscoverySnapshot primero — sugerencia: getDiscoverySnapshot("${targetPath || "query"}")`)
529
- }
726
+ // Orden cheap -> caro: el path no hace I/O; codegraph y la cache en disco solo se evalúan
727
+ // si el path esta realmente sujeto al gate (`read`/`grep`/`glob` corren en cada turno).
728
+ const gated = isCodeReadGated({ isCodeFile, isLiteralGrep, isSpecTasksPath, isDocsPath, isTaskTracked })
729
+ const codegraphOk = gated ? isCodegraphAvailable(directory) : false
730
+ // Sin fallback a disco: la única evidencia válida es un codegraph_explore
731
+ // COMPLETADO en esta sesión (discoveryHitByRequest). El memo se limpia con
732
+ // session.deleted, así "esta sesión" es literal.
733
+ const hasDiscoveryHit = codegraphOk ? discoveryHitByRequest.get(sessionId) === true : false
734
+ if (shouldBlockCodeRead({
735
+ isCodeFile, isLiteralGrep, isSpecTasksPath, isDocsPath, isTaskTracked, codegraphOk, hasDiscoveryHit,
736
+ })) {
737
+ throw new Error(
738
+ `BLOCKED: sin discovery estructural en esta sesión — llamá codegraph_codegraph_explore("${targetPath || "query"}") antes de Read/Grep/Glob de código`
739
+ )
530
740
  }
531
741
  }
532
742
 
@@ -560,12 +770,32 @@ export default {
560
770
  }
561
771
  }
562
772
 
773
+ // ── 3.2) CodeGraph gate para shell: leer código sin discovery → BLOCKED ──
774
+ // El gate de código (arriba) sólo mira read/grep/glob; `cat src/x.ts` revela
775
+ // lo mismo y no costaba nada. Fail-open a propósito: `read` falla cerrado
776
+ // porque es inequívoco; `shell` es ambiguo y un falso positivo rompe tests o
777
+ // builds sin escape hatch para el usuario (change shell-code-read-gate).
778
+ if (tool === "shell") {
779
+ const cmdForRead: string = args?.command || args?.cmd || ""
780
+ if (typeof cmdForRead === "string" && cmdForRead) {
781
+ const shellTargets = shellReadTargets(cmdForRead)
782
+ if (shellTargets.length > 0) {
783
+ const gatedShell = shellTargets.filter((p) => !isShellPathExempt(p, directory, freshState))
784
+ const codegraphOkShell = gatedShell.length > 0 ? isCodegraphAvailable(directory) : false
785
+ const hasDiscoveryShell = codegraphOkShell ? discoveryHitByRequest.get(sessionId) === true : false
786
+ if (gatedShell.length > 0 && codegraphOkShell && !hasDiscoveryShell) {
787
+ throw new Error(
788
+ `BLOCKED: sin discovery estructural en esta sesión — llamá codegraph_codegraph_explore("${gatedShell[0]}") antes de leer código por shell`
789
+ )
790
+ }
791
+ }
792
+ }
793
+ }
794
+
563
795
  // ── 3.5) Shell file-mutation gate (harden-task-integrity: shell mutante requiere validate_edit) ──
564
796
  if (tool === "shell" && freshState && ["EXECUTING_INLINE", "EXECUTING_SUBAGENTS"].includes(freshState.state)) {
565
797
  const cmdForMutation: string = args?.command || args?.cmd || ""
566
- // Fix falso positivo: `2>&1`, `1>&2`, `2>/dev/null` son redirects de stderr, no mutación.
567
- const cmdSansStderr = cmdForMutation.replace(/2>&1/g, "").replace(/1>&2/g, "").replace(/2>\s*\/dev\/null/g, "")
568
- const isMutating = /(^|[^0-9&])>{1,2}\s*(?!&)\S+|\bsed\b[^|;]*-i|\btruncate\b|\btee\b|\bcp\s+|\bmv\s+|python.*open.*w/.test(cmdSansStderr)
798
+ const isMutating = isShellMutation(cmdForMutation)
569
799
  if (isMutating) {
570
800
  const mPaths = extractPathsFromBash(cmdForMutation)
571
801
  for (const p of mPaths) {
@@ -722,24 +952,52 @@ export default {
722
952
  // Track discovery hit to allow subsequent Reads
723
953
  const tool: string = event.tool as string
724
954
  const sessionId: string = (event.sessionID ?? "default") as string
725
- const resultText = event.status === "completed" && event.result ? JSON.stringify(event.result) : ""
726
- if (tool.includes("getDiscoverySnapshot") || tool.includes("get_discovery_snapshot")) {
727
- // if output indicates hit, mark it
955
+ // (fix-codegraph-gate) Evidencia REAL de discovery: un `codegraph_explore` completado en ESTA
956
+ // sesión abre el gate de lecturas de código. Se marca aunque el resultado venga vacío, a
957
+ // propósito: "CodeGraph no tiene este archivo en su índice" es justamente el caso donde el
958
+ // Read TIENE que poder pasar si hubo `codegraph_explore`.
959
+ if (marksDiscovery(tool)) {
728
960
  try {
729
- const text = resultText
730
- if (text && !text.includes("null") && text.length > 10) {
731
- discoveryHitByRequest.set(sessionId, true)
732
- }
733
- // Engram hit via discovery snapshot (contiene engramHits)
734
- if (text && text.includes("engramHits")) {
735
- engramHitBySession.set(sessionId, true)
736
- }
961
+ if (event.status === "completed") discoveryHitByRequest.set(sessionId, true)
737
962
  } catch {}
738
963
  }
739
964
  // Track Engram hit (mem_search / mem_context / dedup) para soft gate 1.6
740
- if (tool.includes("engram_mem_search") || tool.includes("engram_mem_context") || tool.includes("mem_search") || tool.includes("mem_context") || tool.includes("getEngramDedup") || tool.includes("engram_mem_")) {
965
+ if (tool.includes("engram_mem_search") || tool.includes("engram_mem_context") || tool.includes("mem_search") || tool.includes("mem_context") || tool.includes("engram_mem_")) {
741
966
  engramHitBySession.set(sessionId, true)
742
967
  }
968
+ // test-verification-execution: evidencia para tasks `test:`.
969
+ // `status` miente con exit != 0 (ver shellEvidence): sólo se registra con exit numérico.
970
+ // D10 respectado: NO cada shell persiste (git/ls/builds no tienen valor de evidencia y
971
+ // cada persist cuesta lock + re-read + write + 3 renames de backup). Se persisten sólo
972
+ // las corridas cuyo comando matchea un `test:` declarado en taskVerifications — que son
973
+ // las ÚNICAS que complete_task / verifyIntegrity consultan via findTestEvidence.
974
+ // Tradeoff fail-closed: si taskVerifications aún no llegó al state cuando se corre el
975
+ // test, la evidencia no se registra → complete_task pide re-correr (razón explícita).
976
+ try {
977
+ const ev = shellEvidence(event as any)
978
+ // Un shell que escribe (exit 0) también invalida evidencia de test:
979
+ // si no, `sed -i` mutaría el código sin avanzar el timestamp.
980
+ const mutating =
981
+ (isMutatingTool(tool) && event.status === "completed") ||
982
+ (!!ev && ev.exit === 0 && isShellMutation(ev.cmd))
983
+ const isTestRun = !!ev && testVerifyCommands(readState(directory)).has(ev.cmd)
984
+ if (isTestRun || mutating) {
985
+ const s = readState(directory)
986
+ if (s) {
987
+ const owned: string[] = []
988
+ if (ev && isTestRun) {
989
+ const prev = Array.isArray((s as any).shellRuns) ? (s as any).shellRuns : []
990
+ ;(s as any).shellRuns = [...prev, ev].slice(-25)
991
+ owned.push("shellRuns")
992
+ }
993
+ if (mutating) {
994
+ ;(s as any).lastMutationTs = Date.now()
995
+ owned.push("lastMutationTs")
996
+ }
997
+ if (owned.length > 0) persistState(directory, s, owned)
998
+ }
999
+ }
1000
+ } catch {}
743
1001
  // Heartbeat: single-writer — plugin memo, no persist cada tool (D10)
744
1002
  try {
745
1003
  const s = readState(directory)
@@ -36,12 +36,12 @@ Do NOT invoke any implementation skill, write any code, or scaffold any project
36
36
 
37
37
  ### Process
38
38
 
39
- 1. **Check discovery-cache first** — `src/discovery-cache.ts` `getDiscoverySnapshot(query)` + `getEngramDedup(query, requestId)`. Si hit válido (TTL+gitDiffHash), **reusar** sin llamar tools. Solo si miss → `engram_mem_search` + `codegraph_codegraph_explore` y **SHALL `putDiscoverySnapshot`** antes de avanzar. No re-llamar si área difiere <30% del snapshot.
39
+ 1. **Reusar discovery** — si ya llamaste `codegraph_codegraph_explore` para el área en este request, reusá ese output; si no, `engram_mem_search` + `codegraph_codegraph_explore`. No repitas la misma query sin motivo. No hay cache de archivo.
40
40
  2. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
41
41
  4. **Propose 2-3 approaches (hardening-v2 — SHALL)** — con tabla trade-offs (coste|riesgo|complejidad) + evidencia CodeGraph+Engram sin alucinar, YAGNI, y recomendación con razón; cada approach cita symbols existentes y mem_search hits verificables
42
42
  5. **Present design** — in sections scaled to complexity, get user approval after each section. **Gate post-brainstorming (hardening-v2):** tras presentar diseño, preguntar "¿Procedo con este diseño o querés ajustar algo?" y esperar confirmación explícita antes de `record_discovery`, `openspec-propose` o implementación directa
43
43
  6. **Write design doc (output path condicional — router exclusivo) — SHALL read fresco + edit no write + sync proactiva:**
44
- - Antes de `write` SHALL `read` fresco de `openspec/changes/<id>/{proposal,design,tasks}` o `docs/superpowers/specs/*.md` si existen; si existen SHALL `edit` (no `write`); `write` solo si no existía. `getDiscoverySnapshot` no aplica a specs/docs. Para `open-explore` sin archivo SHALL `mem_save topic_key:brainstorm/<hash>` por iteración.
44
+ - Antes de `write` SHALL `read` fresco de `openspec/changes/<id>/{proposal,design,tasks}` o `docs/superpowers/specs/*.md` si existen; si existen SHALL `edit` (no `write`); `write` solo si no existía. El discovery no aplica a specs/docs. Para `open-explore` sin archivo SHALL `mem_save topic_key:brainstorm/<hash>` por iteración.
45
45
  - Si trigger + `level 1+` no-downgradeable (`estLines>30` o `fileCount>2` o API pública) y change activo → escribir `openspec/changes/<id>/design.md` sección `## Alternatives Considered` con 2-3 approaches (tabla coste|riesgo|complejidad + evidencia CodeGraph+Engram), no `docs/`. No invocar `openspec-propose` separado — este es el diseño. Si ya existe, SHALL `edit` no `write`.
46
46
  - Si trigger + `0/0+1` o `1+` downgradeable (`estLines<30`&&`fileCount==1`&&sin API) → solo `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` + gate único `¿Procedo?` → DIRECT sin change. SHALL sugerir downgrade: `"Esto parece 0+1 (~X líneas, 1 archivo). ¿Lo tratamos sin spec?"` y esperar. Si `docsSpec` ya existe SHALL `read` + `edit`.
47
47
  - Gate post-brainstorming es **único**; no hay segundo `¿Procedo con spec?`. `get_audit` `WARN:duplicate_design_generated` si ambos artefactos mismo `requestId`.
@@ -25,14 +25,13 @@ Determinar el modo de ejecución óptimo entre **inline** y **subagent-driven**
25
25
 
26
26
  ## Procedimiento
27
27
 
28
- ### Paso 0: Verificar discovery-cache primero (ÚNICO entrypoint)
29
-
30
- **SHALL** llamar `src/discovery-cache.ts` `getDiscoverySnapshot(query)` y `getEngramDedup(query, requestId)` antes de cualquier `codegraph_codegraph_explore` o `engram_mem_search`. Si hit válido (TTL+gitDiffHash) → **reusar** `codegraph+engramHits` para construir `sharedFiles/fileClusters`; solo `codegraph_impact` para símbolos no cubiertos. Si miss o área difiere >30% → fetch fresco y `putDiscoverySnapshot`. Segunda llamada misma query en mismo `requestId` SHALL ser dedup (no `redundantCallCount`).
31
-
32
- ### Paso 0.1: Consultar Engram por decisiones previas (solo si no hay dedup hit)
33
-
34
- Si `getEngramDedup` fue miss, `engram_mem_search` con keywords del cambio. Si existe decisión previa, usar como referencia no vinculante.
35
-
28
+ ### Paso 0: Reusar el discovery ya obtenido en este request
29
+
30
+ Si en este mismo request ya llamaste `codegraph_codegraph_explore` para el área, **reusá ese output** (sigue en tu contexto) para construir `sharedFiles/fileClusters`; solo `codegraph_impact` para símbolos no cubiertos. Si el área difiere materialmente (overlap <30%) o todavía no hiciste discovery → `codegraph_codegraph_explore` fresco. No hay cache de archivo: no repitas la misma query sin motivo.
31
+
32
+ ### Paso 0.1: Consultar Engram por decisiones previas
33
+
34
+ `engram_mem_search` con keywords del cambio, salvo que ya lo hayas hecho para el mismo tema en este request. Si existe decisión previa, usarla como referencia no vinculante.
36
35
  ### Paso 0.5: Early exit para cambios pequeños
37
36
 
38
37
  Si el change tiene **≤2 tasks** Y **no comparten archivos entre sí** → devolver directamente:
@@ -48,10 +47,10 @@ Si el change tiene **≤2 tasks** Y **no comparten archivos entre sí** → devo
48
47
  }
49
48
  ```
50
49
 
51
- ### Paso 1: Obtener datos de CodeGraph (solo si discovery-cache miss)
52
-
53
- Si Paso 0 fue hit → **SKIP este paso**, derivar `fileClusters/sharedFiles` del snapshot (`state.snapshots.codegraph` + `tasks.md`). Snapshot resultante SHALL usar `codegraphUsed:["discovery-cache"]` (válido, no genera `WARN:execution_without_codegraph` si `snapshots.codegraph` existe) y `reuseDiscovery:true`.
54
- Si miss:
50
+ ### Paso 1: Obtener datos de CodeGraph (solo si no hiciste discovery en este request)
51
+
52
+ Si ya hay discovery en contexto → **SKIP este paso**; derivar `fileClusters/sharedFiles` del output (`state.snapshots.codegraph` + `tasks.md`) con `codegraphUsed:["codegraph_explore"]` y `reuseDiscovery:true`.
53
+ Si no:
55
54
  ```
56
55
  codegraph_codegraph_explore con query: "<área del cambio>"
57
56
  ```
@@ -26,9 +26,9 @@ When ready to implement, run /opsx-apply
26
26
 
27
27
  **Steps**
28
28
 
29
- 0. **Ground with CodeGraph + Engram (SHALL — Ostacky)** — Antes de `openspec new`, verifica `getDiscoverySnapshot(query)` + `getEngramDedup`. Si miss → `codegraph_codegraph_explore` + `engram_mem_search` + `putDiscoverySnapshot`. Cita símbolos existentes en `proposal/design`; sin esto el `design.md` puede alucinar API inexistente y dispara `WARN:codegraphBypass` en `doctor`.
29
+ 0. **Ground with CodeGraph + Engram (SHALL — Ostacky)** — Antes de `openspec new`, usá `codegraph_codegraph_explore` + `engram_mem_search` sobre el área (si no los hiciste ya en este request). Citá símbolos existentes en `proposal/design`; sin esto el `design.md` puede alucinar API inexistente y dispara `WARN:codegraphBypass` en `doctor`.
30
30
 
31
- 1. **Check Engram for similar changes** — `engram_mem_search` con keywords del *paso 0* (reusar si ya fue hit; no duplicar `mem_search` mismo `requestId`). Si hay change similar previo, surfacealo. Pueden extender trabajo existente en vez de empezar de cero.
31
+ 1. **Check Engram for similar changes** — `engram_mem_search` con keywords del *paso 0* (no repetir la misma query en el mismo request). Si hay change similar previo, surfacealo. Pueden extender trabajo existente en vez de empezar de cero.
32
32
 
33
33
  2. **If no clear input provided, ask what they want to build**
34
34
 
@@ -28,7 +28,7 @@ Write the test first. Watch it fail. Write minimal code to pass.
28
28
 
29
29
  Thinking "skip TDD just this once"? Stop. That's rationalization.
30
30
 
31
- > **Ostacky ground (SHALL):** Antes de RED, verifica `getDiscoverySnapshot(query)` + `getEngramDedup`. Si hit → reusar símbolos/decisiones; si miss → `codegraph_codegraph_explore` + `engram_mem_search` + `putDiscoverySnapshot`. Evita testear API inexistente y respeta `discovery-cache (único)` de `ostacky.md §1`. Ver `execution-mode-evaluation` para cache TTL.
31
+ > **Ostacky ground (SHALL):** Antes de RED, verificá el área con `codegraph_codegraph_explore` + `engram_mem_search` (si no los hiciste ya en este request). Citá símbolos existentes para no testear API inexistente. Ver `execution-mode-evaluation`.
32
32
 
33
33
  ## The Iron Law
34
34