ostacky 0.9.3 → 0.9.5

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.
Files changed (113) hide show
  1. package/README.md +27 -25
  2. package/assets/agents/ostacky.md +5 -4
  3. package/assets/commands/install-stack.md +2 -2
  4. package/assets/mcp/ostacky-controller/bun.lock +20 -0
  5. package/assets/mcp/ostacky-controller/index.js +185 -29
  6. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/{auth-B2Q9_Vxp.d.cts → auth--n_Yzytt.d.cts} +14 -9
  7. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/{auth-CVhIKulL.d.mts.map → auth--n_Yzytt.d.cts.map} +1 -1
  8. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/{auth-CfDMXiII.cjs → auth-4-zilcqg.cjs} +3 -2
  9. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/auth-4-zilcqg.cjs.map +1 -0
  10. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/{auth-CUe6YdwF.mjs → auth-CGP0BDVq.mjs} +3 -2
  11. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/auth-CGP0BDVq.mjs.map +1 -0
  12. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/{auth-CVhIKulL.d.mts → auth-YUQV3RRv.d.mts} +14 -9
  13. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/{auth-B2Q9_Vxp.d.cts.map → auth-YUQV3RRv.d.mts.map} +1 -1
  14. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/index.cjs +1 -1
  15. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/index.d.cts +1 -1
  16. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/index.d.mts +1 -1
  17. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/index.mjs +1 -1
  18. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/internal.cjs +1 -1
  19. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/internal.d.cts +1 -1
  20. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/internal.d.mts +1 -1
  21. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/internal.mjs +1 -1
  22. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/package.json +1 -1
  23. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/README.md +3 -3
  24. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{ajvProvider-CplOkyLC.mjs → ajvProvider-CEoC__sr.mjs} +483 -38
  25. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/ajvProvider-CEoC__sr.mjs.map +1 -0
  26. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{ajvProvider-BdSo9BII.d.mts → ajvProvider-CgI-5L6O.d.mts} +25 -8
  27. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{ajvProvider-BdSo9BII.d.mts.map → ajvProvider-CgI-5L6O.d.mts.map} +1 -1
  28. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{ajvProvider-B_4czhCS.d.cts → ajvProvider-FOQRM7up.d.cts} +24 -7
  29. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{ajvProvider-B_4czhCS.d.cts.map → ajvProvider-FOQRM7up.d.cts.map} +1 -1
  30. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{ajvProvider-Brn-Q2J_.cjs → ajvProvider-ZaoO9afR.cjs} +483 -38
  31. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/ajvProvider-ZaoO9afR.cjs.map +1 -0
  32. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{cfWorkerProvider-ojcZaap3.cjs → cfWorkerProvider-BwbPzsdI.cjs} +25 -15
  33. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/cfWorkerProvider-BwbPzsdI.cjs.map +1 -0
  34. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{cfWorkerProvider-BYRW5af5.d.mts → cfWorkerProvider-C03CSigr.d.mts} +21 -6
  35. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/cfWorkerProvider-C03CSigr.d.mts.map +1 -0
  36. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{cfWorkerProvider-D9GAttn8.d.cts → cfWorkerProvider-doTkZbTr.d.cts} +20 -5
  37. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/cfWorkerProvider-doTkZbTr.d.cts.map +1 -0
  38. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{cfWorkerProvider-Bd-iTHAF.mjs → cfWorkerProvider-p3WaZPqB.mjs} +26 -15
  39. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/cfWorkerProvider-p3WaZPqB.mjs.map +1 -0
  40. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{createMcpHandler-DzBzpE4_.d.cts → createMcpHandler-BLVe3qmU.d.cts} +137 -32
  41. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/createMcpHandler-BLVe3qmU.d.cts.map +1 -0
  42. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{createMcpHandler-CEckaSEs.d.mts → createMcpHandler-Bt6U_Fqb.d.mts} +138 -33
  43. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/createMcpHandler-Bt6U_Fqb.d.mts.map +1 -0
  44. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/dialects-D8eXzoGv.cjs +46 -0
  45. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/dialects-D8eXzoGv.cjs.map +1 -0
  46. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/dialects-DoSzNhcb.mjs +34 -0
  47. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/dialects-DoSzNhcb.mjs.map +1 -0
  48. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/index.cjs +209 -257
  49. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/index.cjs.map +1 -1
  50. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/index.d.cts +45 -3
  51. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/index.d.cts.map +1 -1
  52. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/index.d.mts +46 -4
  53. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/index.d.mts.map +1 -1
  54. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/index.mjs +201 -252
  55. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/index.mjs.map +1 -1
  56. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{mcp-DM1iHYFo.cjs → mcp-DUcCQSTW.cjs} +458 -35
  57. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/mcp-DUcCQSTW.cjs.map +1 -0
  58. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{mcp-IJurDZVN.mjs → mcp-Dw2OlZ1f.mjs} +389 -32
  59. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/mcp-Dw2OlZ1f.mjs.map +1 -0
  60. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsBrowser.cjs +1 -1
  61. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsBrowser.d.cts +1 -1
  62. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsBrowser.d.mts +1 -1
  63. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsBrowser.mjs +1 -1
  64. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsNode.cjs +1 -1
  65. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsNode.d.cts +1 -1
  66. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsNode.d.mts +1 -1
  67. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs +1 -1
  68. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsWorkerd.cjs +2 -2
  69. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsWorkerd.d.cts +1 -1
  70. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsWorkerd.d.mts +1 -1
  71. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/shimsWorkerd.mjs +2 -2
  72. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{src-BX_fm-dB.cjs → src-CgFsfg3E.cjs} +161 -48
  73. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/src-CgFsfg3E.cjs.map +1 -0
  74. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{src-D86MbS1I.mjs → src-D-y6h4N7.mjs} +161 -48
  75. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/src-D-y6h4N7.mjs.map +1 -0
  76. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/stdio.cjs +30 -3
  77. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/stdio.cjs.map +1 -1
  78. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/stdio.d.cts +9 -1
  79. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/stdio.d.cts.map +1 -1
  80. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/stdio.d.mts +9 -1
  81. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/stdio.d.mts.map +1 -1
  82. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/stdio.mjs +30 -3
  83. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/stdio.mjs.map +1 -1
  84. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{types-mS0yxCL0.d.mts → types-DUs7mGBv.d.mts} +1 -1
  85. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/{types-mS0yxCL0.d.mts.map → types-DUs7mGBv.d.mts.map} +1 -1
  86. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/validators/ajv.cjs +1 -1
  87. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/validators/ajv.d.cts +1 -1
  88. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/validators/ajv.d.mts +1 -1
  89. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/validators/ajv.mjs +1 -1
  90. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/validators/cfWorker.cjs +1 -1
  91. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/validators/cfWorker.d.cts +1 -1
  92. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/validators/cfWorker.d.mts +1 -1
  93. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/validators/cfWorker.mjs +1 -1
  94. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/package.json +4 -4
  95. package/assets/mcp/ostacky-controller/package.json +1 -1
  96. package/assets/plugins/ostacky-controller/index.ts +53 -11
  97. package/dist/cli.js +93 -53
  98. package/manifest.json +28 -28
  99. package/package.json +5 -1
  100. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/auth-CUe6YdwF.mjs.map +0 -1
  101. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/core/dist/auth-CfDMXiII.cjs.map +0 -1
  102. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/ajvProvider-Brn-Q2J_.cjs.map +0 -1
  103. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/ajvProvider-CplOkyLC.mjs.map +0 -1
  104. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/cfWorkerProvider-BYRW5af5.d.mts.map +0 -1
  105. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/cfWorkerProvider-Bd-iTHAF.mjs.map +0 -1
  106. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/cfWorkerProvider-D9GAttn8.d.cts.map +0 -1
  107. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/cfWorkerProvider-ojcZaap3.cjs.map +0 -1
  108. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/createMcpHandler-CEckaSEs.d.mts.map +0 -1
  109. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/createMcpHandler-DzBzpE4_.d.cts.map +0 -1
  110. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/mcp-DM1iHYFo.cjs.map +0 -1
  111. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/mcp-IJurDZVN.mjs.map +0 -1
  112. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/src-BX_fm-dB.cjs.map +0 -1
  113. package/assets/mcp/ostacky-controller/node_modules/@modelcontextprotocol/server/dist/src-D86MbS1I.mjs.map +0 -1
