ostacky 0.5.7 → 0.5.9

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
@@ -57,7 +57,7 @@ Ostacky integra **5 herramientas** especializadas para que trabajen en conjunto
57
57
 
58
58
  1. **CodeGraph** descubre el alcance del cambio sin escanear el repo entero (consulta `codegraph_context`, `codegraph_impact`).
59
59
  2. **OpenSpec** documenta requisitos, contratos y escenarios de aceptación (proposal → design → spec → tasks).
60
- 3. **Superpowers** ejecuta con TDD, testing automatizado y review (brainstorming → plans → tdd → review).
60
+ 3. **Superpowers** ejecuta con TDD, testing automatizado y review (thinking → plans → tdd → review).
61
61
  4. **Engram** persiste decisiones, bugs y descubrimientos con `mem_save` para que el agente no pierda contexto entre sesiones ni necesite re-ejecutar tool calls.
62
62
  5. **Context7** provee documentación actualizada de librerías y APIs en tiempo real, sin depender de training data.
63
63
 
@@ -197,17 +197,20 @@ Tras instalar, el proyecto queda así:
197
197
  │ ├── install-stack.md
198
198
  │ └── opsx-sync.md
199
199
  ├── skills/
200
- │ ├── brainstorming/
200
+ │ ├── thinking/
201
201
  │ ├── writing-plans/
202
202
  │ ├── tdd/
203
203
  │ ├── review/
204
204
  │ ├── execution-mode-evaluation/
205
205
  │ ├── subagent-driven-development/
206
206
  │ ├── dispatching-parallel-agents/
207
- │ ├── openspec-explore/
208
207
  │ ├── openspec-propose/
209
208
  │ ├── openspec-apply-change/
210
- └── openspec-archive-change/
209
+ ├── openspec-archive-change/
210
+ │ ├── receiving-code-review/
211
+ │ ├── using-git-worktrees/
212
+ │ ├── using-superpowers/
213
+ │ └── writing-skills/
211
214
  └── ostacky-lock.json ← versiones instaladas (agentes, commands y skills)
212
215
  ```
213
216
 
@@ -215,33 +218,33 @@ Tras instalar, el proyecto queda así:
215
218
 
216
219
  ```json
217
220
  {
218
- "version": "0.5.7",
221
+ "version": "0.5.9",
219
222
  "lockedAt": "2025-01-01T00:00:00.000Z",
220
223
  "repo": "JaimeHoracio/Ostacky",
221
- "tag": "v0.5.7",
224
+ "tag": "v0.5.9",
222
225
  "agents": {
223
226
  "ostacky": {
224
- "version": "0.5.7",
227
+ "version": "0.5.9",
225
228
  "installedAt": "2025-01-01T00:00:00.000Z",
226
229
  "sha256": "abc123..."
227
230
  }
228
231
  },
229
232
  "commands": {
230
233
  "install-stack": {
231
- "version": "0.5.7",
234
+ "version": "0.5.9",
232
235
  "installedAt": "2025-01-01T00:00:00.000Z",
233
236
  "sha256": "def456..."
234
237
  },
235
238
  "opsx-sync": {
236
- "version": "0.5.7",
239
+ "version": "0.5.9",
237
240
  "installedAt": "2025-01-01T00:00:00.000Z",
238
241
  "sha256": "ghi789..."
239
242
  }
240
243
  },
241
244
  "skills": {
242
- "brainstorming": { "version": "0.5.7", ... },
243
- "execution-mode-evaluation": { "version": "0.5.7", ... },
244
- "openspec-propose": { "version": "0.5.7", ... }
245
+ "thinking": { "version": "0.5.9", ... },
246
+ "execution-mode-evaluation": { "version": "0.5.9", ... },
247
+ "openspec-propose": { "version": "0.5.9", ... }
245
248
  }
246
249
  }
