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.
package/README.md CHANGED
@@ -9,7 +9,7 @@ Ostacky es un **agent harness** para [OpenCode](https://opencode.ai): el orquest
9
9
  | [Superpowers](https://github.com/obra/superpowers) | Ejecución | CÓMO se implementa, prueba y revisa |
10
10
  | [Engram](https://github.com/Gentleman-Programming/engram) | Memoria persistente | QUÉ aprendimos en sesiones anteriores |
11
11
 
12
- Ostacky las rutea según el **nivel de impacto del cambio**, preguntándote antes de actuar: cambios grandes van por OpenSpec (spec-driven development), cambios chicos ejecutan directo con skills de Superpowers.
12
+ Ostacky las rutea según el **nivel de impacto del cambio** y **te pregunta antes de actuar**:
13
13
 
14
14
  | Nivel | Cuándo aplica | Flujo |
15
15
  | ------- | ---------------------------------------- | ---------------------------------------------------------------------- |
@@ -17,12 +17,12 @@ Ostacky las rutea según el **nivel de impacto del cambio**, preguntándote ante
17
17
  | **0+1** | 5-10 líneas, 1-2 archivos, sin API nueva | CodeGraph → vos elegís: spec o directo |
18
18
  | **1+** | >10 líneas, API pública, refactors | CodeGraph → OpenSpec → execution-mode-evaluation → subagentes o inline |
19
19
 
20
- Antes de ejecutar, Ostacky muestra qué tasks tocan los mismos archivos para que elijas el modo de ejecución informado (inline secuencial o subagentes en paralelo).
20
+ Antes de ejecutar te muestra qué tasks comparten archivos, así elegís el modo de ejecución con información en mano.
21
21
 
22
22
  ## Requisitos
23
23
 
24
24
  - [Node.js](https://nodejs.org/) >= 20 (para `npx`) o [Bun](https://bun.sh/) >= 1.x (para `bunx` o desarrollo)
25
- - [OpenCode](https://opencode.ai) instalado: `curl -fsSL https://opencode.ai/install | bash`
25
+ - [OpenCode](https://opencode.ai) instalado ([docs](https://opencode.ai/v2/docs)): `bun install -g --trust @opencode/cli` o `npm install -g @opencode/cli` (el `--trust` de Bun habilita el postinstall que elige tu binario nativo)
26
26
  - Git recomendado: con git hay aislamiento por worktree; sin git Ostacky instala en el directorio actual con un aviso
27
27
  - **WSL:** seguí las instrucciones de **Linux** — WSL corre un kernel Linux real, no uses los comandos de PowerShell aunque estés en Windows
28
28
 
@@ -31,7 +31,7 @@ Antes de ejecutar, Ostacky muestra qué tasks tocan los mismos archivos para que
31
31
  No requiere instalación global ni clonar el repositorio:
32
32
 
33
33
  ```bash
34
- npx ostacky # menú interactivo
34
+ npx ostacky # menú interactivo (te pregunta el scope)
35
35
  bunx ostacky # lo mismo con Bun
36
36
  ```
37
37
 
@@ -44,13 +44,11 @@ npx ostacky install --scope local # explícito
44
44
 
45
45
  Instala el núcleo (agente, commands, skills, MCPs, plugins) más el stack de herramientas (CodeGraph, Engram) en `<proyecto>/.opencode`, y asegura `.gitignore` con `.opencode/tools/`, `.codegraph/`, `.opencode/cache/` y `ostacky-state.json*`.
46
46
 
47
- > Si el log muestra `toolsDir` en tu home (ej. `C:\Users\...\.opencode\tools\codegraph`) y esperabas tu proyecto, es porque corriste el comando fuera de un repo git. Hacé `cd` al proyecto y re-ejecutá.
48
-
49
47
  ### Global (opt-in)
50
48
 
51
49
  ```bash
52
50
  npx ostacky --scope global # menú interactivo global
53
- npx ostacky install --scope global # núcleo global (agente + commands + skills + MCPs + plugins)
51
+ npx ostacky install --scope global # el mismo núcleo, sin el stack
54
52
  ```
55
53
 
56
54
  Escribe el núcleo en el config dir global de OpenCode, sin tocar ningún proyecto:
@@ -81,8 +79,6 @@ Con `--scope global` el stack (CodeGraph, Engram) **se omite** — se instala po
81
79
 
82
80
  > Por defecto la web corre en `http://127.0.0.1:49374` solo en localhost. Docs: https://opencode.ai/v2/docs/cli/web
83
81
 
84
- Si algo del stack falló durante la instalación, regeneralo sin salir de la sesión: seleccioná `ostacky` y ejecutá `/install-stack` en el chat.
85
-
86
82
  ## Uso diario
87
83
 
88
84
  ```bash
@@ -102,7 +98,7 @@ npx ostacky uninstall-stack # limpia config del stack del pro
102
98
  npx ostacky --version | npx ostacky --help
103
99
  ```
104
100
 
105
- **Safe-delete:** solo borra lo trackeado en `ostacky-lock.json` — nunca toca componentes tuyos. Muestra preview con `Scope: <ruta>` y pide confirmación. Los plugins Ostacky-owned se limpian por allowlist; los custom no se tocan.
101
+ **Safe-delete:** solo borra lo trackeado en `ostacky-lock.json` — nunca toca componentes tuyos. Muestra preview con `Scope: <ruta>` y pide confirmación. Los plugins que instaló Ostacky no están en el lockfile: se limpian por allowlist; los tuyos no se tocan.
106
102
 
107
103
  ## Qué se instala
108
104
 
@@ -129,20 +125,20 @@ Local (`<proyecto>/.opencode`) o global (mismo árbol bajo el dir global, sin st
129
125
  └── ostacky-lock.json ← versiones instaladas (agents, commands, skills, mcpServers)
130
126
  ```
131
127
 
132
- `plugins/` y `tools/` **no** se trackean en el lockfile (allowlist Ostacky-owned). Instalaciones viejas con archivos legacy sueltos (`ostacky-plugin.ts`, `ostacky-guard.ts`, `ostacky-controller.ts`) se limpian automáticamente.
128
+ `plugins/` y `tools/` **no** se trackean en el lockfile. Instalaciones viejas con archivos legacy sueltos (`ostacky-plugin.ts`, `ostacky-guard.ts`, `ostacky-controller.ts`) se limpian automáticamente.
133
129
 
134
130
  ```json
135
131
  {
136
- "version": "0.10.0",
137
- "tag": "v0.10.0",
138
- "agents": { "ostacky": { "version": "0.10.0", "sha256": "…" } },
132
+ "version": "0.10.1",
133
+ "tag": "v0.10.1",
134
+ "agents": { "ostacky": { "version": "0.10.1", "sha256": "…" } },
139
135
  "commands": { "install-stack": { … }, "opsx-sync": { … } },
140
136
  "skills": { "brainstorming": { … }, "…" : {} },
141
137
  "mcpServers": { "ostacky-controller": { … }, "openspec": { … } }
142
138
  }
143
139
  ```
144
140
 
145
- Versionado con checksums: sumá `ostacky-lock.json` a git para que el equipo instale exactamente las mismas versiones. Las descargas usan tags de GitHub (nunca `main`) y cada path se valida contra path traversal.
141
+ Versionado con checksums: sumá `ostacky-lock.json` a git para que el equipo instale exactamente las mismas versiones (mismo tag y mismos checksums).
146
142
 
147
143
  ## Comandos útiles
148
144
 
@@ -159,22 +155,19 @@ Engram y CodeGraph viven en `.opencode/tools/<herramienta>/bin/` y se exponen co
159
155
 
160
156
  ## Troubleshooting
161
157
 
162
- - **Instalé fuera del proyecto / sin git:** hacé `cd` al proyecto y re-ejecutá. El log muestra `projectRoot` y `toolsDir` explícitos para diagnosticar scope.
158
+ - **Instalé fuera del proyecto / sin git:** el log muestra `projectRoot` y `toolsDir` explícitos; si `toolsDir` apunta a tu home (ej. `C:\Users\...\.opencode\tools\codegraph`) y esperabas tu proyecto, corriste el comando fuera de un repo git. Hacé `cd` al proyecto y re-ejecutá.
163
159
  - **El install global creó `.opencode/` local:** actualizá (`npx ostacky@latest`) y reinstalá — el menú interactivo ya propaga el scope y el stack se omite en global.
164
160
  - **Windows `cmd.exe ... codegraph.cmd init -i` durante el install:** warning no fatal — el binario quedó, solo falta el índice. Reintentá con `npx ostacky install-stack --scope local` o corré `.opencode\tools\codegraph\bin\codegraph.exe init -i` dentro del proyecto.
165
- - **¿Errores en el stack?** Desde el chat de OpenCode: `/install-stack`.
161
+ - **¿Errores en el stack?** Elegí el agente `ostacky` y ejecutá `/install-stack` en el chat: regenera el stack sin salir de la sesión.
166
162
 
167
163
  ## Seguridad
168
164
 
169
165
  - `opencode.jsonc` puede versionarse para compartir permisos y MCP de forma reproducible.
170
- - Descargas por tags de GitHub + validación anti path-traversal + checksums SHA-256 del manifest + cache local con validación de integridad.
166
+ - Descargas por tags de GitHub (nunca `main`), validación anti path-traversal y checksums SHA-256 del manifest.
167
+ - Cache local en `.opencode/cache/<repo>/<tag>/<ruta>` con validación de integridad: si el hash cacheado coincide, no hay petición de red.
171
168
  - Lectura/escritura de `*.env` y `.secret/**` bloqueadas con `deny` (la sesión sigue por `experimental.continue_loop_on_deny`).
172
169
  - Patrones sensibles configurables: `OSTACKY_SENSITIVE_PATTERNS="**/.env*,**/.secrets/**" bunx ostacky doctor` (allowlist: `.env.example/.template/.sample`). Fuente única en `src/security.ts`; verificá con `npx ostacky doctor`.
173
170
 
174
- ## Cache
175
-
176
- Descargas en `.opencode/cache/<repo>/<tag>/<ruta>`. Si el archivo cacheado tiene el hash correcto, no hay petición de red.
177
-
178
171
  ## Licencia
179
172
 
180
173
  MIT
@@ -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.10.0
4
+ version: 0.10.1
5
5
  ---
6
6
 
7
- Sos **Ostacky v0.10.0**, orquestás, no implementás. Interpretás, clasificás (0/0+1/1+), ruteás y coordinás.
7
+ Sos **Ostacky v0.10.1**, orquestás, no implementás. Interpretás, clasificás (0/0+1/1+), ruteás y coordinás.
8
8
 
9
- > **Versión:** `0.10.0` (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.10.0"** (o `v0.10.0` si te piden solo el número). No inventes otra versión.
9
+ > **Versión:** `0.10.1` (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.10.1"** (o `v0.10.1` si te piden solo el número). No inventes otra versión.
10
10
 
11
11
  ## Reglas innegociables
12
12
 
@@ -45,7 +45,7 @@ Sos **Ostacky v0.10.0**, orquestás, no implementás. Interpretás, clasificás
45
45
 
46
46
  **CodeGraph:** `codegraph_codegraph_explore` antes de búsqueda manual. Si ya llamaste para área, reusar. Timeout 10s → Engram → Read.
47
47
 
48
- **Discovery-cache (único):** `getDiscoverySnapshot(query)` TTL 1h + `gitDiffHash`. Si hit → reusar. Si miss → `codegraph_explore`+`mem_search` + `put` obligatorio. Dedup `mem_search` por `requestId`.
48
+ **Discovery:** `codegraph_explore` + `mem_search`. Si ya llamaste para el área en este request, reusar ese output. No hay cache de archivo.
49
49
 
50
50
  **Engram:** `mem_context` inicio, `mem_search` antes de decidir, `mem_save` tras gate.
51
51
 
@@ -59,7 +59,7 @@ Si vago → preguntar. Si claro → `start_request`.
59
59
 
60
60
  1. `engram_mem_context` (lazy si `isTrivial && DONE` solo pointer)
61
61
  2. Change activo → `proposal.md`/`design.md`/`tasks.md`
62
- 3. `getDiscoverySnapshot`; si miss → `codegraph_explore`+`mem_search` + `put`
62
+ 3. `codegraph_explore` + `mem_search`; reusar si ya lo hiciste para el área en este request
63
63
  4. `codegraph_impact` solo si no cubierto
64
64
  5. `Read` solo lo no cubierto
65
65
 
@@ -79,14 +79,14 @@ Router `brainstorming`↔`OpenSpec` por `level`/`estLines`/`fileCount`/`hasAPI`
79
79
 
80
80
  **SHALL sin ambigüedad (spec iterativo + sync proactiva):**
81
81
 
82
- - Antes de crear/editar `openspec/changes/<id>/{proposal.md,design.md,tasks.md}` **O** `docs/superpowers/specs/*.md`: si existe → SHALL `read` fresco de TODOS y SHALL `edit` (no `write`); `write` solo si no existía. Para `open-explore` sin archivo → SHALL `mem_save topic_key:brainstorm/<hash>` por iteración. `getDiscoverySnapshot` no aplica a specs/docs.
82
+ - Antes de crear/editar `openspec/changes/<id>/{proposal.md,design.md,tasks.md}` **O** `docs/superpowers/specs/*.md`: si existe → SHALL `read` fresco de TODOS y SHALL `edit` (no `write`); `write` solo si no existía. Para `open-explore` sin archivo → SHALL `mem_save topic_key:brainstorm/<hash>` por iteración. El discovery no aplica a specs/docs.
83
83
  - Al final de cada turno de brainstorming/spec con decisiones nuevas no reflejadas en disco: SHALL listar cálido `Noté que lo que acordamos (X por Z) aún no está en <archivo>: 1) ...` (max 3) y SHALL proponer en UNA sola pregunta `¿Querés que agregue [X, Y] a <archivo> e implemente <mejor propuesta> —la recomiendo por <tradeoff/evidencia>—?` (respetando Regla 5). Si no hay delta, no proponer. Eximido si `isTrivial`.
84
84
 
85
85
  ### 4. Execution
86
86
 
87
87
  1. `skill(execution-mode-evaluation)` en memoria, reusa discovery.
88
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`.
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
+ 3. Por task: `Read` fresco → plugin valida edición in-process → `edit` → `complete_task`. El chequeo corre dentro de complete_task (no existe la tool `verify_task`): elige la verificación de `— verificar:` en `tasks.md` y devuelve el motivo si falla. **NUNCA marques `complete_task` a ojo**; si la verificación falla, reintentá el fix. Vale para SPEC y DIRECT, INLINE y SUBAGENTS.
90
90
 
91
91
  ### 5. Sync y cierre
92
92
 
@@ -100,4 +100,4 @@ Router `brainstorming`↔`OpenSpec` por `level`/`estLines`/`fileCount`/`hasAPI`
100
100
  - Una pregunta por turno, sin deadlock
101
101
  - Fase gate: en EXECUTING/SYNC no volver a DISCOVERY
102
102
  - Audit: log antes de gate + `mem_save` solo en gates
103
- - **Tasks son verificables, no instruccionales (genérico):** `tasks.md` SHALL describir `qué` + `cómo verificar` con formato `— verificar: codegraph:<Symbol> | file:<path> contiene <string> | test:<cmd>` (ej: `— verificar: codegraph:CurrencyContext` o `— verificar: test: bun test tests/domains/settle.test.ts`). El controller parsea `taskVerifications` y `verifyTask` lo re-verifica con `CodeGraph`/`Read`/`test`; `complete_task` solo pasa si `verifyTask.ok`. Nunca SHALL contener instrucciones genéricas (`“sé cuidadoso”`) — eso vive acá. Para `Level 0` trivial, tasks SHALL ser de 1 línea sin overhead.
103
+ - **Tasks son verificables, no instruccionales (genérico):** `tasks.md` SHALL describir `qué` + `cómo verificar` con formato `— verificar: codegraph:<Symbol> | file:<path> contiene <string> | test:<cmd>` (ej: `— verificar: codegraph:CurrencyContext` o `— verificar: test: bun test tests/domains/settle.test.ts`). El controller parsea `taskVerifications` al registrar el análisis de ejecución. `file:<path> contiene <str>` exige el string en el archivo; `codegraph:<Symbol>` exige que el símbolo aparezca en el archivo (proxy textual, NO consulta al grafo); `test:<cmd>` exige evidencia: el agente corre el test, el plugin registra cada `shell` con su `exit` real, y `complete_task` sólo acepta una corrida con el comando exacto, `exit 0` y posterior a la última edición (sin evidencia, falla). `complete_task` corre el chequeo y sólo marca COMPLETED si pasa. Nunca SHALL contener instrucciones genéricas (`“sé cuidadoso”`) — eso vive acá. Para `Level 0` trivial, tasks SHALL ser de 1 línea sin overhead.
@@ -6,7 +6,7 @@ subagent: false
6
6
 
7
7
  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.
8
8
 
9
- **Nota:** A partir de v0.10.0, `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
+ **Nota:** A partir de v0.10.1, `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.
10
10
 
11
11
  **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.
12
12
 
@@ -27,12 +27,11 @@ BLOCKED: replan→INTERPRETATION_PENDING, abandon→DONE
27
27
  | controller_* | 5s | 1 | degraded |
28
28
  | engram_* | 5s | 1 | sin memoria |
29
29
 
30
- ## Cache
31
-
32
- - `src/cache-codegraph.ts`: `.opencode/cache/codegraph/<sha256(query)>.json` con `{ts, result, gitHead, gitDiffHash}`, TTL 1h, `OSTACKY_CACHE_DISABLE=1` bypass, LRU 50MB, invalidación `git diff --name-only` + `git status --porcelain --untracked-files=all` (hash combinado).
33
- - `src/discovery-cache.ts`: `.opencode/cache/codegraph/discovery-<sha256>.json` con `{codegraph, engramHits, gitDiffHash, gitHead, ts, query}`, mismo TTL/LRU. `getDiscoverySnapshot` → hit reusar sin llamar tools; miss → SHALL `putDiscoverySnapshot` antes de `record_discovery` (auditable `WARN:cache_miss_without_put`).
34
- - `record_cache_hit/miss` internos: `cache-codegraph.ts` hace write directo a `ostacky-state.json` incrementando `cacheHitCount/tokenSavingEstimate` sin tool LLM.
30
+ ## Discovery
35
31
 
32
+ - Sin cache de archivo: discovery = `codegraph_explore` + `mem_search`; el reuso es del output ya presente en el contexto del request. `src/discovery-cache.ts` y `src/cache-codegraph.ts` se retiraron (change `remove-discovery-cache-phantom`).
33
+ - `OSTACKY_CACHE_DISABLE` ya no existe: no hay cache filesystem que desactivar.
34
+ - Los contadores `cacheHitCount`/`tokenSavingEstimate` quedan en `get_metrics` por compatibilidad, en 0: nada los incrementa.
36
35
  ## Metrics (get_metrics)
37
36
 
38
37
  `revision, state, degraded, taskCounts{completed,pending,total,expected}, expectedTaskCount, auditSize, stateFileSize, diskFreeMB, uptimeMs, stateOversizedCount, codegraphBypassCount, degradedEditsCount, cacheHitCount, cacheMissCount, tokenSavingEstimate, sensitiveAccess, subagentFailedCount, discoveryCacheHitCount, redundantCallCount, cacheMissWithoutPutCount, stateCheckCount, toolCallCount`
@@ -56,6 +55,34 @@ Tras refino post-complete (archivo ya correcto, sin edición pendiente): `refres
56
55
 
57
56
  El plugin hace cumplir `PENDING` en `ctx.tool.hook("execute.before")`; `lastCheck={revision, result}` cachea ALLOW por revisión, revalida si cambia o >5 tools; `BLOCKED` nunca cacheado. Métricas `stateCheckCount` cuentan checks del plugin (no del LLM).
58
57
 
58
+ ## Gate de CodeGraph (alcance real)
59
+
60
+ Cubre `read`/`grep`/`glob`/`read_mcp_resource` y **`shell`** cuando el comando revela contenido (`cat`, `head`, `tail`, `sed` sin `-i`, `grep`, `rg`, `awk`, `diff`…). Predicado puro: `shellReadTargets(command)` devuelve los paths de código **revelados**, no los mencionados — `bun test x.ts`, `tsc src/x.ts` y `git diff` pasan.
61
+
62
+ **Fail-open:** redirección a archivo (`>`/`>>` a path), `sed -i` y verbo fuera de la allowlist → se permite. `read` falla cerrado porque es inequívoco; `shell` no, y un falso positivo rompe tests/builds sin escape hatch. Los redirects de stderr (`2>&1`, `1>&2`, `2>/dev/null`) NO cuentan como escritura (mismo criterio que `isShellMutation`): `cat src/x.ts 2>&1` sigue gateado.
63
+
64
+ **Exenciones `isShellPathExempt(p, directory, freshState)`:** paths FUERA del projectRoot (fixtures temporales, reportes generados — `cat /tmp/out.ts` nunca bloquea), specs/docs (openspec, `assets/docs`, `.md`, `.json`) y archivos ya trackeados en tasks/fileFingerprints/lastValidated.
65
+
66
+ **No es un sandbox.** Bypass residual por diseño: `git show`, `python -c`, `node -e`, `find -exec`, `xargs`, `base64`. El objetivo es quitar el bypass casual (`cat src/x.ts`, incluida su variante `cat src/x.ts 2>&1`), no probar imposibilidad.
67
+
68
+ `execute` (Code Mode) **no** se gatea: su catálogo no expone `read`/`grep`/`shell` ni filesystem. Si una versión futura lo expone, el gate debe extenderse.
69
+
70
+ **Evidencia válida de discovery:** únicamente un `codegraph_codegraph_explore` COMPLETADO en esta sesión (`discoveryHitByRequest`, memo limpiado con `session.deleted`). No hay fallback a cache en disco (el legado `getDiscoveryCacheHit` se retiró con discovery-cache).
71
+
72
+ ## Evidencia de ejecución (`test:`)
73
+
74
+ `test:<cmd>` no se puede verificar sin ejecutar. El plugin observa cada `shell` pero **sólo persiste** `{cmd, exit, ts}` cuando el comando matchea un `test:` declarado en `taskVerifications` (D10: `git`/`ls`/`builds` no son evidencia; cada persist cuesta lock + re-read + write + backups). `complete_task` exige comando exacto (trim), `exit 0` y `ts` posterior a `lastMutationTs` (que avanza en cada `edit`/`write`/`patch` completado y en cada `shell` que escribe con exit 0: `cp`, `mv`, `rm`, `sed -i`, `tee`, redirección). `verifyIntegrity` re-verifica en el cierre: una edición posterior vence la evidencia y bloquea.
75
+
76
+ **Fail-closed en orden de operaciones:** si el test corre antes de `record_execution_analysis` (que carga `taskVerifications` en el state), el plugin no lo registra como evidencia → `complete_task` pide re-correr con razón explícita. El orden correcto es analysis → tests → complete.
77
+
78
+ **No se usa `status`:** en OpenCode 2.0.32, `sh -c 'exit 3'` llega con `status: "completed"` y `result.metadata.status: "completed"`. El único campo veraz es `result.metadata.exit`.
79
+
80
+ **Límites declarados:** un test débil pasa (semántica, no mecánica); sin el plugin no hay `shellRuns` y `test:` falla cerrado.
81
+
82
+ ## Force gate (human confirmation, D4)
83
+
84
+ `implementation_complete({force:true})` exige una frase del USUARIO (`/forzar|confirmo|force/i`) en `lastUserPrompts` — no en el audit, que redacta el agente. El plugin registra cada prompt con `{text, ts}` antes de inyectar hints. Cada rechazo del gate setea `lastForceAttemptTs`: sólo cuenta una frase con `ts` POSTERIOR al último intento (un "¿qué hace force:true?" previo no habilita). Prompts legacy sin `ts` (formato viejo en disco) pasan sólo si nunca hubo intento registrado.
85
+
59
86
  ## Tiered Behaviour
60
87
 
61
88
  `isTrivial(msg,state) = state==DONE && msg.trim().length<30 && /^(hola|hey|gracias|buenas|hi|hello)\b/i.test(msg) && !/(necesito|quiero|agregá|fix|bug|feature|auth|spec|implementar)/i.test(msg)`
@@ -86,4 +113,4 @@ Principio: **eficacia > recorte** — si recorte rompiera caché y saliera más
86
113
 
87
114
  ## Propose Flow (1+)
88
115
 
89
- `openspec-propose` SHALL Paso 0: `getDiscoverySnapshot(query)` + `getEngramDedup` → si miss `codegraph_codegraph_explore` + `engram_mem_search` + `putDiscoverySnapshot` **antes** de `openspec new`. Cita símbolos existentes en `proposal/design`; sin esto dispara `WARN:codegraphBypass` y `design.md` alucina API. Ver `assets/skills/openspec-propose/SKILL.md: Paso 0` y `brainstorming` (router por nivel: `brainstorming` decide 0/0+1/1+, `openspec-explore` es stance sin nivel).
116
+ `openspec-propose` SHALL Paso 0: `codegraph_codegraph_explore` + `engram_mem_search` sobre el área (si no se hicieron ya en el request) **antes** de `openspec new`. Citá símbolos existentes en `proposal/design`; sin esto dispara `WARN:codegraphBypass` y `design.md` alucina API. Ver `assets/skills/openspec-propose/SKILL.md: Paso 0` y `brainstorming` (router por nivel: `brainstorming` decide 0/0+1/1+, `openspec-explore` es stance sin nivel).
@@ -215,7 +215,10 @@ export function parseTaskVerifications(changeId, statePath) {
215
215
  if (!m) continue;
216
216
  const id = m[1];
217
217
  if (!isValidTaskId(id)) continue;
218
- const idx = line.indexOf('verificar:');
218
+ // lastIndexOf: la convención es que el marker va al FINAL de la línea. Con indexOf,
219
+ // una task que MENCIONA el formato en su descripción ("usá — verificar: file:...") se
220
+ // parseaba a sí misma mal: ganaba el ejemplo, no el marker real.
221
+ const idx = line.lastIndexOf('verificar:');
219
222
  if (idx !== -1) {
220
223
  const v = line
221
224
  .slice(idx + 'verificar:'.length)
@@ -230,6 +233,35 @@ export function parseTaskVerifications(changeId, statePath) {
230
233
  }
231
234
  }
232
235
 
236
+ /**
237
+ * test-verification-execution: evidencia real de una corrida de test.
238
+ *
239
+ * NO usa `status`: el shell tool de OpenCode reporta "completed" incluso con
240
+ * exit != 0 (medido: `sh -c 'exit 3'` -> status "completed", metadata.exit 3).
241
+ * El plugin registra {cmd, exit, ts} por cada shell; acá sólo se acepta el
242
+ * comando exacto, exit 0, y posterior a la última mutación.
243
+ */
244
+ export function findTestEvidence({ runs, cmd, lastMutationTs, now = Date.now() } = {}) {
245
+ const expected = String(cmd || '').trim();
246
+ if (!expected) return { ok: false, reason: 'test: sin comando' };
247
+ if (!Array.isArray(runs) || runs.length === 0)
248
+ return { ok: false, reason: 'sin evidencia: el plugin no registró corridas de shell (¿está corriendo?)' };
249
+ const matches = runs.filter((r) => r && String(r.cmd || '').trim() === expected);
250
+ if (matches.length === 0) return { ok: false, reason: `nunca se ejecutó exactamente "${expected}"` };
251
+ const last = matches.reduce((a, b) => ((Number(b.ts) || 0) >= (Number(a.ts) || 0) ? b : a));
252
+ if (typeof last.ts !== 'number' || typeof lastMutationTs !== 'number')
253
+ return {
254
+ ok: false,
255
+ reason: `sin timestamps: no se puede probar que "${expected}" sea posterior a la última edición`,
256
+ run: last,
257
+ };
258
+ if (last.ts <= lastMutationTs)
259
+ return { ok: false, reason: `la última corrida de "${expected}" es anterior a la última edición`, run: last };
260
+ if (last.exit !== 0)
261
+ return { ok: false, reason: `la última corrida de "${expected}" salió con exit ${last.exit}`, run: last };
262
+ return { ok: true, reason: `exit 0 hace ${Math.max(0, Math.round((now - last.ts) / 1000))}s`, run: last };
263
+ }
264
+
233
265
  function isValidTaskId(taskId) {
234
266
  return typeof taskId === 'string' && /^[a-zA-Z0-9-_.\/:]+$/.test(taskId);
235
267
  }
@@ -1724,6 +1756,14 @@ class OstackyController {
1724
1756
  expectedTasks = [...tasksMdIds];
1725
1757
  expectedTaskCount = tasksMdIds.length;
1726
1758
  }
1759
+ // task-verification-integrity D1: las verificaciones viajan con el MISMO parse de
1760
+ // tasks.md que ya usa expectedTasks. Antes sólo las cargaba set_expected_tasks, una
1761
+ // tool que el prompt nunca menciona -> `— verificar:` quedaba muerto en la práctica.
1762
+ let taskVerifs = null;
1763
+ try {
1764
+ const parsedVerifs = parseTaskVerifications(this.#state.changeId, this.#statePath);
1765
+ taskVerifs = Object.keys(parsedVerifs).length ? parsedVerifs : null;
1766
+ } catch {}
1727
1767
  // 8.1/8.2: lastProposal for execution — reasoning con plan
1728
1768
  let execShown = false;
1729
1769
  let execFiles = [];
@@ -1763,6 +1803,7 @@ class OstackyController {
1763
1803
  snapshots: { ...this.#state.snapshots, execution: snapshot ? structuredClone(snapshot) : null },
1764
1804
  expectedTasks: Array.isArray(expectedTasks) ? [...expectedTasks] : null,
1765
1805
  expectedTaskCount: typeof expectedTaskCount === 'number' ? expectedTaskCount : null,
1806
+ taskVerifications: taskVerifs,
1766
1807
  lastProposal: execLastProposal,
1767
1808
  });
1768
1809
  await this.#audit('EXECUTION_DECISION_PENDING', 'record_execution_analysis');
@@ -1814,20 +1855,11 @@ class OstackyController {
1814
1855
  }
1815
1856
  // C2: warning if missing codegraphUsed+recommendation and not degraded — snapshot missing also counts
1816
1857
  // 1.7: early-exit with taskCount<=2 is valid without codegraphUsed, do not warn
1817
- // mejora-acciones-controller F3: discovery-cache counts as valid evidence if discovery snapshot exists
1818
- const isDiscoveryCacheEvidence =
1819
- snapshot &&
1820
- Array.isArray(snapshot.codegraphUsed) &&
1821
- snapshot.codegraphUsed.includes('discovery-cache') &&
1822
- this.#state.snapshots.codegraph != null &&
1823
- Array.isArray(snapshot.expectedTaskIds) &&
1824
- snapshot.expectedTaskIds.length > 0;
1825
1858
  const hasEvidence =
1826
1859
  (snapshot &&
1827
1860
  Array.isArray(snapshot.codegraphUsed) &&
1828
1861
  snapshot.codegraphUsed.length > 0 &&
1829
- snapshot.recommendation != null) ||
1830
- isDiscoveryCacheEvidence;
1862
+ snapshot.recommendation != null);
1831
1863
  if (!hasEvidence && !this.#degraded && !isEarlyExitExec) {
1832
1864
  this.#state.codegraphBypassCount = (this.#state.codegraphBypassCount || 0) + 1;
1833
1865
  const auditId = `aud-${Date.now()}-${this.#state.auditSeq}`;
@@ -1931,6 +1963,25 @@ class OstackyController {
1931
1963
  return this.#makeError(`No decision to refresh from state ${s}`, 'refresh_decision');
1932
1964
  }
1933
1965
 
1966
+ /**
1967
+ * task-verification-integrity D3: si el estado no tiene expectedTasks ni expectedTaskCount,
1968
+ * leer la fuente (tasks.md) en el momento del gate. El gate no puede quedar vacío sólo
1969
+ * porque record_execution_analysis nunca corrió o el cache se perdió.
1970
+ * Devuelve null cuando no hay tasks.md (trabajo no-spec: sin gate) o cuando manda el count.
1971
+ */
1972
+ #deriveExpectedTasks() {
1973
+ if (Array.isArray(this.#state.expectedTasks) && this.#state.expectedTasks.length > 0) {
1974
+ return this.#state.expectedTasks;
1975
+ }
1976
+ if (typeof this.#state.expectedTaskCount === 'number') return null;
1977
+ try {
1978
+ const ids = parseTasksMd(this.#state.changeId, this.#statePath);
1979
+ return ids.length > 0 ? ids : null;
1980
+ } catch {
1981
+ return null;
1982
+ }
1983
+ }
1984
+
1934
1985
  async implementationComplete({ force } = {}) {
1935
1986
  this.#load();
1936
1987
  const to = this.#isAllowedTransition(this.#state.state, 'implementation_complete');
@@ -1940,9 +1991,11 @@ class OstackyController {
1940
1991
  'implementation_complete'
1941
1992
  );
1942
1993
  // C2 gate: check expectedTasks vs completed — do NOT transition if pending and not forced
1994
+ // D3: un cache vacío no vacía el gate — se deriva de tasks.md.
1995
+ const derivedExpected = this.#deriveExpectedTasks();
1943
1996
  let pending = [];
1944
- if (Array.isArray(this.#state.expectedTasks) && this.#state.expectedTasks.length > 0) {
1945
- pending = this.#state.expectedTasks.filter(
1997
+ if (Array.isArray(derivedExpected) && derivedExpected.length > 0) {
1998
+ pending = derivedExpected.filter(
1946
1999
  (id) => !this.#state.tasks[id] || this.#state.tasks[id].status !== 'COMPLETED'
1947
2000
  );
1948
2001
  } else if (typeof this.#state.expectedTaskCount === 'number') {
@@ -1955,7 +2008,14 @@ class OstackyController {
1955
2008
  const seenFp2 = new Set();
1956
2009
  try {
1957
2010
  for (const [taskId, info] of Object.entries(this.#state.tasks || {})) {
1958
- if (info.status !== 'COMPLETED' || !info.filePath || !info.fileHash) continue;
2011
+ if (info.status !== 'COMPLETED' || !info.filePath) continue;
2012
+ // task-verification-integrity D2: COMPLETED con filePath y sin hash es un fantasma
2013
+ // (complete_task lo aceptaba); antes se salteaba y quedaba invisible al cierre.
2014
+ if (!info.fileHash) {
2015
+ staleFiles.push(`${taskId}:${info.filePath} (missing hash)`);
2016
+ seenFp2.add(info.filePath);
2017
+ continue;
2018
+ }
1959
2019
  const current = fingerprintFor(info.filePath, this.#statePath);
1960
2020
  if (!current) staleFiles.push(`${taskId}:${info.filePath} (missing)`);
1961
2021
  else if (current !== info.fileHash) staleFiles.push(`${taskId}:${info.filePath} (stale fingerprint)`);
@@ -2027,10 +2087,27 @@ class OstackyController {
2027
2087
  };
2028
2088
  }
2029
2089
  if (hasBlocking && force) {
2030
- // 4.3: force requiere confirmación humana en últimas 5 entradas de audit
2031
- const recentAudit = [...(this.#state.audit || []).slice(-5), ...this.#auditBuffer.slice(-5)];
2032
- const hasHuman = recentAudit.some((e) => e.reasoning && /forzar|confirmo|force/i.test(e.reasoning));
2090
+ // task-verification-integrity D4: la evidencia es el TEXTO DEL USUARIO, no el audit.
2091
+ // El audit lo redacta el agente: aceptar /forzar|confirmo|force/ ahí era self-attestation
2092
+ // (cualquier tool call con esa palabra abría el gate). El plugin registra lastUserPrompts
2093
+ // desde ctx.session.hook("prompt"), antes de inyectar hints.
2094
+ // Durden: cada prompt lleva ts y cada rechazo setea lastForceAttemptTs — sólo
2095
+ // cuenta una confirmación POSTERIOR al último intento ("¿qué hace force:true?"
2096
+ // previo al attempt ya no habilita). Strings sin ts = estado legacy: válidos
2097
+ // sólo si nunca hubo intento registrado.
2098
+ const userPrompts = Array.isArray(this.#state.lastUserPrompts) ? this.#state.lastUserPrompts : [];
2099
+ const lastAttempt = Number(this.#state.lastForceAttemptTs) || 0;
2100
+ const isUserPhrase = (p) => {
2101
+ const text = typeof p === 'string' ? p : typeof p?.text === 'string' ? p.text : null;
2102
+ if (!text || !/forzar|confirmo|force/i.test(text)) return false;
2103
+ const ts = typeof p?.ts === 'number' ? p.ts : null;
2104
+ if (ts === null) return lastAttempt === 0;
2105
+ return ts > lastAttempt;
2106
+ };
2107
+ const hasHuman = userPrompts.some(isUserPhrase);
2033
2108
  if (!hasHuman) {
2109
+ this.#state.lastForceAttemptTs = Date.now();
2110
+ await this.#persist();
2034
2111
  const pendingMsg =
2035
2112
  pending.length > 0 ? `Te faltan ${pending.length} task(s): ${pending.join(', ')}.` : '';
2036
2113
  const staleMsg = staleFiles.length > 0 ? `Archivos stale: ${staleFiles.join(', ')}.` : '';
@@ -2044,7 +2121,7 @@ class OstackyController {
2044
2121
  verificationFailed: verificationFailed.length ? verificationFailed : undefined,
2045
2122
  current_state: this.#state.state,
2046
2123
  attempted_transition: 'implementation_complete',
2047
- suggestion: `${detail} Para forzar el cierre, escribí literal "confirmo forzar" en tu mensaje (quedará auditado) y reintentá con {force:true}. Si no querés forzar y es solo stale por refino, llamá refresh_fingerprint({filePath}) y reintentá sin force.`,
2124
+ suggestion: `${detail} Para forzar el cierre, el USUARIO debe escribir literal "confirmo forzar" en un mensaje NUEVO suyo (el plugin lo registra con timestamp; una frase previa a este aviso no cuenta, y el audit que redacta el agente tampoco) y luego reintentá con {force:true}. Si no querés forzar y es solo stale por refino, llamá refresh_fingerprint({filePath}) y reintentá sin force.`,
2048
2125
  };
2049
2126
  }
2050
2127
  const all = [...pending, ...staleFiles, ...verificationFailed].join(',');
@@ -2212,9 +2289,11 @@ class OstackyController {
2212
2289
 
2213
2290
  async verifyIntegrity() {
2214
2291
  this.#load();
2292
+ // D3: un cache vacío no vacía el gate — se deriva de tasks.md.
2293
+ const derivedExpected = this.#deriveExpectedTasks();
2215
2294
  let pending = [];
2216
- if (Array.isArray(this.#state.expectedTasks) && this.#state.expectedTasks.length > 0) {
2217
- pending = this.#state.expectedTasks.filter(
2295
+ if (Array.isArray(derivedExpected) && derivedExpected.length > 0) {
2296
+ pending = derivedExpected.filter(
2218
2297
  (id) => !this.#state.tasks[id] || this.#state.tasks[id].status !== 'COMPLETED'
2219
2298
  );
2220
2299
  } else if (typeof this.#state.expectedTaskCount === 'number') {
@@ -2227,7 +2306,13 @@ class OstackyController {
2227
2306
  const seenFp = new Set();
2228
2307
  try {
2229
2308
  for (const [taskId, info] of Object.entries(this.#state.tasks || {})) {
2230
- if (info.status !== 'COMPLETED' || !info.filePath || !info.fileHash) continue;
2309
+ if (info.status !== 'COMPLETED' || !info.filePath) continue;
2310
+ // task-verification-integrity D2: COMPLETED con filePath y sin hash es un fantasma
2311
+ if (!info.fileHash) {
2312
+ staleFiles.push(`${taskId}:${info.filePath} (missing hash)`);
2313
+ seenFp.add(info.filePath);
2314
+ continue;
2315
+ }
2231
2316
  const current = fingerprintFor(info.filePath, this.#statePath);
2232
2317
  if (!current) staleFiles.push(`${taskId}:${info.filePath} (missing)`);
2233
2318
  else if (current !== info.fileHash) staleFiles.push(`${taskId}:${info.filePath} (stale fingerprint)`);
@@ -2255,7 +2340,14 @@ class OstackyController {
2255
2340
  const parts = rest.split('contiene');
2256
2341
  needle = (parts[1] || parts[0] || '').trim() || rest;
2257
2342
  } else if (needle.startsWith('test:')) {
2258
- // test: asumimos ok si ya está completa (test se corrió en agente)
2343
+ // test-verification-execution: cierre re-verifica evidencia; antes se salteaba.
2344
+ const cmd = needle.slice('test:'.length).trim();
2345
+ const ev = findTestEvidence({
2346
+ runs: this.#state.shellRuns,
2347
+ cmd,
2348
+ lastMutationTs: this.#state.lastMutationTs,
2349
+ });
2350
+ if (!ev.ok) verificationFailed.push(`${tid}: ${verif} — ${ev.reason}`);
2259
2351
  continue;
2260
2352
  }
2261
2353
  if (!needle) continue;
@@ -2790,6 +2882,19 @@ class OstackyController {
2790
2882
  // Generico: test: tasks no requieren archivo nuevo (son verificaciones de comportamiento)
2791
2883
  const verifEarly = this.#state.taskVerifications?.[taskId];
2792
2884
  const isTestTaskEarly = verifEarly && verifEarly.trim().startsWith('test:');
2885
+ // task-verification-integrity D2: un filePath que no existe no puede quedar COMPLETED.
2886
+ // Antes se aceptaba con fileHash:null y verifyIntegrity lo salteaba -> fantasma invisible.
2887
+ // Sin excepción por tipo de task: un path declarado debe existir; una task `test:` no
2888
+ // necesita declarar filePath (y si lo declara, ese archivo ya se creó antes de completarla).
2889
+ if (filePath) {
2890
+ const absPath =
2891
+ filePath.startsWith('/') || /^[A-Za-z]:/.test(filePath)
2892
+ ? resolve(filePath)
2893
+ : resolve(getProjectRoot(this.#statePath), filePath);
2894
+ if (!existsSync(absPath)) {
2895
+ return { error: `filePath does not exist: ${filePath}`, taskId, filePath };
2896
+ }
2897
+ }
2793
2898
  const isNewFile = !!(
2794
2899
  isTestTaskEarly ||
2795
2900
  (filePath &&
@@ -2827,7 +2932,7 @@ class OstackyController {
2827
2932
  `task ${taskId} verification failed: ${verifForComplete}`
2828
2933
  );
2829
2934
  return {
2830
- error: `verification failed for ${taskId}: ${verifForComplete} not found in ${vres.fileHint || filePath || 'unknown file'}`,
2935
+ error: `verification failed for ${taskId}: ${verifForComplete} — ${vres.reason || `not found in ${vres.fileHint || filePath || 'unknown file'}`}`,
2831
2936
  taskId,
2832
2937
  verification: verifForComplete,
2833
2938
  verificationFailed: true,
@@ -2967,14 +3072,27 @@ class OstackyController {
2967
3072
  else fileHint = fileHint || info?.filePath || null;
2968
3073
  if (!needle) needle = rest;
2969
3074
  } else if (needle.startsWith('test:')) {
2970
- // test: no podemos ejecutar desde controller, consideramos ok si hay filePath (ya editado) o fileHash
2971
- needle = needle.slice('test:'.length).trim();
2972
- const hasFile = !!(fileHint || info?.filePath || info?.fileHash);
2973
- const ok = hasFile;
3075
+ // test-verification-execution: evidencia real registrada por el plugin
3076
+ // (exit + timestamp). Nunca "assumed ok"; el comando debe coincidir exacto.
3077
+ const cmd = needle.slice('test:'.length).trim();
3078
+ const ev = findTestEvidence({
3079
+ runs: this.#state.shellRuns,
3080
+ cmd,
3081
+ lastMutationTs: this.#state.lastMutationTs,
3082
+ });
2974
3083
  if (!this.#state.taskVerificationResults) this.#state.taskVerificationResults = {};
2975
- this.#state.taskVerificationResults[taskId] = { ok, verif, ts: Date.now() };
3084
+ this.#state.taskVerificationResults[taskId] = {
3085
+ ok: ev.ok,
3086
+ verif,
3087
+ reason: ev.reason,
3088
+ // Copia plana, NO la referencia de shellRuns: el serializador seguro
3089
+ // marca referencias repetidas como "[Circular]" y corrompería la
3090
+ // evidencia persistida (bug detectado por el test de integración).
3091
+ run: ev.run ? { cmd: ev.run.cmd, exit: ev.run.exit, ts: ev.run.ts } : null,
3092
+ ts: Date.now(),
3093
+ };
2976
3094
  await this.#persist();
2977
- return { ok, taskId, verif, reason: hasFile ? 'test verification assumed ok (has file)' : 'no file' };
3095
+ return { ok: ev.ok, taskId, verif, command: cmd, reason: ev.reason, run: ev.run };
2978
3096
  }
2979
3097
  // Generic: check file content contains needle
2980
3098
  let content = null;
@@ -3214,7 +3332,7 @@ function safeHandler(fn, options = {}) {
3214
3332
 
3215
3333
  const server = new McpServer({
3216
3334
  name: 'ostacky-controller',
3217
- version: '0.10.0',
3335
+ version: '0.10.1',
3218
3336
  });
3219
3337
 
3220
3338
  server.registerTool(
@@ -3816,7 +3934,7 @@ function setupGracefulShutdown(ctrl) {
3816
3934
  }
3817
3935
 
3818
3936
  async function main() {
3819
- log('Starting ostacky-controller MCP v0.10.0...');
3937
+ log('Starting ostacky-controller MCP v0.10.1...');
3820
3938
  log('State path:', { path: statePath });
3821
3939
  // Clean up stale tmp/lock files from previous runs
3822
3940
  cleanupTmpFiles(statePath);