package/README.md CHANGED
@@ -23,24 +23,26 @@ opencode
23
23
 
24
24
  Abre la interfaz de terminal interactiva. **Para elegir el agente Ostacky: usá `Tab` para navegar entre agentes** en el panel lateral, o escribí `/agent Ostacky` en el chat.
25
25
 
26
- ### Web UI — para usar desde el navegador
26
+ ### Web UI — para usar desde el navegador (OpenCode v2)
27
27
 
28
28
  ```bash
29
- opencode web --port 4096
29
+ opencode pair
30
30
  ```
31
31
 
32
- Abre OpenCode en el navegador en `http://localhost:4096`. **Para elegir el agente Ostacky: abrí el combo/selector de agentes** en la barra superior y seleccioná "Ostacky". Útil para sesiones largas, trabajo en equipo o cuando querés compartir pantalla sin compartir terminal.
32
+ Abre el link de un solo uso (expira en 5 minutos) o escaneá el QR en el navegador para cargar la web UI con cookie de sesión (30 días). **Para elegir el agente Ostacky: abrí el combo/selector de agentes** en la barra superior y seleccioná "Ostacky". Útil para sesiones largas, trabajo en equipo o cuando querés compartir pantalla sin compartir terminal.
33
33
 
34
- Opciones útiles:
34
+ > Por defecto el server corre en `http://127.0.0.1:49374` solo en localhost. Cambialo con `opencode service` (ver tabla). Docs: https://opencode.ai/v2/docs/cli/web
35
35
 
36
- | Comando | Qué hace |
37
- | ----------------------------------------------- | --------------------------------------------------- |
38
- | `opencode web` | Puerto aleatorio, abre el navegador automáticamente |
39
- | `opencode web --port 4096` | Puerto fijo |
40
- | `opencode web --hostname 0.0.0.0` | Accesible desde la red local |
41
- | `opencode web --mdns` | Descubrible como `opencode.local` |
42
- | `OPENCODE_SERVER_PASSWORD=secreta opencode web` | Con autenticación HTTP Basic |
43
- | `opencode attach http://localhost:4096` | Conectar una terminal TUI a un servidor web activo |
36
+ Opciones útiles (v2):
37
+
38
+ | Comando | Qué hace |
39
+ | -------------------------------------------------- | --------------------------------------------------------------- |
40
+ | `opencode pair` | Muestra URL + QR para conectar web/app (links de un uso, 5 min) |
41
+ | `opencode serve --hostname 0.0.0.0 --port 4096` | Server dedicado en foreground (host remoto, Docker/systemd) |
42
+ | `opencode --server http://127.0.0.1:4096` | Conectar un cliente TUI a un server activo |
43
+ | `opencode service set hostname 0.0.0.0` | Escuchar en red local (luego `opencode service start`) |
44
+ | `opencode service set port 49374` | Puerto fijo en vez del default del channel |
45
+ | `opencode service set password "un-secreto-largo"` | Reemplaza password generado (rota todas las sesiones) |
44
46
 
45
47
  > 💡 Si no tenés OpenCode instalado aún: `curl -fsSL https://opencode.ai/install | bash`
46
48
 
@@ -248,37 +250,37 @@ Tras instalar (`--scope local`), el proyecto queda así:
248
250
 