247
250
  ```
@@ -271,7 +274,7 @@ Es opcional y solo necesario si algo falló durante la instalación o si querés
271
274
  ## Seguridad
272
275
 
273
276
  - `opencode.jsonc` se versiona en el repo para compartir permisos y MCP de forma reproducible.
274
- - Las URLs de descarga usan **tags de GitHub** (ej. `v0.5.7`), nunca `main` — instalaciones reproducibles
277
+ - Las URLs de descarga usan **tags de GitHub** (ej. `v0.5.9`), nunca `main` — instalaciones reproducibles
275
278
  - Cada path de archivo descargado es validado para prevenir **path traversal**
276
279
  - Los archivos incluyen **checksum SHA-256** opcional; si el manifest lo define, el contenido se verifica antes de escribir
277
280
  - El cache local (`~/.opencode/cache/`) también valida integridad al servir archivos cacheados
@@ -7,15 +7,19 @@ Sos **Ostacky**, el orquestador. Tu laburo es **interpretar qué quiere el usuar
7
7
 
8
8
  ## Reglas innegociables
9
9
 
10
- 1. **`validate_edit` antes de `edit`, sin excepciones.** Si llamás `edit` sin `validate_edit` primero, desperdiciás un round-trip completo.
11
- 2. **Una pregunta por turno.** `question` tool es el final de tu mensaje. No generás más texto ni ejecutas tools mientras esperás.
12
- 3. **No edites sin leer fresco.** Jamás uses contenido cacheado de un turno anterior para un `edit` siempre `Read` primero, luego `validate_edit`, luego `edit`.
10
+ 1. **CodeGraph primero, siempre.** Nunca uses `rg`/`grep` en `Bash` para buscar código. Si CodeGraph puede responder, lo usás. `Grep` tool nativo solo para strings literales, nunca `Bash` con `rg`.
11
+ 2. **`validate_edit` antes de `edit`, sin excepciones.** Si llamás `edit` sin `validate_edit` primero, desperdiciás un round-trip completo. El error "No changes to apply: oldString and newString are identical" significa que tus strings son idénticos — posible señal de que leíste contenido cacheado o que el cambio ya fue aplicado. `validate_edit` detecta esto y devuelve `ALREADY_APPLIED` en vez de fallar.
12
+ 3. **Una pregunta por turno.** `question` tool es el final de tu mensaje. No generás más texto ni ejecutas tools mientras esperás.
13
+ 4. **No edites sin leer fresco.** Jamás uses contenido cacheado de un turno anterior para un `edit` — siempre `Read` primero, luego `validate_edit`, luego `edit`.
13
14
 
14
- <HARD-STOP>
15
- DESPUÉS de llamar al `question` tool, TU RESPUESTA TERMINÓ. No hay nada más que agregar. No generes texto explicativo después de la pregunta. No ejecutes tools. No justifiques. No resumas. La pregunta ES el cierre del turno.
15
+ ## Stack
16
16
 
17
- Si sentís la necesidad de agregar algo después de la pregunta, ES UNA SEÑAL DE QUE LA PREGUNTA NO ESTÁ BIEN FORMULADA. Reescribí la pregunta para que sea autónoma.
18
- </HARD-STOP>
17
+ - **Controller** (`.opencode/mcp/ostacky-controller/index.js`): máquina de estados persistida. Valida transiciones, consume decisiones, autoriza side effects, persiste snapshots y tasks. No lo reemplazás con lógica inline.
18
+ - **CodeGraph**: contexto estructural del código. Tu **primera opción** para entender el código.
19
+ - **OpenSpec**: requisitos y contratos para cambios complejos.
20
+ - **Superpowers**: skills de ejecución, TDD, review, delegación.
21
+ - **Engram**: memoria persistente — saves por decisión/discovery, no por edit.
22
+ - **Context7**: documentación de APIs/librerías externas.
19
23
 
20
24
  ## Core Instructions — SINGLE SOURCE OF VERDAD
21
25
 
@@ -58,24 +62,20 @@ Si sentís la necesidad de agregar algo después de la pregunta, ES UNA SEÑAL D
58
62
 
59
63
  **Trigger:** después de cada tarea completada, evaluá: ¿tomé una decisión, fixeé un bug, o aprendí algo no obvio? Si sí → `engram_mem_save`.
60
64
 
61
- ## Preguntas al usuario
65
+ ## Regla de oro
62
66
 
63
- Usá `question` tool (nativo). NUNCA uses `ask_user` MCP.
67
+ **Siempre describí tu interpretación al usuario ANTES de actuar.** Sin validación no ejecutes nada.
64
68
 
65
- **Reglas:**
66
- - Una pregunta por turno
67
- - La pregunta ES el final de tu mensaje (HARD-STOP después)
68
- - No agregues texto después de la pregunta
69
- - No ejecutes tools mientras esperás respuesta
69
+ 1. **Interpretá** — "Entendí que querés [X]. Esto afecta a [archivos/áreas]."
70
+ 2. **Preguntá** — con `question` tool (nativo). Una pregunta por turno. **Esa pregunta es el final de tu mensaje.**
71
+ 3. **Esperá** la respuesta del usuario. No generes más texto ni ejecutes tools mientras esperás.
72
+ 4. **Actuá** según lo que dijo. La respuesta es **vinculante** y se consume una sola vez.
70
73
 
71
- ## Stack
74
+ <HARD-STOP>
75
+ DESPUÉS de llamar al `question` tool, TU RESPUESTA TERMINÓ. No hay nada más que agregar. No generes texto explicativo después de la pregunta. No ejecutes tools. No justifiques. No resumas. La pregunta ES el cierre del turno.
72
76
 
73
- - **Controller** (`.opencode/mcp/ostacky-controller/index.js`): máquina de estados persistida. Valida transiciones, consume decisiones, autoriza side effects, persiste snapshots y tasks.
74
- - **CodeGraph**: contexto estructural del código. Tu **primera opción** para entender el código.
75
- - **OpenSpec**: requisitos y contratos para cambios complejos.
76
- - **Superpowers**: skills de ejecución, TDD, review, delegación.
77
- - **Engram**: memoria persistente — saves por decisión/discovery, no por edit.
78
- - **Context7**: documentación de APIs/librerías externas.
77
+ Si sentís la necesidad de agregar algo después de la pregunta, ES UNA SEÑAL DE QUE LA PREGUNTA NO ESTÁ BIEN FORMULADA. Reescribí la pregunta para que sea autónoma.
78
+ </HARD-STOP>
79
79
 
80
80
  ## Flujo
81
81
 
@@ -90,27 +90,33 @@ Usá `question` tool (nativo). NUNCA uses `ask_user` MCP.
90
90
 
91
91
  ### 1. Discovery
92
92
 
93
- 1. Si existe un change activo, leé `proposal.md`, `design.md`, `tasks.md` solo estos tres.
94
- 2. **Primer tool de código:** CodeGraph sobre el área afectada. Una llamada te da entry points, related symbols y key code snippets.
95
- 3. Si vas a modificar símbolos específicos `codegraph_impact` para ver el blast radius.
96
- 4. Leé con `Read` **solo** archivos que el grafo no cubrió.
97
- 5. Si CodeGraph no da base suficiente reportá blocker.
93
+ 1. `engram_mem_context` recuperá historial reciente. ¿Ya se analizó algo similar?
94
+ 2. Si existe un change activo, leé `proposal.md`, `design.md`, `tasks.md` solo estos tres, no todo el directorio.
95
+ 3. **Primer tool de código: `codegraph_explore`** sobre el área afectada. Una llamada te da entry points, related symbols y key code snippets. No la reemplaces con `Grep` + `Read` + `Glob`.
96
+ 4. Si vas a modificar símbolos específicos → `codegraph_impact` para ver el blast radius.
97
+ 5. Leé con `Read` **solo** archivos que el grafo no cubrió (ej: archivos nuevos no indexados, o secciones específicas que necesitas ver literal).
98
+ 6. Si CodeGraph no da base suficiente → reportá blocker. No caigas a `Grep` como workaround.
98
99
 
99
100
  ### 2. Clasificación por nivel y ruteo
100
101
 
102
+ Después de CodeGraph, clasificá usando **señales de scope, contratos, dependencias, riesgo e impacto**. El conteo de líneas es orientativo, no determinista.
103
+
101
104
  | Señal | Nivel |
102
105
  |---|---|
103
106
  | 1 archivo, sin API pública, sin dependencias nuevas, <15 líneas | **Nivel 0** (trivial) |
104
107
  | 1-2 archivos, sin API pública nueva, sin dependencias nuevas, <30 líneas | **Nivel 0+1** (chico no trivial) |
105
108
  | Modifica API pública, agrega archivos/deps, refactor amplio, >30 líneas, impacto cross-module | **Nivel 1+** (requiere OpenSpec) |
106
109
 
107
- Llamá `record_discovery` con `{ level, routeDecisionId }`. El controller devuelve `routeDecisionId` y `defaultChoice`.
110
+ Llamá `record_discovery` con `{ level, routeDecisionId }`. El controller devuelve `routeDecisionId` y `defaultChoice`:
111
+ - Nivel 0/0+1 → `defaultChoice: "DIRECT"` (Superpowers inline por defecto)
112
+ - Nivel 1+ → `defaultChoice: "SPEC"` (OpenSpec por defecto)
108
113
 
109
114
  **Preguntale al usuario con `question` tool:**
115
+
110
116
  > Nivel 0/0+1: "Esto es Nivel [0/0+1]. Por defecto lo ejecuto directo con Superpowers. ¿O preferís spec?"
111
117
  > Nivel 1+: "Esto es Nivel 1+ porque [razón]. Recomiendo generar spec con OpenSpec. ¿O preferís ejecutar directo?"
112
118
 
113
- La opción por defecto va primera. **La primera respuesta del usuario es vinculante.**
119
+ La opción por defecto va primera. **La primera respuesta del usuario es vinculante.** Si dice spec → `consume_route_decision` con `{ decisionId, choice: "SPEC" }`. Si dice directo → `{ choice: "DIRECT" }`. No reinterpretes, no preguntes de nuevo.
114
120
 
115
121
  **HARD-STOP:** Después de esta pregunta, NO hagas nada más en este turno.
116
122
 
@@ -123,19 +129,25 @@ La opción por defecto va primera. **La primera respuesta del usuario es vincula
123
129
 
124
130
  ### 4. Execution
125
131
 
126
- 1. **Llamá `record_execution_analysis`** con el snapshot de análisis.
127
- 2. **Mostrá el análisis al usuario usando el formato markdown del skill** (shared files, clusters, deps, razón) con `question` tool.
128
- 3. **HARD-STOP:** Después de esta pregunta, NO ejecutes `consume_execution_decision`.
129
- 4. **La confirmación del usuario autoriza la ejecución.** Llamá `consume_execution_decision` con `{ decisionId, mode }`.
130
- 5. **Ejecutá las tasks** — para cada una:
131
- - Leé el archivo fresco con `Read`.
132
- - **Antes de cualquier `edit`**, llamá `validate_edit`.
133
- - `EDITABLE` ejecutá `edit`.
134
- - `ALREADY_APPLIED` skip.
135
- - `CONFLICT` reportá al usuario.
136
- - Después de cada edit exitoso `complete_task`.
137
- 6. **Superpowers**: `tdd`, `review`, skills de ejecución.
138
- 7. **Subagentes** solo para trabajo realmente independiente (sin archivos compartidos).
132
+ 1. **Llamá `record_execution_analysis`** con el snapshot de análisis (archivos por task, shared files, clusters, dependencias, estimación de líneas, recomendación INLINE/SUBAGENT_DRIVEN).
133
+ 2. **Mostrá el análisis al usuario y preguntá** con `question` tool:
134
+ - Mapa de tasks archivos
135
+ - Archivos compartidos
136
+ - Clusters
137
+ - Recomendación y razón
138
+ - "¿Cómo preferís ejecutar?" (inline / subagent-driven)
139
+ 3. **La confirmación del usuario autoriza la ejecución.** Llamá `consume_execution_decision` con `{ decisionId, mode }`.
140
+ **HARD-STOP:** Después de esta pregunta, NO ejecutes `consume_execution_decision`.
141
+ 4. **Ejecutá las tasks** para cada una:
142
+ - Leé el archivo fresco con `Read` (jamás uses un contenido cacheado de un turno anterior) y **guardá el contenido**.
143
+ - **Antes de cualquier `edit`**, llamá `validate_edit` con `{ oldString, newString, content, taskId }`. **`content` es OBLIGATORIO — es el contenido que leíste con Read.** Previene el error "No changes to apply" y conflictos por archivos modificados externamente.
144
+ - `EDITABLE` ejecutá `edit` con los mismos `oldString`/`newString`.
145
+ - ✅ `ALREADY_APPLIED` → **STOP**. No llames `edit`. No preguntes al usuario. Pasá a la próxima task inmediatamente. Este caso ocurre cuando oldString y newString son idénticos (cambio ya aplicado) o cuando newString ya está presente en el contenido.
146
+ - ❌ `CONFLICT` → reportá al usuario el `reason`, no edites. Si el reason dice "found N times", ampliá `oldString` con más contexto y volvé a validar.
147
+ - **HARD-STOP**: Si `oldString === newString`, NO llames `edit`. El cambio ya fue aplicado o no hay nada que hacer. Reportá al usuario si es necesario, pero no ejecutes la tool.
148
+ - Después de cada edit exitoso → `complete_task` con `{ taskId, filePath, fileHash }` (sin Engram por edit).
149
+ 5. **Superpowers**: `tdd`, `review`, skills de ejecución.
150
+ 6. **Subagentes** solo para trabajo realmente independiente (sin archivos compartidos). Son execution-only, no heredan ruteo.
139
151
 
140
152
  ### 5. Sync y cierre
141
153
 
@@ -150,13 +162,21 @@ La opción por defecto va primera. **La primera respuesta del usuario es vincula
150
162
 
151
163
  ## Guardrails
152
164
 
165
+ ### Decisiones y estado
153
166
  - Si una decisión ya está en OpenSpec, CodeGraph, o el controller → no la resolvés de nuevo.
154
167
  - CodeGraph > intuición.
155
168
  - `question` tool para todas las preguntas. Una por turno. HARD-STOP después.
156
169
  - No cadenas de preguntas. Cuando el usuario responde, esa decisión está cerrada.
170
+ - No tool calls en el mismo mensaje que una pregunta.
157
171
  - Fase gate: si estás en Execution o Sync, no volvás a Discovery o Specification automáticamente.
158
172
  - Controller no disponible → reportá confianza reducida, default a inline, no ejecutes subagentes sin autorización.
159
- - **`validate_edit` es obligatorio antes de `edit`.** Un edit fallido desperdicia un round-trip completo.
160
- - **No repitas análisis.** Si ya llamaste CodeGraph para un área, no lo llames de nuevo.
173
+ - Browser/URL: solo si el usuario lo pide explícitamente.
174
+
175
+ ### Eficiencia de tokens
176
+ - **CodeGraph primero, siempre.** Para entender código, buscar símbolos, callers, impact — una llamada a `codegraph_explore` reemplaza docenas de `Read` + `Grep`.
177
+ - **No leas archivos sin justificación.** Solo leé con `Read` lo que CodeGraph o el change activo justifiquen. Si `codegraph_node` con `includeCode: true` te da el cuerpo, no lo re-leas con `Read`.
178
+ - **`validate_edit` es obligatorio antes de `edit`.** Siempre pasá `content` (el resultado de Read). Un edit sin contenido genera un error de validación.
179
+ - **No repitas análisis.** Si ya llamaste `codegraph_explore` para un área en este request, no lo llames de nuevo para la misma área. Si ya tienes un snapshot en el controller, úsalo.
161
180
  - **Una tool por intención.** Si `codegraph_explore` ya te da callers + blast radius, no llames `codegraph_callers` por separado.
181
+ - **Filtra output de comandos con `grep` en `Bash`** solo cuando sea filtrar (ej: `tsc 2>&1 | grep error`). Para buscar en el código, usa CodeGraph o el tool `Grep` nativo, nunca `Bash` con `rg`.
162
182
  - **No expliques lo que vas a hacer antes de hacerlo** si el usuario no lo pidió. Ejecutá y reportá el resultado.
@@ -5,7 +5,7 @@ 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.5.7, `npx ostacky install` ya instala automáticamente el stack completo (CodeGraph, OpenSpec, Engram, Context7, 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.5.9, `npx ostacky install` ya instala automáticamente el stack completo (CodeGraph, OpenSpec, Engram, Context7, 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
 
@@ -50,8 +50,13 @@ class OstackyController {
50
50
 
51
51
  constructor(opts = {}) {
52
52
  this.#statePath = opts.statePath;
53
- this.#state = opts.initialState ? { ...DEFAULT_STATE, ...opts.initialState } : null;
54
- this.#loaded = false;
53
+ if (opts.initialState) {
54
+ this.#state = { ...DEFAULT_STATE, ...opts.initialState };
55
+ this.#loaded = true;
56
+ } else {
57
+ this.#state = null;
58
+ this.#loaded = false;
59
+ }
55
60
  }
56
61
 
57
62
  #load() {
@@ -411,7 +416,7 @@ const controller = new OstackyController({ statePath });
411
416
 
412
417
  const server = new McpServer({
413
418
  name: 'ostacky-controller',
414
- version: '0.5.7',
419
+ version: '0.5.9',
415
420
  });
416
421
 
417
422
  server.registerTool(
@@ -621,16 +626,38 @@ server.registerTool(
621
626
  {
622
627
  description:
623
628
  'Validate an edit against current file content. Returns EDITABLE, ALREADY_APPLIED, or CONFLICT. ' +
624
- 'Call BEFORE executing an edit tool. Only valid in EXECUTING_INLINE or EXECUTING_SUBAGENTS states.',
629
+ 'Call BEFORE executing an edit tool. Only valid in EXECUTING_INLINE or EXECUTING_SUBAGENTS states. ' +
630
+ 'IMPORTANT: content parameter is REQUIRED. Read the file first, then pass the full content.',
625
631
  inputSchema: z.object({
626
632
  oldString: z.string().describe('The exact string to find in content (must be unique).'),
627
633
  newString: z.string().describe('The replacement string.'),
628
- content: z.string().describe('The current file content (read fresh with Read tool).'),
634
+ content: z
635
+ .string()
636
+ .optional()
637
+ .describe('The current file content. REQUIRED — read the file first with Read tool.'),
629
638
  taskId: z.string().optional().describe('Optional task ID for tracking.'),
630
639
  }),
631
640
  },
632
641
  async ({ oldString, newString, content, taskId }) => {
633
- log('tool:validate_edit', { taskId, oldLen: oldString.length, newLen: newString.length });
642
+ log('tool:validate_edit', {
643
+ taskId,
644
+ oldLen: oldString?.length,
645
+ newLen: newString?.length,
646
+ hasContent: !!content,
647
+ });
648
+ if (typeof content !== 'string' || typeof oldString !== 'string' || typeof newString !== 'string') {
649
+ return {
650
+ content: [
651
+ {
652
+ type: 'text',
653
+ text: JSON.stringify({
654
+ outcome: 'CONFLICT',
655
+ reason: 'Missing required fields: content, oldString, and newString are all required. Read the file first, then pass content to validate_edit.',
656
+ }),
657
+ },
658
+ ],
659
+ };
660
+ }
634
661
  const result = await controller.validateEdit({ oldString, newString, content, taskId });
635
662
  return { content: [{ type: 'text', text: JSON.stringify(result) }] };
636
663
  }
@@ -663,7 +690,14 @@ async function main() {
663
690
  log('ostacky-controller connected and ready');
664
691
  }
665
692
 
666
- main().catch((error) => {
667
- console.error('Fatal error:', error);
668
- process.exit(1);
669
- });
693
+ const isDirectRun =
694
+ process.argv[1] && (process.argv[1].endsWith('/index.js') || process.argv[1].endsWith('\\index.js'));
695
+
696
+ if (isDirectRun) {
697
+ main().catch((error) => {
698
+ console.error('Fatal error:', error);
699
+ process.exit(1);
700
+ });
701
+ }
702
+
703
+ export { OstackyController };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ostacky-controller",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -0,0 +1,226 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const ASSETS_DIR = join(__dirname, '..');
10
+ const AGENT_FILE = join(ASSETS_DIR, 'agents', 'ostacky.md');
11
+ const CONTROLLER_FILE = join(ASSETS_DIR, 'mcp', 'ostacky-controller', 'index.js');
12
+
13
+ const agentContent = readFileSync(AGENT_FILE, 'utf8');
14
+ const controllerContent = readFileSync(CONTROLLER_FILE, 'utf8');
15
+
16
+ // Tools registered in the MCP controller
17
+ const MCP_TOOLS = [
18
+ 'start_request', 'request_clarification', 'record_clarification',
19
+ 'record_discovery', 'consume_route_decision', 'spec_complete',
20
+ 'record_execution_analysis', 'consume_execution_decision',
21
+ 'implementation_complete', 'sync_complete', 'block', 'replan',
22
+ 'get_state', 'get_tasks', 'validate_edit', 'complete_task',
23
+ ];
24
+
25
+ // CodeGraph tools referenced in agent instructions
26
+ const CODEGRAPH_TOOLS = [
27
+ 'codegraph_explore', 'codegraph_node', 'codegraph_search',
28
+ 'codegraph_callers', 'codegraph_callees', 'codegraph_impact',
29
+ 'codegraph_files', 'codegraph_status',
30
+ ];
31
+
32
+ // Native OpenCode tools
33
+ const NATIVE_TOOLS = ['question', 'Read', 'Edit', 'Bash', 'Grep', 'Glob'];
34
+
35
+ const ALL_VALID_TOOLS = [...MCP_TOOLS, ...CODEGRAPH_TOOLS, ...NATIVE_TOOLS];
36
+
37
+ describe('Agent contract — ostacky.md', () => {
38
+ describe('Core Instructions section', () => {
39
+ it('has "Core Instructions" heading', () => {
40
+ assert.ok(agentContent.includes('## Core Instructions'),
41
+ 'Missing "## Core Instructions" section');
42
+ });
43
+
44
+ it('has SINGLE SOURCE OF VERDAD marker', () => {
45
+ assert.ok(agentContent.includes('SINGLE SOURCE OF VERDAD'),
46
+ 'Missing "SINGLE SOURCE OF VERDAD" marker');
47
+ });
48
+ });
49
+
50
+ describe('CodeGraph section', () => {
51
+ it('has CodeGraph — búsqueda de código subsection', () => {
52
+ assert.ok(agentContent.includes('### CodeGraph — búsqueda de código'),
53
+ 'Missing "### CodeGraph — búsqueda de código" section');
54
+ });
55
+
56
+ it('has tool table with codegraph_explore', () => {
57
+ assert.ok(agentContent.includes('codegraph_explore'),
58
+ 'Missing codegraph_explore in tool table');
59
+ });
60
+ });
61
+
62
+ describe('Engram section', () => {
63
+ it('has Engram — memoria persistente subsection', () => {
64
+ assert.ok(agentContent.includes('### Engram — memoria persistente'),
65
+ 'Missing "### Engram — memoria persistente" section');
66
+ });
67
+
68
+ it('has engram_mem_context in Discovery flow', () => {
69
+ assert.ok(agentContent.includes('engram_mem_context'),
70
+ 'Missing engram_mem_context — agent should consult memory at start of request');
71
+ });
72
+
73
+ it('has engram_mem_search reference', () => {
74
+ assert.ok(agentContent.includes('engram_mem_search'),
75
+ 'Missing engram_mem_search — agent should search before deciding');
76
+ });
77
+
78
+ it('has engram_mem_save reference', () => {
79
+ assert.ok(agentContent.includes('engram_mem_save'),
80
+ 'Missing engram_mem_save — agent should save after significant work');
81
+ });
82
+ });
83
+
84
+ describe('HARD-STOP rules', () => {
85
+ it('has at least 2 HARD-STOP blocks (HTML tags or inline markers)', () => {
86
+ const htmlBlocks = (agentContent.match(/<HARD-STOP>/g) || []).length;
87
+ const inlineMarkers = (agentContent.match(/\*\*HARD-STOP\*\*/g) || []).length;
88
+ const total = htmlBlocks + inlineMarkers;
89
+ assert.ok(total >= 2,
90
+ `Expected at least 2 HARD-STOP blocks (HTML or inline), found ${total} (${htmlBlocks} HTML, ${inlineMarkers} inline)`);
91
+ });
92
+
93
+ it('has closing HARD-STOP tags matching opening tags', () => {
94
+ const opens = (agentContent.match(/<HARD-STOP>/g) || []).length;
95
+ const closes = (agentContent.match(/<\/HARD-STOP>/g) || []).length;
96
+ assert.equal(opens, closes,
97
+ `Mismatched HARD-STOP tags: ${opens} opens, ${closes} closes`);
98
+ });
99
+
100
+ it('has HARD-STOP in Regla de oro (global question stop)', () => {
101
+ const reglaDeOro = agentContent.substring(
102
+ agentContent.indexOf('## Regla de oro'),
103
+ agentContent.indexOf('## Flujo')
104
+ );
105
+ assert.ok(reglaDeOro.includes('<HARD-STOP>'),
106
+ 'Missing HARD-STOP in Regla de Oro section — agent can continue after questions');
107
+ });
108
+ });
109
+
110
+ describe('Tool name references — no stale names', () => {
111
+ it('does NOT reference "ask_user" (non-existent tool)', () => {
112
+ const refs = agentContent.match(/ask_user/g);
113
+ const count = refs ? refs.length : 0;
114
+ assert.equal(count, 0,
115
+ `Found ${count} reference(s) to "ask_user" — this tool does not exist. Use "question" instead.`);
116
+ });
117
+
118
+ it('does NOT reference "codegraph_context" (stale name)', () => {
119
+ // Allow it in comments or descriptions, but not as an active tool call
120
+ const lines = agentContent.split('\n');
121
+ const offenders = lines.filter(line =>
122
+ line.includes('codegraph_context') &&
123
+ !line.startsWith('|') &&
124
+ !line.includes('NUNCA') &&
125
+ !line.includes('Prohibido')
126
+ );
127
+ assert.equal(offenders.length, 0,
128
+ `Found stale "codegraph_context" references (use codegraph_explore): ${offenders.join('; ')}`);
129
+ });
130
+
131
+ it('references valid MCP tools in flow instructions', () => {
132
+ // Extract tool names used in flow steps (lines with backtick-quoted tool names)
133
+ const toolRefs = agentContent.match(/`([a-z_]+)`/g) || [];
134
+ const uniqueTools = [...new Set(toolRefs.map(t => t.replace(/`/g, '')))];
135
+
136
+ const flowTools = uniqueTools.filter(t =>
137
+ t.includes('request') || t.includes('record') || t.includes('consume') ||
138
+ t.includes('spec_') || t.includes('implementation') || t.includes('sync') ||
139
+ t.includes('validate') || t.includes('complete') || t.includes('block') ||
140
+ t.includes('replan') || t.includes('start_') || t.includes('get_')
141
+ );
142
+
143
+ const invalidTools = flowTools.filter(t => !MCP_TOOLS.includes(t));
144
+ assert.equal(invalidTools.length, 0,
145
+ `References non-existent MCP tools: ${invalidTools.join(', ')}`);
146
+ });
147
+ });
148
+
149
+ describe('validate_edit instructions', () => {
150
+ it('mentions content as required parameter', () => {
151
+ assert.ok(
152
+ agentContent.includes('content') &&
153
+ (agentContent.includes('content es OBLIGATORIO') ||
154
+ agentContent.includes('`content` es OBLIGATORIO')),
155
+ 'Missing explicit instruction that content is required for validate_edit'
156
+ );
157
+ });
158
+
159
+ it('instructs to Read file before validate_edit', () => {
160
+ const executionSection = agentContent.substring(
161
+ agentContent.indexOf('### 4. Execution'),
162
+ agentContent.indexOf('### 5. Sync')
163
+ );
164
+ assert.ok(executionSection.includes('Read'),
165
+ 'Execution section should instruct to Read file before validate_edit');
166
+ });
167
+ });
168
+
169
+ describe('Classification rules', () => {
170
+ it('has Nivel 0, 0+1, and 1+', () => {
171
+ assert.ok(agentContent.includes('Nivel 0'), 'Missing Nivel 0');
172
+ assert.ok(agentContent.includes('Nivel 0+1'), 'Missing Nivel 0+1');
173
+ assert.ok(agentContent.includes('Nivel 1+'), 'Missing Nivel 1+');
174
+ });
175
+
176
+ it('has defaultChoice mapping', () => {
177
+ assert.ok(agentContent.includes('defaultChoice'), 'Missing defaultChoice mapping');
178
+ assert.ok(agentContent.includes('"DIRECT"'), 'Missing DIRECT default');
179
+ assert.ok(agentContent.includes('"SPEC"'), 'Missing SPEC default');
180
+ });
181
+ });
182
+
183
+ describe('Guardrails', () => {
184
+ it('has Eficiencia de tokens section', () => {
185
+ assert.ok(agentContent.includes('### Eficiencia de tokens'),
186
+ 'Missing "### Eficiencia de tokens" guardrail section');
187
+ });
188
+
189
+ it('has Decisiones y estado section', () => {
190
+ assert.ok(agentContent.includes('### Decisiones y estado'),
191
+ 'Missing "### Decisiones y estado" guardrail section');
192
+ });
193
+ });
194
+ });
195
+
196
+ describe('Controller contract — index.js', () => {
197
+ it('exports OstackyController', () => {
198
+ assert.ok(controllerContent.includes('export { OstackyController }'),
199
+ 'Missing OstackyController export');
200
+ });
201
+
202
+ it('has validate_edit tool registered', () => {
203
+ assert.ok(controllerContent.includes("server.registerTool(\n 'validate_edit'") ||
204
+ controllerContent.includes("server.registerTool('validate_edit'"),
205
+ 'validate_edit not registered as MCP tool');
206
+ });
207
+
208
+ it('validate_edit schema has content as optional (not required)', () => {
209
+ // Find the validate_edit schema section
210
+ const validateSection = controllerContent.substring(
211
+ controllerContent.indexOf("'validate_edit'"),
212
+ controllerContent.indexOf("'complete_task'")
213
+ );
214
+ assert.ok(validateSection.includes('content: z.string().optional()'),
215
+ 'validate_edit content param should be z.string().optional() — agent may omit it');
216
+ });
217
+
218
+ it('validate_edit handler validates content type', () => {
219
+ const validateSection = controllerContent.substring(
220
+ controllerContent.indexOf("'validate_edit'"),
221
+ controllerContent.indexOf("'complete_task'")
222
+ );
223
+ assert.ok(validateSection.includes("typeof content !== 'string'"),
224
+ 'validate_edit handler should check typeof content');
225
+ });
226
+ });
@@ -0,0 +1,187 @@
1
+ import { describe, it, beforeEach } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { OstackyController } from '../mcp/ostacky-controller/index.js';
4
+
5
+ function createController(initialState = {}) {
6
+ return new OstackyController({
7
+ statePath: null,
8
+ initialState: {
9
+ state: 'INTERPRETATION_PENDING',
10
+ revision: 0,
11
+ requestId: 'test',
12
+ changeId: null,
13
+ routeDecisionId: null,
14
+ routeChoice: null,
15
+ executionDecisionId: null,
16
+ executionMode: null,
17
+ snapshots: { codegraph: null, execution: null },
18
+ tasks: {},
19
+ fileFingerprints: {},
20
+ error: null,
21
+ ...initialState,
22
+ },
23
+ });
24
+ }
25
+
26
+ describe('OstackyController', () => {
27
+ describe('startRequest', () => {
28
+ it('transitions to INTERPRETATION_PENDING', async () => {
29
+ const ctrl = createController({ state: 'DONE' });
30
+ const result = await ctrl.startRequest({ requestId: 'req-1' });
31
+ assert.equal(result.state, 'INTERPRETATION_PENDING');
32
+ assert.equal(result.requestId, 'req-1');
33
+ });
34
+
35
+ it('generates requestId when not provided', async () => {
36
+ const ctrl = createController({ state: 'DONE' });
37
+ const result = await ctrl.startRequest();
38
+ assert.ok(result.requestId.startsWith('req-'));
39
+ });
40
+ });
41
+
42
+ describe('validateEdit', () => {
43
+ it('returns CONFLICT when content is undefined (the bug we fixed)', async () => {
44
+ const ctrl = createController({ state: 'EXECUTING_INLINE' });
45
+ const result = await ctrl.validateEdit({
46
+ oldString: 'foo',
47
+ newString: 'bar',
48
+ content: undefined,
49
+ });
50
+ assert.equal(result.outcome, 'CONFLICT');
51
+ assert.ok(result.reason.includes('Missing required fields'));
52
+ });
53
+
54
+ it('returns CONFLICT when content is not a string', async () => {
55
+ const ctrl = createController({ state: 'EXECUTING_INLINE' });
56
+ const result = await ctrl.validateEdit({
57
+ oldString: 'foo',
58
+ newString: 'bar',
59
+ content: 123,
60
+ });
61
+ assert.equal(result.outcome, 'CONFLICT');
62
+ assert.ok(result.reason.includes('Missing required fields'));
63
+ });
64
+
65
+ it('returns EDITABLE when oldString found exactly once', async () => {
66
+ const ctrl = createController({ state: 'EXECUTING_INLINE' });
67
+ const result = await ctrl.validateEdit({
68
+ oldString: 'hello world',
69
+ newString: 'hello universe',
70
+ content: 'say hello world to everyone',
71
+ });
72
+ assert.equal(result.outcome, 'EDITABLE');
73
+ });
74
+
75
+ it('returns ALREADY_APPLIED when oldString equals newString', async () => {
76
+ const ctrl = createController({ state: 'EXECUTING_INLINE' });
77
+ const result = await ctrl.validateEdit({
78
+ oldString: 'hello',
79
+ newString: 'hello',
80
+ content: 'hello world',
81
+ });
82
+ assert.equal(result.outcome, 'ALREADY_APPLIED');
83
+ });
84
+
85
+ it('returns ALREADY_APPLIED when newString already present and oldString not found', async () => {
86
+ const ctrl = createController({ state: 'EXECUTING_INLINE' });
87
+ const result = await ctrl.validateEdit({
88
+ oldString: 'old text',
89
+ newString: 'new text',
90
+ content: 'already has new text here',
91
+ });
92
+ assert.equal(result.outcome, 'ALREADY_APPLIED');
93
+ });
94
+
95
+ it('returns CONFLICT when oldString not found and newString not present', async () => {
96
+ const ctrl = createController({ state: 'EXECUTING_INLINE' });
97
+ const result = await ctrl.validateEdit({
98
+ oldString: 'nonexistent',
99
+ newString: 'replacement',
100
+ content: 'some content without the old string',
101
+ });
102
+ assert.equal(result.outcome, 'CONFLICT');
103
+ assert.ok(result.reason.includes('not found'));
104
+ });
105
+
106
+ it('returns CONFLICT when oldString found multiple times', async () => {
107
+ const ctrl = createController({ state: 'EXECUTING_INLINE' });
108
+ const result = await ctrl.validateEdit({
109
+ oldString: 'dup',
110
+ newString: 'replaced',
111
+ content: 'dup and dup again',
112
+ });
113
+ assert.equal(result.outcome, 'CONFLICT');
114
+ assert.ok(result.reason.includes('2 times'));
115
+ });
116
+
117
+ it('returns CONFLICT when not in executing state', async () => {
118
+ const ctrl = createController();
119
+ const result = await ctrl.validateEdit({
120
+ oldString: 'foo',
121
+ newString: 'bar',
122
+ content: 'foo bar baz',
123
+ });
124
+ assert.equal(result.outcome, 'CONFLICT');
125
+ assert.ok(result.reason.includes('Cannot validate edit from state'));
126
+ });
127
+ });
128
+
129
+ describe('state transitions', () => {
130
+ it('full flow: start → discovery → route → execution → sync → done', async () => {
131
+ const ctrl = createController({ state: 'DONE' });
132
+ let r = await ctrl.startRequest({ requestId: 'test' });
133
+ assert.equal(r.state, 'INTERPRETATION_PENDING');
134
+
135
+ r = await ctrl.recordDiscovery({ level: '0', routeDecisionId: 'rd-1' });
136
+ assert.equal(r.state, 'ROUTE_DECISION_PENDING');
137
+ assert.equal(r.defaultChoice, 'DIRECT');
138
+
139
+ r = await ctrl.consumeRouteDecision({ decisionId: 'rd-1', choice: 'DIRECT' });
140
+ assert.equal(r.state, 'EXECUTION_ANALYSIS');
141
+
142
+ r = await ctrl.recordExecutionAnalysis({ executionDecisionId: 'ed-1' });
143
+ assert.equal(r.state, 'EXECUTION_DECISION_PENDING');
144
+
145
+ r = await ctrl.consumeExecutionDecision({ decisionId: 'ed-1', mode: 'INLINE' });
146
+ assert.equal(r.state, 'EXECUTING_INLINE');
147
+
148
+ r = await ctrl.implementationComplete();
149
+ assert.equal(r.state, 'SYNC');
150
+
151
+ r = await ctrl.syncComplete();
152
+ assert.equal(r.state, 'DONE');
153
+ });
154
+
155
+ it('rejects invalid transition: start from EXECUTING_INLINE', async () => {
156
+ const ctrl = createController({ state: 'EXECUTING_INLINE', requestId: 'original' });
157
+ const r = await ctrl.startRequest({ requestId: 'nope' });
158
+ assert.equal(r.state, 'EXECUTING_INLINE');
159
+ assert.equal(r.requestId, 'original');
160
+ });
161
+
162
+ it('level 1+ defaults to SPEC', async () => {
163
+ const ctrl = createController();
164
+ const r = await ctrl.recordDiscovery({ level: '1+', routeDecisionId: 'rd-2' });
165
+ assert.equal(r.defaultChoice, 'SPEC');
166
+ });
167
+ });
168
+
169
+ describe('completeTask', () => {
170
+ it('marks task as completed', async () => {
171
+ const ctrl = createController({ state: 'EXECUTING_INLINE' });
172
+ const r = await ctrl.completeTask({
173
+ taskId: 'task-1',
174
+ filePath: 'src/foo.ts',
175
+ fileHash: 'abc123',
176
+ });
177
+ assert.equal(r.status, 'COMPLETED');
178
+ assert.equal(r.totalCompleted, 1);
179
+ });
180
+
181
+ it('rejects completeTask from wrong state', async () => {
182
+ const ctrl = createController();
183
+ const r = await ctrl.completeTask({ taskId: 'task-1' });
184
+ assert.ok(r.error.includes('Cannot complete task from state'));
185
+ });
186
+ });
187
+ });
@@ -236,6 +236,24 @@ else
236
236
  fi
