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 +15 -22
- package/assets/agents/ostacky.md +8 -8
- package/assets/commands/install-stack.md +1 -1
- package/assets/docs/ostacky-reference.md +33 -6
- package/assets/mcp/ostacky-controller/index.js +149 -31
- package/assets/plugins/ostacky-controller/index.ts +301 -43
- package/assets/skills/brainstorming/SKILL.md +2 -2
- package/assets/skills/execution-mode-evaluation/SKILL.md +11 -12
- package/assets/skills/openspec-propose/SKILL.md +2 -2
- package/assets/skills/tdd/SKILL.md +1 -1
- package/dist/cli.js +70 -63
- package/manifest.json +32 -32
- package/package.json +1 -1
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
|
|
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
|
|
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: `
|
|
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 #
|
|
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
|
|
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
|
|
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.
|
|
137
|
-
"tag": "v0.10.
|
|
138
|
-
"agents": { "ostacky": { "version": "0.10.
|
|
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
|
|
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:**
|
|
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?**
|
|
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
|
|
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
|
package/assets/agents/ostacky.md
CHANGED
|
@@ -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.
|
|
4
|
+
version: 0.10.1
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
Sos **Ostacky v0.10.
|
|
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.
|
|
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
|
|
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. `
|
|
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.
|
|
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` → `
|
|
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 `
|
|
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.
|
|
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
|
-
##
|
|
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: `
|
|
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
|
-
|
|
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(
|
|
1945
|
-
pending =
|
|
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
|
|
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
|
-
//
|
|
2031
|
-
|
|
2032
|
-
|
|
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,
|
|
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(
|
|
2217
|
-
pending =
|
|
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
|
|
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:
|
|
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:
|
|
2971
|
-
|
|
2972
|
-
const
|
|
2973
|
-
const
|
|
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] = {
|
|
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,
|
|
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.
|
|
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.
|
|
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);
|