249
251
  ```json
250
252
  {
251
- "version": "0.9.3",
253
+ "version": "0.9.5",
252
254
  "lockedAt": "2025-01-01T00:00:00.000Z",
253
255
  "repo": "JaimeHoracio/Ostacky",
254
- "tag": "v0.9.3",
256
+ "tag": "v0.9.5",
255
257
  "agents": {
256
258
  "ostacky": {
257
- "version": "0.9.3",
259
+ "version": "0.9.5",
258
260
  "installedAt": "2025-01-01T00:00:00.000Z",
259
261
  "sha256": "abc123..."
260
262
  }
261
263
  },
262
264
  "commands": {
263
265
  "install-stack": {
264
- "version": "0.9.3",
266
+ "version": "0.9.5",
265
267
  "installedAt": "2025-01-01T00:00:00.000Z",
266
268
  "sha256": "def456..."
267
269
  },
268
270
  "opsx-sync": {
269
- "version": "0.9.3",
271
+ "version": "0.9.5",
270
272
  "installedAt": "2025-01-01T00:00:00.000Z",
271
273
  "sha256": "ghi789..."
272
274
  }
273
275
  },
274
276
  "skills": {
275
- "brainstorming": { "version": "0.9.3", ... },
276
- "execution-mode-evaluation": { "version": "0.9.3", ... },
277
- "openspec-propose": { "version": "0.9.3", ... }
277
+ "brainstorming": { "version": "0.9.5", ... },
278
+ "execution-mode-evaluation": { "version": "0.9.5", ... },
279
+ "openspec-propose": { "version": "0.9.5", ... }
278
280
  },
279
281
  "mcpServers": {
280
- "ostacky-controller": { "version": "0.9.3", ... },
281
- "openspec": { "version": "0.9.3", ... }
282
+ "ostacky-controller": { "version": "0.9.5", ... },
283
+ "openspec": { "version": "0.9.5", ... }
282
284
  }
283
285
  }
284
286
  ```
@@ -293,7 +295,7 @@ Al terminar la instalación, el flujo normal es:
293
295
 
294
296
  1. **Iniciar OpenCode** (si no está corriendo):
295
297
  - **TUI:** `opencode` en tu terminal → seleccioná el agente con `/agents` (o atajos **Shift+Tab** / **Ctrl+X A**)
296
- - **Web:** `opencode web --port 4096` y abrí `http://localhost:4096` → abrí el selector de agentes de la interfaz y elegí `ostacky`
298
+ - **Web:** `opencode pair` y abrí el link impreso (ej. `http://127.0.0.1:49374/auth/...`) o escaneá el QR → abrí el selector de agentes de la interfaz y elegí `ostacky`
297
299
  2. OpenCode detecta automáticamente los archivos nuevos en `.opencode/` al iniciar, no necesita recarga manual.
298
300
  3. Para usar el agente, seleccionalo con `/agents` y escribí tu mensaje en el chat. (En OpenCode v2, `@` es solo para referenciar archivos, no agentes.)
299
301
 
@@ -310,7 +312,7 @@ Es opcional y solo necesario si algo falló durante la instalación o si querés
310
312
  ## Seguridad
311
313
 
312
314
  - `opencode.jsonc` se versiona en el repo para compartir permisos y MCP de forma reproducible.
313
- - Las URLs de descarga usan **tags de GitHub** (ej. `v0.9.3`), nunca `main` — instalaciones reproducibles
315
+ - Las URLs de descarga usan **tags de GitHub** (ej. `v0.9.5`), nunca `main` — instalaciones reproducibles
314
316
  - Cada path de archivo descargado es validado para prevenir **path traversal**
315
317
  - Los archivos incluyen **checksum SHA-256** opcional; si el manifest lo define, el contenido se verifica antes de escribir
316
318
  - El cache local (`.opencode/cache/`) también valida integridad al servir archivos cacheados
@@ -367,7 +369,7 @@ CodeGraph está instalado en `.opencode/tools/codegraph/bin/codegraph` y se conf
367
369
  | `.opencode/tools/codegraph/bin/codegraph status` | Muestra estado del index y archivos pendientes |
368
370
  | `.opencode/tools/codegraph/bin/codegraph install --target opencode --location local --yes` | Configura CodeGraph para OpenCode y genera AGENTS.md |
369
371
 
370
- > **Windows:** si ves `Command failed: cmd.exe /d /c call ... codegraph.cmd init -i` durante `npx ostacky install`, es un _warning_ no fatal — el binario se instaló pero el índice no se pudo crear. Reintentá con `npx ostacky install-stack --scope local` o ejecutá manualmente `.opencode\tools\codegraph\bin\codegraph.exe init -i` (o `codegraph.cmd` si no hay `.exe`) dentro del proyecto. Desde v0.9.3 el instalador usa el patrón resiliente de Engram (strip 0 + búsqueda recursiva) y loguea `projectRoot|toolsDir` para diagnosticar scope.
372
+ > **Windows:** si ves `Command failed: cmd.exe /d /c call ... codegraph.cmd init -i` durante `npx ostacky install`, es un _warning_ no fatal — el binario se instaló pero el índice no se pudo crear. Reintentá con `npx ostacky install-stack --scope local` o ejecutá manualmente `.opencode\tools\codegraph\bin\codegraph.exe init -i` (o `codegraph.cmd` si no hay `.exe`) dentro del proyecto. Desde v0.9.5 el instalador usa el patrón resiliente de Engram (strip 0 + búsqueda recursiva) y loguea `projectRoot|toolsDir` para diagnosticar scope.
371
373
 
372
374
  ## Licencia
373
375
 
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  description: Orquestador principal — rutea por nivel, orquesta CodeGraph + OpenSpec + Superpowers.
3
3
  mode: primary
4
- version: 0.9.3
4
+ version: 0.9.5
5
5
  ---
6
6
 