237
237
  echo ""
238
238
 
239
+ # ─── 14. Runtime tests (node:test) ──────────────────────────────────────────
240
+ echo "14. Runtime tests"
241
+ if command -v node &>/dev/null; then
242
+ TEST_OUTPUT=$(cd "$ASSETS_DIR" && node --test tests/controller.test.js tests/agent-contract.test.js 2>&1)
243
+ TEST_EXIT=$?
244
+ TEST_PASS=$(echo "$TEST_OUTPUT" | grep "^# pass" | awk '{print $3}')
245
+ TEST_FAIL=$(echo "$TEST_OUTPUT" | grep "^# fail" | awk '{print $3}')
246
+ if [ "$TEST_EXIT" -eq 0 ]; then
247
+ pass "runtime tests: ${TEST_PASS}/${TEST_PASS} passed"
248
+ else
249
+ fail "runtime tests: ${TEST_FAIL} failed out of $((TEST_PASS + TEST_FAIL))"
250
+ echo "$TEST_OUTPUT" | grep "not ok" | head -5
251
+ fi
252
+ else
253
+ warn "node not found — skipping runtime tests"
254
+ fi
255
+ echo ""
256
+
239
257
  # ─── Summary ──────────────────────────────────────────────────────────────────
240
258
  echo "═══════════════════════════════════════════════════════════"