7
- Sos **Ostacky v0.9.3**, orquestás, no implementás. Interpretás, clasificás (0/0+1/1+), ruteás y coordinás.
7
+ Sos **Ostacky v0.9.5**, orquestás, no implementás. Interpretás, clasificás (0/0+1/1+), ruteás y coordinás.
8
8
 
9
- > **Versión:** `0.9.3` (sincronizada desde `package.json` vía `scripts/sync-version.ts`). Cuando te pregunten qué versión tenés, qué versión sos, o `¿qué versión tenés?` / `version` / `¿en qué versión estás?`, respondé exactamente: **"Ostacky v0.9.3"** (o `v0.9.3` si te piden solo el número). No inventes otra versión.
9
+ > **Versión:** `0.9.5` (sincronizada desde `package.json` vía `scripts/sync-version.ts`). Cuando te pregunten qué versión tenés, qué versión sos, o `¿qué versión tenés?` / `version` / `¿en qué versión estás?`, respondé exactamente: **"Ostacky v0.9.5"** (o `v0.9.5` si te piden solo el número). No inventes otra versión.
10
10
 
11
11
  ## Reglas innegociables
12
12
 
@@ -23,6 +23,7 @@ Sos **Ostacky v0.9.3**, orquestás, no implementás. Interpretás, clasificás (
23
23
  > **Regla de oro:** Nunca escribas instrucciones genéricas tipo `“sé cuidadoso / no introduzcas errores / no inventes”`. Son no-verificables y el LLM las ignora (instruction fatigue). Cada SHALL abajo es **verificable** (comando, evidencia o artefacto). `tasks.md` también SHALL ser verificable — no behavioral.
24
24
 
25
25
  1. **Engram contradiction check SHALL.** Antes de proponer o discutir cambios (Nivel 0+1/1+, **eximido si `isTrivial && DONE`** — ej: "cambiar título" no paga `mem_search`), SHALL `mem_search` por keywords del pedido (max 5 keywords = sustantivos del pedido + `topic_key` del change activo si lo hay); si hit `type:decision|architecture` con contradicción semántica (ej: pide Zustand y hay #571 “usar Redux por X”), SHALL mostrar diff `“Antes decidimos X el <fecha> por Z (topic_key), ahora pedís ¬X”`, llamar `request_clarification` y esperar. Si Engram `degraded`/timeout → SHALL advertir “sin memoria” y continuar (no block). Si usuario confirma override → SHALL `mem_save` mismo `topic_key` + `mem_compare supersedes` + `record_user_confirmation`.
26
+ 1b. **Code-contradiction check SHALL.** Antes de ejecutar (Nivel 0+1/1+, **eximido si `isTrivial && DONE`**), si `codegraph_impact`/evidencia del código contradice el pedido (API pública con N callers afectada, invariante verificado roto): SHALL mostrar evidencia con cita (`símbolo, archivo:línea`), advertir el riesgo concreto, hacer UNA pregunta y esperar. Si el usuario insiste → SHALL `record_user_confirmation` + ejecutar. Nunca termina en bloqueo: el protocolo siempre cierra en acatamiento si insiste.
26
27
  2. **Solo propuestas que ayudan SHALL.** Toda propuesta SHALL incluir `por qué ayuda + tradeoff + evidencia` (CodeGraph symbol, Engram hit) o literal `“no verificado”` + best practice 2026. No sugerir por sugerir.
27
28
  3. **Pocas honestas > muchas de relleno SHALL.** Si solo hay una opción honesta, dar una. Si hay 2-3, tabla `coste|riesgo|complejidad`. No inventar para llenar.
28
29
  4. **Si no hay propuesta honesta SHALL decirlo.** Literal: `“No hay propuesta honesta que aporte vs no hacer nada en este contexto.”`
@@ -84,7 +85,7 @@ Router `brainstorming`↔`OpenSpec` por `level`/`estLines`/`fileCount`/`hasAPI`
84
85
  ### 4. Execution
85
86
 
86
87
  1. `skill(execution-mode-evaluation)` en memoria, reusa discovery.
87
- 2. Mostrar análisis → `¿Procedo?` → `record_execution_analysis` → `consume_execution_decision`.
88
+ 2. Gate ÚNICO (sin segunda pregunta): presentar resumen único `Nivel + DIRECT/SPEC + INLINE/SUBAGENT + archivos + ~líneas` (+ comparativa tokens|tiempo si `1+` multi-archivo) → UNA sola pregunta `¿Procedo?` (el usuario puede modificar). Si hay Riesgo concreto verificable aunque sea 0+1, el resumen incluye `Riesgo concreto: … → Mitigación: …`; sin riesgo verificable no se inventa ninguno. Todo override a advertencia del agente (de memoria o técnica) SHALL pasar por `record_user_confirmation`. Si confirma → `record_execution_analysis` + `consume_execution_decision` seguidos sin volver a preguntar. `Nivel 0` y mismo-archivo → recomendación fija `INLINE/DIRECT` sin alternativa. `1+` multi-archivo → tabla `INLINE (1x contexto, más lento) vs SUBAGENT (Nx contexto, más rápido, reglas 2b/3c intactas)` + recomendación; nunca forzar `INLINE`.
88
89
  3. Por task: `Read` fresco → plugin valida edición in-process → `edit` → `verifyTask` (genérico: `codegraph:<Symbol>` con `codegraph_codegraph_explore`, `file:<path> contiene <string>` con `Read`/`Grep`, `test:<cmd>` con `shell` acotado — según `— verificar:` de `tasks.md`) → solo si `verifyTask.ok` → `complete_task`. **NUNCA marques `complete_task` a ojo**; si `verifyTask` falla, reintentá el fix. Vale para SPEC y DIRECT, INLINE y SUBAGENTS.
89
90
 
90
91
  ### 5. Sync y cierre
@@ -5,13 +5,13 @@ agent: build
5
5
 
6
6
  Instala el stack tecnológico de desarrollo para OpenCode. **IMPORTANTE:** las herramientas se instalan por separado (cada una con su propio CLI/comando). `npx ostacky install` solo instala el agente y commands de Ostacky en `.opencode/`. Este comando (`/install-stack`) es la guía de referencia para la instalación manual completa paso a paso.
7
7
 
8
- **Nota:** A partir de v0.9.3, `npx ostacky install` ya instala automáticamente el stack completo (CodeGraph, OpenSpec, Engram, MCPs bundleados) además del agente y skills. Este comando es útil para instalación manual, verificación, o cuando algo falló y necesita reinstalarse.
8
+ **Nota:** A partir de v0.9.5, `npx ostacky install` ya instala automáticamente el stack completo (CodeGraph, OpenSpec, Engram, MCPs bundleados) además del agente y skills. Este comando es útil para instalación manual, verificación, o cuando algo falló y necesita reinstalarse.
9
9
 
10
10
  **RESTRICCIÓN ABSOLUTA:** instalar ÚNICAMENTE para OpenCode. Está terminantemente prohibido crear o modificar archivos en `.claude/`, `.kiro/`, `.cursor/`, `.gemini/`, `.codex/`, `.antigravity/`, `.windsurf/` o cualquier otro directorio de plataformas externas.
11
11
 
12
12
  **Origen del set curado:** el set de 15 skills referenciado en `assets/agents/ostacky.md` está bundleado en `assets/skills/` dentro del paquete npm. La definición del set y su trazabilidad viven en `manifest.json` y `.opencode/ostacky-lock.json`.
13
13
 
14
- ## Scope de instalación — local vs global (desde v0.9.3)
14
+ ## Scope de instalación — local vs global (desde v0.9.5)
15
15
 
16
16
  `npx ostacky install` soporta `--scope local|global|auto` (también `--scope=...`):
17
17
 
@@ -0,0 +1,20 @@
1
+ {
2
+ "lockfileVersion": 2,
3
+ "configVersion": 0,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "ostacky-controller",
7
+ "dependencies": {
8
+ "@modelcontextprotocol/server": "^2.0.0",
9
+ "zod": "^4.0.0",
10
+ },
11
+ },
12
+ },
13
+ "packages": {
14
+ "@modelcontextprotocol/core": ["@modelcontextprotocol/core@2.1.0", "", { "dependencies": { "zod": "^4.2.0" } }, "sha512-YdFj5gMRHr0gYNAhTbQDgjLHiEJdIAUYxomhKseUNye7ZGlISQmUoLh2sveFFCiQ7j2YMffT1kgSND0FSfIFjQ=="],
15
+
16
+ "@modelcontextprotocol/server": ["@modelcontextprotocol/server@2.1.0", "", { "dependencies": { "@modelcontextprotocol/core": "2.1.0", "zod": "^4.2.0" } }, "sha512-FdvwZU5N03O77Suh35JqjAqq5R1ZTEPFtwu6N4BgBq28qrQtrCnpbLL5U2pwYCuE194jAw0xnlOVmIqhfoYuUQ=="],
17
+
18
+ "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="],
19
+ }
20
+ }
@@ -42,6 +42,13 @@ import {
42
42
  getMaxTasks,
43
43
  } from './controller-core.js';
44
44
 
45
+ // --- approved-but-blocked recovery (sin timers) ---
46
+ // El PENDING que espera confirmación del usuario puede esperar indefinidamente (por diseño):
47
+ // acá NO hay timeouts ni watchdogs de idle (ver tests/controller-source.test.ts).
48
+ // Esto solo cuenta consumes fallidos cuando el usuario YA aprobó pero el consume no completa,
49
+ // y al umbral habilita recovery (refresh_decision) sin ejecutar nada por sí mismo.
50
+ const CONSUME_FAIL_THRESHOLD = 3;
51
+
45
52
  // --- Audit JSONL (D5) — single source: src/audit-jsonl.ts ---
46
53
  function getAuditPath(projectRoot) {
47
54
  return join(projectRoot, '.opencode', 'ostacky-audit.jsonl');
@@ -1042,6 +1049,27 @@ class OstackyController {
1042
1049
  return suggestions[state] || `Unexpected state: ${state}`;
1043
1050
  }
1044
1051
 
1052
+ // --- approved-but-blocked: contador de consumes fallidos + hint de recovery ---
1053
+ async #noteConsumeFailure(via, reason) {
1054
+ const count = (this.#state.consumeFailedCount || 0) + 1;
1055
+ this.#state.consumeFailedCount = count;
1056
+ await this.#audit(
1057
+ 'WARN',
1058
+ 'consume_failed',
1059
+ `${via}: ${reason} (attempt ${count}/${CONSUME_FAIL_THRESHOLD})${count >= CONSUME_FAIL_THRESHOLD ? ' — recovery available: call refresh_decision for fresh IDs, then retry' : ''}`
1060
+ );
1061
+ // Durability: el fallo y su evidencia nunca deben perderse en un restart
1062
+ await this.#flushAudit(true);
1063
+ return count;
1064
+ }
1065
+
1066
+ #consumeRecoveryHint(count) {
1067
+ if (count >= CONSUME_FAIL_THRESHOLD) {
1068
+ return 'Recovery: call refresh_decision for fresh decision IDs, then retry the consume. The user approval still stands — only the IDs are repaired.';
1069
+ }
1070
+ return undefined;
1071
+ }
1072
+
1045
1073
  // --- 3.3: Degraded mode ---
1046
1074
  #enterDegradedMode(reason) {
1047
1075
  this.#degraded = true;
@@ -1100,6 +1128,7 @@ class OstackyController {
1100
1128
  fileFingerprints: {},
1101
1129
  expectedTasks: null,
1102
1130
  expectedTaskCount: null,
1131
+ consumeFailedCount: 0,
1103
1132
  error: null,
1104
1133
  });
1105
1134
  await this.#audit(