241
259
  echo " Summary"
package/dist/cli.js CHANGED
@@ -161,7 +161,7 @@ var require_picocolors = __commonJS((exports, module) => {
161
161
  // package.json
162
162
  var package_default = {
163
163
  name: "ostacky",
164
- version: "0.5.7",
164
+ version: "0.5.9",
165
165
  description: "Instalador interactivo de agentes y comandos para OpenCode",
166
166
  type: "module",
167
167
  bin: {
@@ -879,16 +879,16 @@ import { join as join5 } from "path";
879
879
  import { existsSync as existsSync4 } from "fs";
880
880
  // manifest.json
881
881
  var manifest_default = {
882
- version: "0.5.7",
882
+ version: "0.5.9",
883
883
  repo: "JaimeHoracio/Ostacky",
884
- tag: "v0.5.7",
884
+ tag: "v0.5.9",
885
885
  agents: [
886
886
  {
887
887
  name: "ostacky",
888
888
  file: "assets/agents/ostacky.md",
889
889
  description: "Orquestador principal con ruteo por nivel de impacto, máquina de estados persistida (controller MCP), edición segura con 3 outcomes, y delegación en OpenSpec + Superpowers",
890
- version: "0.5.7",
891
- sha256: "eb3ab4cfcdedee63dd4aae05d748d34d9cfc3323bdc5f53f06848cfe3efd439f"
890
+ version: "0.5.9",
891
+ sha256: "ede9800f644d890fa81c4827592ca62ce00756a25fbdb594cb8a7b40cd7ca2fc"
892
892
  }
893
893
  ],
894
894
  commands: [
@@ -896,14 +896,14 @@ var manifest_default = {
896
896
  name: "install-stack",
897
897
  file: "assets/commands/install-stack.md",
898
898
  description: "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, Context7, controller MCP)",
899
- version: "0.5.7",
900
- sha256: "f8357678ae180fc9fda03be2e4f44a18ee4d7eedf138a6f9dbe22cc63bd2bc41"
899
+ version: "0.5.9",
900
+ sha256: "598b54bed669586f832193226ebeddbda325325f8f237ba5104ab3ee58f2ac29"
901
901
  },
902
902
  {
903
903
  name: "opsx-sync",
904
904
  file: "assets/commands/opsx-sync.md",
905
905
  description: "Sincroniza delta specs del change activo sin inicializar CodeGraph si ya existe índice",
906
- version: "0.5.7",
906
+ version: "0.5.9",
907
907
  sha256: "ed9948f1910743b672e1dfad496bc96972a224a98b063232be39290d43068c50"
908
908
  }
909
909
  ],
@@ -912,8 +912,8 @@ var manifest_default = {
912
912
  name: "ostacky-controller",
913
913
  file: "assets/mcp/ostacky-controller/",
914
914
  description: "Máquina de estados persistida para Ostacky: 13 tools MCP para ciclo completo de request (start_request → discovery → route → execution → sync → done), edición segura con validación de transiciones y persistencia atómica",
915
- version: "0.5.7",
916
- sha256: "f457acabe9187fea9ea4a132d4a54e26021d1dc8a87469ba5815d8d3052336af"
915
+ version: "0.5.9",
916
+ sha256: "68b42dfb65938e423a7c81d9e9b179c0d82533047d5c0205cf459f5aa2fc6fa2"
917
917
  }
918
918
  ],
919
919
  skills: [
@@ -921,99 +921,99 @@ var manifest_default = {
921
921
  name: "thinking",
922
922
  file: "assets/skills/thinking/SKILL.md",
923
923
  description: "Skill unificado de pensamiento con dos modos: creative-design (producción de diseño → transición a writing-plans o openspec-propose) y open-exploration (exploración libre)",
924
- version: "0.5.7",
925
- sha256: "3801f3c964e3c4a64f54747d7a8b2e5c2c956c907ebc938b45d05c3b5ee6dfa4"
924
+ version: "0.5.9",
925
+ sha256: "6a1fdac86357f89e132013e56cb84c1920880c1c035a8dcf53f69e01824724bb"
926
926
  },
927
927
  {
928
928
  name: "execution-mode-evaluation",
929
929
  file: "assets/skills/execution-mode-evaluation/SKILL.md",
930
930
  description: "Skill de análisis de modo de ejecución — output reconciliado con controller snapshot contract (recommendation field)",
931
- version: "0.5.7",
932
- sha256: "0d791c514c1bde60ac3a2b02e75fdebf36230e46c30d76d5f1c32aefad82463f"
931
+ version: "0.5.9",
932
+ sha256: "048925aec576dff014eed0b44e2a553c635ae405f5a245773f480be9b84d18a7"
933
933
  },
934
934
  {
935
935
  name: "writing-plans",
936
936
  file: "assets/skills/writing-plans/SKILL.md",
937
937
  description: "Skill de planificación de implementación (Superpowers) — execution handoff removido, Ostacky decide modo de ejecución",
938
- version: "0.5.7",
939
- sha256: "d8dde75ee7aac1195060444e93484319d5b1ddc7125576715978b5352354c9a8"
938
+ version: "0.5.9",
939
+ sha256: "13f6e43522567505da90793934d414d290d56f4ea587ac696f66416af9e7633f"
940
940
  },
941
941
  {
942
942
  name: "tdd",
943
943
  file: "assets/skills/tdd/SKILL.md",
944
944
  description: "Skill de test-driven development (Superpowers)",
945
- version: "0.5.7",
946
- sha256: "7dee67b4af6bdccc7a914ca34533184d64592d0f5b23aeae631538168db14994"
945
+ version: "0.5.9",
946
+ sha256: "7eacd8ee81dc5c0b85065c0392e37ee10314661f7edb59dd8e70a9e0dae8f371"
947
947
  },
948
948
  {
949
949
  name: "subagent-driven-development",
950
950
  file: "assets/skills/subagent-driven-development/SKILL.md",
951
951
  description: "Skill de ejecución con subagentes (Superpowers) — ejecuta solo después de confirmación del coordinador Ostacky",
952
- version: "0.5.7",
953
- sha256: "6b5344c5749dd9b61bed0fded1516c212cdfeec59779f9c32a5a84fa94315431"
952
+ version: "0.5.9",
953
+ sha256: "cdacae2e5b86f6a642ad0316172cee7dc8d19e909ef4f9ab7fcf7e84a248dda3"
954
954
  },
955
955
  {
956
956
  name: "dispatching-parallel-agents",
957
957
  file: "assets/skills/dispatching-parallel-agents/SKILL.md",
958
958
  description: "Skill de dispatch paralelo de agentes (Superpowers)",
959
- version: "0.5.7",
960
- sha256: "76806091c7f923ba2596546b19cccd98a08e57a68745df77c3a7b998fe838e2b"
959
+ version: "0.5.9",
960
+ sha256: "281edf0c38f358497c7e2066fa8217a2ba3e2a39b4205c4af0c41d328fc035a1"
961
961
  },
962
962
  {
963
963
  name: "review",
964
964
  file: "assets/skills/review/SKILL.md",
965
965
  description: "Skill de revisión de código (Superpowers)",
966
- version: "0.5.7",
967
- sha256: "fc68671ac4592ceeae6bf0d3ac33abaf78cd34e94e67c07ad0329843d6003468"
966
+ version: "0.5.9",
967
+ sha256: "14831d9ef3746ef6e2e6047c868fe7aa296d01f0644227ce776fe99436357b1a"
968
968
  },
969
969
  {
970
970
  name: "receiving-code-review",
971
971
  file: "assets/skills/receiving-code-review/SKILL.md",
972
972
  description: "Skill de recibir y procesar feedback de code review",
973
- version: "0.5.7",
974
- sha256: "b6ace0ce93c5eade7b3f17d6744aa2fca6f492632b80e5dd754f93ba2c331550"
973
+ version: "0.5.9",
974
+ sha256: "0a5780e8a41539d15428114b7c46f36670acc16aba0db3348d36be1175433872"
975
975
  },
976
976
  {
977
977
  name: "openspec-propose",
978
978
  file: "assets/skills/openspec-propose/SKILL.md",
979
979
  description: "Skill de generación de proposal (OpenSpec)",
980
- version: "0.5.7",
981
- sha256: "674b84346d954f61fbbe53d1efb7e6cd9a761b49ea6a9950aa9dfbfe6366a165"
980
+ version: "0.5.9",
981
+ sha256: "3306b21ba9cb8c1611e8ba335126982f3767f7d317b5ed506c57a14c5f88b263"
982
982
  },
983
983
  {
984
984
  name: "openspec-apply-change",
985
985
  file: "assets/skills/openspec-apply-change/SKILL.md",
986
986
  description: "Skill de aplicación de change (OpenSpec)",
987
- version: "0.5.7",
988
- sha256: "9e4fe9fe780b580bfba9f842c3c3603e77150355f8bf2b68a85e8ce41e2c941a"
987
+ version: "0.5.9",
988
+ sha256: "31c1313c3de4616a07efd89306c4c058175d018f42d4381cc7da7030fd03ba30"
989
989
  },
990
990
  {
991
991
  name: "openspec-archive-change",
992
992
  file: "assets/skills/openspec-archive-change/SKILL.md",
993
993
  description: "Skill de archivo de change (OpenSpec)",
994
- version: "0.5.7",
995
- sha256: "4c611a20654841c585fe1c6f573e6e69c8b95c4eecdfe5552923be44dd3da0c4"
994
+ version: "0.5.9",
995
+ sha256: "5bf65d1848457bd57be1d21707e55fe6746cdee33bce2f32f12d87661b6aa3a4"
996
996
  },
997
997
  {
998
998
  name: "using-git-worktrees",
999
999
  file: "assets/skills/using-git-worktrees/SKILL.md",
1000
1000
  description: "Skill de uso de git worktrees para aislamiento de trabajo",
1001
- version: "0.5.7",
1002
- sha256: "ff16257abe3836ca0f1003f130236a79acf4c8574a2e9fed092a5101d52ae860"
1001
+ version: "0.5.9",
1002
+ sha256: "33433c24f753d566cde16c0931f70d746b6b0cb4d1f3daaa619e928b009e4fc9"
1003
1003
  },
1004
1004
  {
1005
1005
  name: "using-superpowers",
1006
1006
  file: "assets/skills/using-superpowers/SKILL.md",
1007
1007
  description: "Skill de orquestación de Superpowers skills",
1008
- version: "0.5.7",
1009
- sha256: "bc8bd5445b623c1158f83c518155518b6556c58dd194f73f3bab444a983410ab"
1008
+ version: "0.5.9",
1009
+ sha256: "fba542003aa788b5edb4ae2d1135773a1dde8111d1dd60378e2f61b6695d46fc"
1010
1010
  },
1011
1011
  {
1012
1012
  name: "writing-skills",
1013
1013
  file: "assets/skills/writing-skills/SKILL.md",
1014
1014
  description: "Skill de creación y edición de skills",
1015
- version: "0.5.7",
1016
- sha256: "915369d6a74570c1605ad49d6e6551a946ca232b12400fbb45797149f16a7f2c"
1015
+ version: "0.5.9",
1016
+ sha256: "7f74ffe049283803640d78f8c9ac1ea5f5e626dedb885ecff29a87c5b5d41598"
1017
1017
  }
1018
1018
  ]
1019
1019
  };
@@ -1204,7 +1204,7 @@ function clearLockfile(opencodeRoot) {
1204
1204
  const lockfile = readLockfile(opencodeRoot);
1205
1205
  if (!lockfile) {
1206
1206
  writeLockfile(opencodeRoot, {
1207
- version: "0.5.7",
1207
+ version: "0.5.9",
1208
1208
  lockedAt: new Date().toISOString(),
1209
1209
  repo: "",
1210
1210
  tag: "",
@@ -19539,8 +19539,13 @@ class OstackyController {
19539
19539
  #loaded;
19540
19540
  constructor(opts = {}) {
19541
19541
  this.#statePath = opts.statePath;
19542
- this.#state = opts.initialState ? { ...DEFAULT_STATE, ...opts.initialState } : null;
19543
- this.#loaded = false;
19542
+ if (opts.initialState) {
19543
+ this.#state = { ...DEFAULT_STATE, ...opts.initialState };
19544
+ this.#loaded = true;
19545
+ } else {
19546
+ this.#state = null;
19547
+ this.#loaded = false;
19548
+ }
19544
19549
  }
19545
19550
  #load() {
19546
19551
  if (this.#loaded)
@@ -19884,7 +19889,7 @@ var statePath = process.env.OSTACKY_STATE_PATH || ".opencode/ostacky-state.json"
19884
19889
  var controller = new OstackyController({ statePath });
19885
19890
  var server = new McpServer({
19886
19891
  name: "ostacky-controller",
19887
- version: "0.5.7"
19892
+ version: "0.5.9"
19888
19893
  });
19889
19894
  server.registerTool("start_request", {
19890
19895
  description: "Start or resume a new request in the state machine. Call this first.",
@@ -20019,15 +20024,33 @@ server.registerTool("get_tasks", {
20019
20024
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
20020
20025
  });
20021
20026
  server.registerTool("validate_edit", {
20022
- description: "Validate an edit against current file content. Returns EDITABLE, ALREADY_APPLIED, or CONFLICT. " + "Call BEFORE executing an edit tool. Only valid in EXECUTING_INLINE or EXECUTING_SUBAGENTS states.",
20027
+ description: "Validate an edit against current file content. Returns EDITABLE, ALREADY_APPLIED, or CONFLICT. " + "Call BEFORE executing an edit tool. Only valid in EXECUTING_INLINE or EXECUTING_SUBAGENTS states. " + "IMPORTANT: content parameter is REQUIRED. Read the file first, then pass the full content.",
20023
20028
  inputSchema: object({
20024
20029
  oldString: string2().describe("The exact string to find in content (must be unique)."),
20025
20030
  newString: string2().describe("The replacement string."),
20026
- content: string2().describe("The current file content (read fresh with Read tool)."),
20031
+ content: string2().optional().describe("The current file content. REQUIRED — read the file first with Read tool."),
20027
20032
  taskId: string2().optional().describe("Optional task ID for tracking.")
20028
20033
  })
20029
20034
  }, async ({ oldString, newString, content, taskId }) => {
20030
- log("tool:validate_edit", { taskId, oldLen: oldString.length, newLen: newString.length });
20035
+ log("tool:validate_edit", {
20036
+ taskId,
20037
+ oldLen: oldString?.length,
20038
+ newLen: newString?.length,
20039
+ hasContent: !!content
20040
+ });
20041
+ if (typeof content !== "string" || typeof oldString !== "string" || typeof newString !== "string") {
20042
+ return {
20043
+ content: [
20044
+ {
20045
+ type: "text",
20046
+ text: JSON.stringify({
20047
+ outcome: "CONFLICT",
20048
+ reason: "Missing required fields: content, oldString, and newString are all required. Read the file first, then pass content to validate_edit."
20049
+ })
20050
+ }
20051
+ ]
20052
+ };
20053
+ }
20031
20054
  const result = await controller.validateEdit({ oldString, newString, content, taskId });
20032
20055
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
20033
20056
  });
@@ -20050,7 +20073,13 @@ async function main() {
20050
20073
  await server.connect(transport);
20051
20074
  log("ostacky-controller connected and ready");
20052
20075
  }
20053
- main().catch((error2) => {
20054
- console.error("Fatal error:", error2);
20055
- process.exit(1);
20056
- });
20076
+ var isDirectRun = process.argv[1] && (process.argv[1].endsWith("/index.js") || process.argv[1].endsWith("\\index.js"));
20077
+ if (isDirectRun) {
20078
+ main().catch((error2) => {
20079
+ console.error("Fatal error:", error2);
20080
+ process.exit(1);
20081
+ });
20082
+ }
20083
+ export {
20084
+ OstackyController
20085
+ };
package/manifest.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
- "version": "0.5.7",
2
+ "version": "0.5.9",
3
3
  "repo": "JaimeHoracio/Ostacky",
4
- "tag": "v0.5.7",
4
+ "tag": "v0.5.9",
5
5
  "agents": [
6
6
  {
7
7
  "name": "ostacky",
8
8
  "file": "assets/agents/ostacky.md",
9
9
  "description": "Orquestador principal con ruteo por nivel de impacto, máquina de estados persistida (controller MCP), edición segura con 3 outcomes, y delegación en OpenSpec + Superpowers",
10
- "version": "0.5.7",
11
- "sha256": "eb3ab4cfcdedee63dd4aae05d748d34d9cfc3323bdc5f53f06848cfe3efd439f"
10
+ "version": "0.5.9",
11
+ "sha256": "ede9800f644d890fa81c4827592ca62ce00756a25fbdb594cb8a7b40cd7ca2fc"
12
12
  }
13
13
  ],
14
14
  "commands": [
@@ -16,14 +16,14 @@
16
16
  "name": "install-stack",
17
17
  "file": "assets/commands/install-stack.md",
18
18
  "description": "Instala el stack tecnológico del proyecto (CodeGraph, skills, OpenSpec, Engram, Context7, controller MCP)",
19
- "version": "0.5.7",
20
- "sha256": "f8357678ae180fc9fda03be2e4f44a18ee4d7eedf138a6f9dbe22cc63bd2bc41"
19
+ "version": "0.5.9",
20
+ "sha256": "598b54bed669586f832193226ebeddbda325325f8f237ba5104ab3ee58f2ac29"
21
21
  },
22
22
  {
23
23
  "name": "opsx-sync",
24
24
  "file": "assets/commands/opsx-sync.md",
25
25
  "description": "Sincroniza delta specs del change activo sin inicializar CodeGraph si ya existe índice",
26
- "version": "0.5.7",
26
+ "version": "0.5.9",
27
27
  "sha256": "ed9948f1910743b672e1dfad496bc96972a224a98b063232be39290d43068c50"
28
28
  }
29
29
  ],
@@ -32,8 +32,8 @@
32
32
  "name": "ostacky-controller",
33
33
  "file": "assets/mcp/ostacky-controller/",
34
34
  "description": "Máquina de estados persistida para Ostacky: 13 tools MCP para ciclo completo de request (start_request → discovery → route → execution → sync → done), edición segura con validación de transiciones y persistencia atómica",
35
- "version": "0.5.7",
36
- "sha256": "f457acabe9187fea9ea4a132d4a54e26021d1dc8a87469ba5815d8d3052336af"
35
+ "version": "0.5.9",
36
+ "sha256": "68b42dfb65938e423a7c81d9e9b179c0d82533047d5c0205cf459f5aa2fc6fa2"
37
37
  }
38
38
  ],
39
39
  "skills": [
@@ -41,99 +41,99 @@
41
41
  "name": "thinking",
42
42
  "file": "assets/skills/thinking/SKILL.md",
43
43
  "description": "Skill unificado de pensamiento con dos modos: creative-design (producción de diseño → transición a writing-plans o openspec-propose) y open-exploration (exploración libre)",
44
- "version": "0.5.7",
45
- "sha256": "3801f3c964e3c4a64f54747d7a8b2e5c2c956c907ebc938b45d05c3b5ee6dfa4"
44
+ "version": "0.5.9",
45
+ "sha256": "6a1fdac86357f89e132013e56cb84c1920880c1c035a8dcf53f69e01824724bb"
46
46
  },
47
47
  {
48
48
  "name": "execution-mode-evaluation",
49
49
  "file": "assets/skills/execution-mode-evaluation/SKILL.md",
50
50
  "description": "Skill de análisis de modo de ejecución — output reconciliado con controller snapshot contract (recommendation field)",
51
- "version": "0.5.7",
52
- "sha256": "0d791c514c1bde60ac3a2b02e75fdebf36230e46c30d76d5f1c32aefad82463f"
51
+ "version": "0.5.9",
52
+ "sha256": "048925aec576dff014eed0b44e2a553c635ae405f5a245773f480be9b84d18a7"
53
53
  },
54
54
  {
55
55
  "name": "writing-plans",
56
56
  "file": "assets/skills/writing-plans/SKILL.md",
57
57
  "description": "Skill de planificación de implementación (Superpowers) — execution handoff removido, Ostacky decide modo de ejecución",
58
- "version": "0.5.7",
59
- "sha256": "d8dde75ee7aac1195060444e93484319d5b1ddc7125576715978b5352354c9a8"
58
+ "version": "0.5.9",
59
+ "sha256": "13f6e43522567505da90793934d414d290d56f4ea587ac696f66416af9e7633f"
60
60
  },
61
61
  {
62
62
  "name": "tdd",
63
63
  "file": "assets/skills/tdd/SKILL.md",
64
64
  "description": "Skill de test-driven development (Superpowers)",
65
- "version": "0.5.7",
66
- "sha256": "7dee67b4af6bdccc7a914ca34533184d64592d0f5b23aeae631538168db14994"
65
+ "version": "0.5.9",
66
+ "sha256": "7eacd8ee81dc5c0b85065c0392e37ee10314661f7edb59dd8e70a9e0dae8f371"
67
67
  },
68
68
  {
69
69
  "name": "subagent-driven-development",
70
70
  "file": "assets/skills/subagent-driven-development/SKILL.md",
71
71
  "description": "Skill de ejecución con subagentes (Superpowers) — ejecuta solo después de confirmación del coordinador Ostacky",
72
- "version": "0.5.7",
73
- "sha256": "6b5344c5749dd9b61bed0fded1516c212cdfeec59779f9c32a5a84fa94315431"
72
+ "version": "0.5.9",
73
+ "sha256": "cdacae2e5b86f6a642ad0316172cee7dc8d19e909ef4f9ab7fcf7e84a248dda3"
74
74
  },
75
75
  {
76
76
  "name": "dispatching-parallel-agents",
77
77
  "file": "assets/skills/dispatching-parallel-agents/SKILL.md",
78
78
  "description": "Skill de dispatch paralelo de agentes (Superpowers)",
79
- "version": "0.5.7",
80
- "sha256": "76806091c7f923ba2596546b19cccd98a08e57a68745df77c3a7b998fe838e2b"
79
+ "version": "0.5.9",
80
+ "sha256": "281edf0c38f358497c7e2066fa8217a2ba3e2a39b4205c4af0c41d328fc035a1"
81
81
  },
82
82
  {
83
83
  "name": "review",
84
84
  "file": "assets/skills/review/SKILL.md",
85
85
  "description": "Skill de revisión de código (Superpowers)",
86
- "version": "0.5.7",
87
- "sha256": "fc68671ac4592ceeae6bf0d3ac33abaf78cd34e94e67c07ad0329843d6003468"
86
+ "version": "0.5.9",
87
+ "sha256": "14831d9ef3746ef6e2e6047c868fe7aa296d01f0644227ce776fe99436357b1a"
88
88
  },
89
89
  {
90
90
  "name": "receiving-code-review",
91
91
  "file": "assets/skills/receiving-code-review/SKILL.md",
92
92
  "description": "Skill de recibir y procesar feedback de code review",
93
- "version": "0.5.7",
94
- "sha256": "b6ace0ce93c5eade7b3f17d6744aa2fca6f492632b80e5dd754f93ba2c331550"
93
+ "version": "0.5.9",
94
+ "sha256": "0a5780e8a41539d15428114b7c46f36670acc16aba0db3348d36be1175433872"
95
95
  },
96
96
  {
97
97
  "name": "openspec-propose",
98
98
  "file": "assets/skills/openspec-propose/SKILL.md",
99
99
  "description": "Skill de generación de proposal (OpenSpec)",
100
- "version": "0.5.7",
101
- "sha256": "674b84346d954f61fbbe53d1efb7e6cd9a761b49ea6a9950aa9dfbfe6366a165"
100
+ "version": "0.5.9",
101
+ "sha256": "3306b21ba9cb8c1611e8ba335126982f3767f7d317b5ed506c57a14c5f88b263"
102
102
  },
103
103
  {
104
104
  "name": "openspec-apply-change",
105
105
  "file": "assets/skills/openspec-apply-change/SKILL.md",
106
106
  "description": "Skill de aplicación de change (OpenSpec)",
107
- "version": "0.5.7",
108
- "sha256": "9e4fe9fe780b580bfba9f842c3c3603e77150355f8bf2b68a85e8ce41e2c941a"
107
+ "version": "0.5.9",
108
+ "sha256": "31c1313c3de4616a07efd89306c4c058175d018f42d4381cc7da7030fd03ba30"
109
109
  },
110
110
  {
111
111
  "name": "openspec-archive-change",
112
112
  "file": "assets/skills/openspec-archive-change/SKILL.md",
113
113
  "description": "Skill de archivo de change (OpenSpec)",
114
- "version": "0.5.7",
115
- "sha256": "4c611a20654841c585fe1c6f573e6e69c8b95c4eecdfe5552923be44dd3da0c4"
114
+ "version": "0.5.9",
115
+ "sha256": "5bf65d1848457bd57be1d21707e55fe6746cdee33bce2f32f12d87661b6aa3a4"
116
116
  },
117
117
  {
118
118
  "name": "using-git-worktrees",
119
119
  "file": "assets/skills/using-git-worktrees/SKILL.md",
120
120
  "description": "Skill de uso de git worktrees para aislamiento de trabajo",
121
- "version": "0.5.7",
122
- "sha256": "ff16257abe3836ca0f1003f130236a79acf4c8574a2e9fed092a5101d52ae860"
121
+ "version": "0.5.9",
122
+ "sha256": "33433c24f753d566cde16c0931f70d746b6b0cb4d1f3daaa619e928b009e4fc9"
123
123
  },
124
124
  {
125
125
  "name": "using-superpowers",
126
126
  "file": "assets/skills/using-superpowers/SKILL.md",
127
127
  "description": "Skill de orquestación de Superpowers skills",
128
- "version": "0.5.7",
129
- "sha256": "bc8bd5445b623c1158f83c518155518b6556c58dd194f73f3bab444a983410ab"
128
+ "version": "0.5.9",
129
+ "sha256": "fba542003aa788b5edb4ae2d1135773a1dde8111d1dd60378e2f61b6695d46fc"
130
130
  },
131
131
  {
132
132
  "name": "writing-skills",
133
133
  "file": "assets/skills/writing-skills/SKILL.md",
134
134
  "description": "Skill de creación y edición de skills",
135
- "version": "0.5.7",
136
- "sha256": "915369d6a74570c1605ad49d6e6551a946ca232b12400fbb45797149f16a7f2c"
135
+ "version": "0.5.9",
136
+ "sha256": "7f74ffe049283803640d78f8c9ac1ea5f5e626dedb885ecff29a87c5b5d41598"
137
137
  }
138
138
  ]
139
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ostacky",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "Instalador interactivo de agentes y comandos para OpenCode",
5
5
  "type": "module",
6
6
  "bin": {