@@ -1138,6 +1167,7 @@ class OstackyController {
1138
1167
  );
1139
1168
  await this.#transition(to, { error: null });
1140
1169
  await this.#audit('DISCOVERY', 'record_clarification');
1170
+ await this.#flushAudit(true);
1141
1171
  return { state: this.#state.state, revision: this.#state.revision };
1142
1172
  }
1143
1173
 
@@ -1221,6 +1251,7 @@ class OstackyController {
1221
1251
  lastProposal,
1222
1252
  });
1223
1253
  await this.#audit('ROUTE_DECISION_PENDING', 'record_discovery', `level=${level}, default=${defaultChoice}`);
1254
+ await this.#flushAudit(true);
1224
1255
  // 8.2: reasoning sin plan → WARN
1225
1256
  if (!shownToUser && !isTrivial) {
1226
1257
  const auditId = `aud-${Date.now()}-${this.#state.auditSeq}`;
@@ -1352,13 +1383,25 @@ class OstackyController {
1352
1383
  'consume_route_decision'
1353
1384
  );
1354
1385
  }
1355
- if (this.#state.routeDecisionId !== decisionId)
1356
- return this.#makeError('Decision ID mismatch', 'consume_route_decision');
1386
+ if (this.#state.routeDecisionId !== decisionId) {
1387
+ const fails = await this.#noteConsumeFailure('consume_route_decision', 'Decision ID mismatch');
1388
+ const err = this.#makeError('Decision ID mismatch', 'consume_route_decision');
1389
+ const hint = this.#consumeRecoveryHint(fails);
1390
+ return hint ? { ...err, recovery: hint } : err;
1391
+ }
1357
1392
  const to = this.#isAllowedTransition(this.#state.state, 'consume_route_decision', choice);
1358
- if (!to)
1359
- return this.#makeError(`Route ${choice} not allowed from ${this.#state.state}`, 'consume_route_decision');
1360
- await this.#transition(to, { routeChoice: choice });
1393
+ if (!to) {
1394
+ const fails = await this.#noteConsumeFailure('consume_route_decision', `choice ${choice} not allowed`);
1395
+ const err = this.#makeError(
1396
+ `Route ${choice} not allowed from ${this.#state.state}`,
1397
+ 'consume_route_decision'
1398
+ );
1399
+ const hint = this.#consumeRecoveryHint(fails);
1400
+ return hint ? { ...err, recovery: hint } : err;
1401
+ }
1402
+ await this.#transition(to, { routeChoice: choice, consumeFailedCount: 0 });
1361
1403
  await this.#audit(to, 'consume_route_decision', `choice=${choice}`);
1404
+ await this.#flushAudit(true);
1362
1405
  return { state: this.#state.state, revision: this.#state.revision, routeChoice: choice };
1363
1406
  }
1364
1407
 
@@ -1562,6 +1605,7 @@ class OstackyController {
1562
1605
  lastProposal: execLastProposal,
1563
1606
  });
1564
1607
  await this.#audit('EXECUTION_DECISION_PENDING', 'record_execution_analysis');
1608
+ await this.#flushAudit(true);
1565
1609
  // fix-execution-analysis-validation: degraded/early-exit defaulted snapshot → WARN
1566
1610
  if (_snapshotDefaulted) {
1567
1611
  const auditId = `aud-${Date.now()}-${this.#state.auditSeq}`;
@@ -1678,16 +1722,54 @@ class OstackyController {
1678
1722
  'consume_execution_decision'
1679
1723
  );
1680
1724
  }
1681
- if (this.#state.executionDecisionId !== decisionId)
1682
- return this.#makeError('Decision ID mismatch', 'consume_execution_decision');
1725
+ if (this.#state.executionDecisionId !== decisionId) {
1726
+ const fails = await this.#noteConsumeFailure('consume_execution_decision', 'Decision ID mismatch');
1727
+ const err = this.#makeError('Decision ID mismatch', 'consume_execution_decision');
1728
+ const hint = this.#consumeRecoveryHint(fails);
1729
+ return hint ? { ...err, recovery: hint } : err;
1730
+ }
1683
1731
  const to = this.#isAllowedTransition(this.#state.state, 'consume_execution_decision', mode);
1684
- if (!to)
1685
- return this.#makeError(`Mode ${mode} not allowed from ${this.#state.state}`, 'consume_execution_decision');
1686
- await this.#transition(to, { executionMode: mode });
1732
+ if (!to) {
1733
+ const fails = await this.#noteConsumeFailure('consume_execution_decision', `mode ${mode} not allowed`);
1734
+ const err = this.#makeError(
1735
+ `Mode ${mode} not allowed from ${this.#state.state}`,
1736
+ 'consume_execution_decision'
1737
+ );
1738
+ const hint = this.#consumeRecoveryHint(fails);
1739
+ return hint ? { ...err, recovery: hint } : err;
1740
+ }
1741
+ await this.#transition(to, { executionMode: mode, consumeFailedCount: 0 });
1687
1742
  await this.#audit(to, 'consume_execution_decision', `mode=${mode}`);
1743
+ await this.#flushAudit(true);
1688
1744
  return { state: this.#state.state, revision: this.#state.revision, executionMode: mode };
1689
1745
  }
1690
1746
 
1747
+ /**
1748
+ * Regenera decision IDs frescos dentro del PENDING actual.
1749
+ * Recovery para "usuario aprobó pero el consume no completa" (IDs viejos tras
1750
+ * restart/compactación). No ejecuta nada: solo repara el tubo, la aprobación sigue valiendo.
1751
+ */
1752
+ async refreshDecision() {
1753
+ this.#load();
1754
+ const s = this.#state.state;
1755
+ const prevFails = this.#state.consumeFailedCount || 0;
1756
+ if (s === 'ROUTE_DECISION_PENDING') {
1757
+ const routeDecisionId = 'route-' + Date.now();
1758
+ await this.#transition(s, { routeDecisionId, consumeFailedCount: 0 });
1759
+ await this.#audit(s, 'refresh_decision', `fresh routeDecisionId after ${prevFails} failed consume(s)`);
1760
+ await this.#flushAudit(true);
1761
+ return { state: this.#state.state, revision: this.#state.revision, routeDecisionId };
1762
+ }
1763
+ if (s === 'EXECUTION_DECISION_PENDING') {
1764
+ const executionDecisionId = 'exec-' + Date.now();
1765
+ await this.#transition(s, { executionDecisionId, consumeFailedCount: 0 });
1766
+ await this.#audit(s, 'refresh_decision', `fresh executionDecisionId after ${prevFails} failed consume(s)`);
1767
+ await this.#flushAudit(true);
1768
+ return { state: this.#state.state, revision: this.#state.revision, executionDecisionId };
1769
+ }
1770
+ return this.#makeError(`No decision to refresh from state ${s}`, 'refresh_decision');
1771
+ }
1772
+
1691
1773
  async implementationComplete({ force } = {}) {
1692
1774
  this.#load();
1693
1775
  const to = this.#isAllowedTransition(this.#state.state, 'implementation_complete');
@@ -2300,6 +2382,47 @@ class OstackyController {
2300
2382
  };
2301
2383
  }
2302
2384
 
2385
+ // --- fix-new-file-gate: ligadura validate → complete por filePath (mapa + fallback legacy) ---
2386
+ _getValidatedFor(filePath) {
2387
+ const lv = this.#state.lastValidated;
2388
+ if (!lv || !filePath) return null;
2389
+ if (lv.byFile && typeof lv.byFile === 'object' && lv.byFile[filePath]) return lv.byFile[filePath];
2390
+ if (lv.filePath === filePath) return lv; // fallback slot legacy (single)
2391
+ return null;
2392
+ }
2393
+ async _setValidatedFor(filePath, hash, mode) {
2394
+ const prev = this.#state.lastValidated;
2395
+ let byFile = {};
2396
+ if (prev) {
2397
+ if (prev.byFile && typeof prev.byFile === 'object') byFile = { ...prev.byFile };
2398
+ else if (prev.filePath)
2399
+ byFile[prev.filePath] = { hash: prev.hash ?? null, ts: prev.ts ?? 0, mode: prev.mode ?? 'replace' };
2400
+ }
2401
+ if (filePath) byFile[filePath] = { hash: hash ?? null, ts: Date.now(), mode: mode || 'replace' };
2402
+ const keys = Object.keys(byFile);
2403
+ if (keys.length > 50) {
2404
+ keys.sort((a, b) => (byFile[a].ts || 0) - (byFile[b].ts || 0));
2405
+ for (const k of keys.slice(0, keys.length - 50)) delete byFile[k];
2406
+ }
2407
+ this.#state.lastValidated = {
2408
+ byFile,
2409
+ filePath: filePath || null,
2410
+ hash: hash ?? null,
2411
+ ts: Date.now(),
2412
+ mode: mode || 'replace',
2413
+ };
2414
+ }
2415
+ async _clearValidatedFor(filePath) {
2416
+ const lv = this.#state.lastValidated;
2417
+ if (lv && lv.byFile && typeof lv.byFile === 'object') {
2418
+ if (filePath && lv.byFile[filePath]) delete lv.byFile[filePath];
2419
+ if (Object.keys(lv.byFile).length === 0) this.#state.lastValidated = null;
2420
+ else this.#state.lastValidated = { byFile: lv.byFile, filePath: null, hash: null, ts: Date.now() };
2421
+ } else {
2422
+ this.#state.lastValidated = null;
2423
+ }
2424
+ }
2425
+
2303
2426
  // --- O6: Validate edit with fast fingerprint + D6/D4 hard gate + 10.4 freshness ---
2304
2427
  async validateEdit({ oldString, newString, content, taskId, filePath } = {}) {
2305
2428
  this.#load();
@@ -2323,8 +2446,10 @@ class OstackyController {
2323
2446
  await this.#persist();
2324
2447
  } catch {}
2325
2448
  }
2326
- // 10.4: validación de frescura — content debe coincidir con disco si filePath dado
2327
- if (filePath && typeof content === 'string') {
2449
+ // 10.4: validación de frescura — content debe coincidir con disco si filePath dado.
2450
+ // fix-new-file-gate: con oldString "" (intento create) no hay nada que comparar; la rama
2451
+ // create más abajo produce el error preciso (file exists vs EDITABLE).
2452
+ if (filePath && typeof content === 'string' && oldString !== '') {
2328
2453
  try {
2329
2454
  const projectRoot = getProjectRoot(this.#statePath);
2330
2455
  const absolutePath =
@@ -2353,12 +2478,8 @@ class OstackyController {
2353
2478
  ? resolve(filePath)
2354
2479
  : resolve(projectRoot, filePath);
2355
2480
  const currentHash = fastFingerprint(absolutePath);
2356
- if (
2357
- currentHash &&
2358
- hashArg === currentHash &&
2359
- this.#state.lastValidated?.filePath === filePath &&
2360
- this.#state.lastValidated?.hash === currentHash
2361
- ) {
2481
+ const _prevValidated = this._getValidatedFor(filePath);
2482
+ if (currentHash && hashArg === currentHash && _prevValidated && _prevValidated.hash === currentHash) {
2362
2483
  try {
2363
2484
  content = readFileSync(absolutePath, 'utf8');
2364
2485
  } catch {
@@ -2374,11 +2495,8 @@ class OstackyController {
2374
2495
  }
2375
2496
  }
2376
2497
  // 5.3: optimization — si fastFingerprint no cambió, no re-enviar content completo
2377
- if (
2378
- (typeof content !== 'string' || content.length === 0) &&
2379
- filePath &&
2380
- this.#state.lastValidated?.filePath === filePath
2381
- ) {
2498
+ const _optValidated = filePath ? this._getValidatedFor(filePath) : null;
2499
+ if ((typeof content !== 'string' || content.length === 0) && filePath && _optValidated) {
2382
2500
  try {
2383
2501
  const projectRoot = getProjectRoot(this.#statePath);
2384
2502
  const absolutePath =
@@ -2386,7 +2504,7 @@ class OstackyController {
2386
2504
  ? resolve(filePath)
2387
2505
  : resolve(projectRoot, filePath);
2388
2506
  const currentHash = fastFingerprint(absolutePath);
2389
- if (currentHash && currentHash === this.#state.lastValidated.hash) {
2507
+ if (currentHash && currentHash === _optValidated.hash) {
2390
2508
  try {
2391
2509
  const diskContent = readFileSync(absolutePath, 'utf8');
2392
2510
  content = diskContent;
@@ -2394,6 +2512,29 @@ class OstackyController {
2394
2512
  }
2395
2513
  } catch {}
2396
2514
  }
2515
+ // fix-new-file-gate: modo create — oldString "" + archivo inexistente en disco
2516
+ if (typeof oldString === 'string' && oldString === '' && typeof newString === 'string' && filePath) {
2517
+ if (newString.length === 0) {
2518
+ return { outcome: 'CONFLICT', reason: 'newString required for create' };
2519
+ }
2520
+ try {
2521
+ const projectRoot = getProjectRoot(this.#statePath);
2522
+ const absolutePath =
2523
+ filePath.startsWith('/') || /^[A-Za-z]:/.test(filePath)
2524
+ ? resolve(filePath)
2525
+ : resolve(projectRoot, filePath);
2526
+ if (existsSync(absolutePath)) {
2527
+ return { outcome: 'CONFLICT', reason: 'file exists, use replace mode', filePath };
2528
+ }
2529
+ } catch {
2530
+ return { outcome: 'CONFLICT', reason: 'create validation failed', filePath };
2531
+ }
2532
+ await this._setValidatedFor(filePath, null, 'create');
2533
+ try {
2534
+ await this.#persist();
2535
+ } catch {}
2536
+ return { outcome: 'EDITABLE', taskId, mode: 'create' };
2537
+ }
2397
2538
  if (typeof content !== 'string' || typeof oldString !== 'string' || typeof newString !== 'string') {
2398
2539
  return { outcome: 'CONFLICT', reason: 'Missing required fields: content, oldString, newString' };
2399
2540
  }
@@ -2438,7 +2579,7 @@ class OstackyController {
2438
2579
  : resolve(projectRoot, filePath)
2439
2580
  : null;
2440
2581
  const hash = absolutePath ? fastFingerprint(absolutePath) : null;
2441
- this.#state.lastValidated = { filePath: filePath || null, hash, ts: Date.now() };
2582
+ await this._setValidatedFor(filePath || null, hash, 'replace');
2442
2583
  await this.#persist();
2443
2584
  } catch {}
2444
2585
  return { outcome: 'EDITABLE', taskId };
@@ -2483,7 +2624,9 @@ class OstackyController {
2483
2624
  !this.#state.fileFingerprints?.[filePath] &&
2484
2625
  !Object.values(this.#state.tasks || {}).some((t) => t.filePath === filePath))
2485
2626
  );
2486
- if (!this.#state.lastValidated || (filePath && this.#state.lastValidated.filePath !== filePath)) {
2627
+ // fix-new-file-gate: ligadura por-archivo (create o replace); se consume solo ese path
2628
+ const _ligadura = filePath ? this._getValidatedFor(filePath) : this.#state.lastValidated;
2629
+ if (!_ligadura) {
2487
2630
  if (!isNewFile) {
2488
2631
  return {
2489
2632
  error: 'validate required',
@@ -2498,7 +2641,7 @@ class OstackyController {
2498
2641
  `complete_task without prior validate_edit for ${filePath || taskId}${isNewFile ? ' (new file, WARN not BLOCK)' : ''}`
2499
2642
  );
2500
2643
  } else {
2501
- this.#state.lastValidated = null;
2644
+ await this._clearValidatedFor(filePath);
2502
2645
  }
2503
2646
 
2504
2647
  // --- Generico: verificar task antes de marcar completa (evita invento)
@@ -2853,7 +2996,7 @@ function safeHandler(fn, options = {}) {
2853
2996
 
2854
2997
  const server = new McpServer({
2855
2998
  name: 'ostacky-controller',
2856
- version: '0.9.3',
2999
+ version: '0.9.5',
2857
3000
  });
2858
3001
 
2859
3002
  server.registerTool(
@@ -2971,6 +3114,19 @@ server.registerTool(
2971
3114
  })
2972
3115
  );
2973
3116
 
3117
+ server.registerTool(
3118
+ 'refresh_decision',
3119
+ {
3120
+ description:
3121
+ 'Regenerate fresh decision IDs within the current PENDING state (recovery when the user approved but consume keeps failing with stale IDs; executes nothing)',
3122
+ inputSchema: z.object({}),
3123
+ },
3124
+ safeHandler(async () => {
3125
+ log('tool:refresh_decision');
3126
+ return await controller.refreshDecision();
3127
+ })
3128
+ );
3129
+
2974
3130
  server.registerTool(
2975
3131
  'implementation_complete',
2976
3132
  {
@@ -3442,7 +3598,7 @@ function setupGracefulShutdown(ctrl) {
3442
3598
  }
3443
3599
 
3444
3600
  async function main() {
3445
- log('Starting ostacky-controller MCP v0.9.3...');
3601
+ log('Starting ostacky-controller MCP v0.9.5...');
3446
3602
  log('State path:', { path: statePath });
3447
3603
  // Clean up stale tmp/lock files from previous runs
3448
3604
  cleanupTmpFiles(statePath);