zugzbot 1.6.0 → 1.8.0

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.
@@ -65,11 +65,40 @@ Eres el coordinador principal del arnés de desarrollo SDD (Spec-Driven Developm
65
65
  4. **Autopiloto (/loop)**:
66
66
  - Si el usuario especificó `/loop N` o `iteraciones=N`, autoselecciona Next.js 16, Console mode, y el diseño `default` nativo.
67
67
  - Transiciona atómicamente a F1 con `sdd_set_phase({ phase: "F1_CONTRACT", spec_name: "<nombre-kebab>", loopMode: true, loopTargetIterations: N, loopCurrentIteration: 1 })`.
68
- 5. **Normal (HIL) — Pregunta Mínima SINGLE-QUESTION**: Si NO se detectó Fast-Track Dashboard, llama **UNA sola vez** a `question` con **una sola pregunta** (NO múltiples):
69
- - **PROHIBIDO** hacer arrays de múltiples preguntas en `questions: [...]` causa errores de JSON parsing con caracteres especiales. SOLO una pregunta por llamada.
70
- - **Defaults razonables automáticos** (NO preguntes): Stack = Next.js 16 + Shadcn UI, Persistencia = localStorage/mock, Diseño = `default` nativo.
71
- - Si llegas a preguntar, hazlo solo sobre `verificationMode` (Console vs Visual).
72
- - Luego, transiciona con `sdd_set_phase` a `F1_CONTRACT`.
68
+ 5. **Normal (HIL) — Interrogatorio Secuencial (5 preguntas)**: Si NO se detectó Fast-Track Dashboard NI `/loop`, ejecuta un wizard de 5 pasos usando llamadas separadas a `question` (1 por paso). Cada respuesta informa al siguiente paso. **REGLA DURA**: SOLO una pregunta por llamada a `question`. PROHIBIDO hacer arrays de múltiples preguntas en `questions: [...]` — causa errores de JSON parsing con caracteres especiales.
69
+ - **Defaults razonables** (si el usuario no sabe o aborta): Stack = Next.js 16 + Shadcn UI, Persistencia = localStorage/mock, Diseño = `default` zinc nativo, Track = SDD, Verificación = Console.
70
+ - **Mecanismo de salida rápida**: el usuario puede escribir "default", "skip" o "recomienda" en cualquier paso y saltas al siguiente asumiendo el default recomendado.
71
+
72
+ **5.1 PROPÓSITO** (libre + categorías sugeridas):
73
+ - Pregunta: *"¿Qué quieres construir? Descríbelo en 1-2 frases."*
74
+ - Opciones sugeridas (con descripciones cortas): Landing/Marketing, Dashboard/App interna, E-commerce, API/Backend, Script/Tooling, Otro.
75
+ - **Guarda la respuesta** internamente; se usará para guiar las recomendaciones de stack (5.3) y diseño (5.4).
76
+
77
+ **5.2 TRACK (Fast vs SDD)**:
78
+ - Pregunta: *"¿Modo Fast (1 turno directo, sin contratos/tests/docker) o SDD completo (F0→F4 con contratos, tests y deploy)?"*
79
+ - Recomendación: **SDD** si el alcance implica >1 componente, persistencia, o deploy; **Fast** solo para micro-fixes triviales o scripts puntuales.
80
+ - Si elige **Fast** → delega a `@sdd-coder` directamente, llama `sdd_set_phase({ phase: "F2_IMPLEMENTATION", skip_lint_gate: true })` saltando F1/F3/F4, y termina el flujo SDD para esta sesión.
81
+
82
+ **5.3 STACK (con recomendaciones priorizadas)**:
83
+ - Si `sdd_get_initial_session_data` ya reportó `state.stack.core.length > 0` (bootstrap previo) → pregunta: *"Detecté stack existente `[X, Y]`. ¿Lo mantenemos o cambiamos?"*
84
+ - Si NO hay stack detectado → ofrece top-3 priorizados en este orden:
85
+ 1. **Next.js 16 + Shadcn UI + Tailwind v4** (recomendado para frontend/landing/dashboard).
86
+ 2. **FastAPI + Pydantic + Uvicorn** (recomendado para backend/API).
87
+ 3. **Stack existente en el repo / Agnóstico** (scripts sin UI, Bash, Apps Script, etc.).
88
+ - Marca con "(Recomendado)" la primera opción.
89
+
90
+ **5.4 DISEÑO (con preview_url del catálogo unificado)**:
91
+ - Si la detección 2.5 ya trajo keywords de catálogo → reutiliza esos resultados (ya traen `preview_url`).
92
+ - Si NO se detectaron keywords → llama `sdd_catalog_list_blocks({ registry: "all", limit: 5 })` y presenta los top-3 con: nombre, registry (`shadcn`/`basecn`/`reactbits`/`blocks-so`), descripción corta de 1 línea, **`preview_url`** y `install_command`.
93
+ - **OBLIGATORIO**: incluir SIEMPRE el `preview_url` en la respuesta para que el usuario vea el demo en su navegador antes de elegir. Para reactbits el demo es animado; para blocks.so es live en `https://blocks.so/<category>/<name>`.
94
+ - Opción final: **"default zinc nativo"** (omite catálogo y usa el tema base del template `nextjs-shadcn`).
95
+ - Si el usuario eligió un bloque → el spec-writer lo inyectará en `sdd_hints.blocks_to_install[]` del contrato.
96
+
97
+ **5.5 VERIFICACIÓN (Console vs Visual)**:
98
+ - Pregunta: *"¿Verificación Console (5x más rápido, sin Playwright) o Visual (screenshots en `.openspec/ts-*.png` y tests E2E)?"*
99
+ - Recomendado: **Console** salvo que sea landing pública, portfolio, o el usuario quiera ver el resultado visualmente antes de cerrar.
100
+
101
+ **Cierre del wizard**: tras las 5 respuestas (o defaults), llama a `sdd_set_phase({ phase: "F1_CONTRACT", spec_name: "<kebab>", coreStack: [...], databases: [...] })` con los valores inferidos de 5.3. La respuesta de 5.4 se inyectará en `sdd_hints.design_choice` para que el spec-writer la consulte.
73
102
  </f0_detect>
74
103
 
75
104
  <f1_contract>
@@ -68,12 +68,7 @@ Eres el Validador de Contratos (sdd-tester) del flujo SDD. Tu trabajo es ejecuta
68
68
  - **Auto-generación de Plantillas (OBLIGATORIO)**: Al comenzar, ejecuta obligatoriamente `sdd_generate_tests` para autogenerar las plantillas a partir de los `test_scenarios` del contrato. Las plantillas ya incluyen imports reales, mocks estándar (Proxy lucide-react, mock reactivo next-themes), y assertions significativas basadas en el `then` de cada escenario. NO las reescribas desde cero.
69
69
  - **NO explorar el código**: Tu brief ya contiene los archivos de producción a testear y los patrones de mock pre-armados. NO hagas `read` masivos de componentes, NO hagas `glob` ciegos. Solo lee el archivo de test específico que estés arreglando.
70
70
  - **Preparación de Puerto**: Llama proactivamente a `sdd_free_port` para liberar el puerto de pruebas.
71
- - **Ejecución Incremental/Dirigida**: Ejecuta la suite de pruebas de forma focalizada apuntando únicamente a los archivos de test específicos relacionados con la funcionalidad modificada (ej. `npx vitest run src/__tests__/<test_name>.test.tsx` o `pytest tests/unit/<test_name>.py` en lugar de correr toda la base de pruebas), acelerando exponencialmente los tiempos de espera.
72
- - **Timeout duro OBLIGATORIO**: SIEMPRE ejecuta vitest con `--testTimeout=10000 --bail=1` para fallar rápido en el primer error. NUNCA ejecutes `vitest` sin estos flags. Esto previene cuelgues infinitos por imports circulares o chains de Providers que happy-dom no puede resolver.
73
- ```bash
74
- npx vitest run --testTimeout=10000 --bail=1 --reporter=verbose src/__tests__/<file>.test.tsx
75
- ```
76
- Si el comando excede 60 segundos, ABORTA inmediatamente y reporta el cuelgue. NO esperes más de 1 minuto bajo ninguna circunstancia.
71
+ - **Ejecución Incremental/Dirigida (Uso Mandatorio de Tool de Tests)**: Para ejecutar la suite de pruebas de forma focalizada ahorrando una enorme cantidad de tokens de logs de consola, **DEBES** utilizar la herramienta `sdd_run_tests({ file: "src/__tests__/<test_name>.test.tsx" })` (o la ruta correspondiente en pytest). Solo utiliza comandos `bash` directos (como `npx vitest run --testTimeout=10000 --bail=1 --reporter=verbose ...`) si la herramienta `sdd_run_tests` reporta un fallo de entorno o si necesitas depurar localmente. Si un comando de tests excede 60 segundos, ABORTA de inmediato y reporta el cuelgue.
77
72
  - **Si el test runner se cuelga**: Antes de reintentar, ejecuta `npx tsc --noEmit` y `npm run build` para validar la compilación. Si ambos pasan, el problema es del test runner mismo (happy-dom + componente complejo). Reporta al orquestador como "rollback evitable" — el código de producción es válido.
78
73
  - **Linter Focalizado**: Ejecuta `npx eslint` apuntando específicamente a los archivos de producción modificados listados en `files_affected` del brief y a tus archivos de test (ej. `npx eslint src/components/blocks/MyBlock.tsx src/__tests__/MyBlock.test.tsx`), optimizando el análisis estático.
79
74
  - **Resolución Proactiva de Fallas**: Si los tests fallan debido a problemas menores de mocking, importaciones incorrectas, llaves duplicadas en React o configuraciones de test, **tienes autorización total para editarlos y repararlos tú mismo** dentro de `tests/` o `src/__tests__/`. Solo si el error es un fallo lógico en el código de producción de la aplicación (el cual tienes prohibido modificar), debes reportar un rollback a `F2_IMPLEMENTATION`.
@@ -10,6 +10,7 @@ Estás ejecutando el ciclo SDD en modo **Autopiloto Autónomo** para resolver la
10
10
  > $ARGUMENTS
11
11
 
12
12
  ## Instrucciones de Autopiloto Obligatorias:
13
+ 0. **Bypass total del wizard F0**: este modo bypasea las 5 preguntas secuenciales de F0 (Propósito → Track → Stack → Diseño → Verificación) y salta directo a F1 asumiendo todos los defaults recomendados. El `loop-enforcer` plugin bloquea mecánicamente cualquier intento de usar `question` en este modo.
13
14
  1. Activa inmediatamente el modo piloto en el estado llamando a `sdd_set_phase` pasándole `phase: "F1_CONTRACT"`, un `spec_name` descriptivo en kebab-case, `loopMode: true`, `loopTargetIterations: N` (si el usuario ingresó un número en su comando, ej: `/loop 3` o `iteraciones=3`; por defecto 1), y `loopCurrentIteration: 1`.
14
15
  2. Tienes estrictamente prohibido usar la herramienta `question` para pedir confirmaciones de diseño, stack o contratos.
15
16
  3. Elige los valores recomendados por defecto de forma autónoma (Next.js 16, Console mode, diseño `shadcn-zinc` nativo del template).
@@ -436,6 +436,12 @@ export const SddBridgePlugin: Plugin = async ({ project, client, $, directory, w
436
436
  try { fs.rmSync(playwrightTmpDir, { recursive: true, force: true }) } catch (e) {}
437
437
  }
438
438
 
439
+ // 2.5 Clear ZCS raw_context cache
440
+ const rawContextDir = path.resolve(openspecDir, ".raw_context")
441
+ if (fs.existsSync(rawContextDir)) {
442
+ try { fs.rmSync(rawContextDir, { recursive: true, force: true }) } catch (e) {}
443
+ }
444
+
439
445
  // 3. Clear transient metrics file
440
446
  const metricsPath = path.resolve(openspecDir, ".sdd_session_metrics.json")
441
447
  if (fs.existsSync(metricsPath)) {
@@ -96,3 +96,9 @@ Para evitar que los subagentes se queden sin pasos (step/token exhaustion) o acu
96
96
  - **Compilación Atómica:** El Coder **debe** implementar y compilar exitosamente cada archivo de forma individual antes de escribir el siguiente. Está estrictamente prohibido escribir todos los componentes de golpe sin ejecutar validaciones intermedias de TypeScript (`tsc --noEmit`).
97
97
  - **Verificación Temprana de Dependencias:** El Coder **debe** comprobar en el primer paso de F2 que dependencias de pruebas críticas como `@testing-library/dom` estén correctamente instaladas en proyectos React 19 para evitar fallos intermitentes de tipado en Vitest.
98
98
  - **PROHIBICIÓN ESTRICTA DE `todowrite` en Coder/Tester/Deployer/Spec-Writer:** Estas herramientas **no deben invocar** `todowrite` en ningún momento de su sesión. El seguimiento de fases está centralizado en el Orquestador. Inyectar TODOs en subagentes satura su contexto y agota pasos de LLM innecesariamente. El Orquestador solo debe usar `todowrite` **2 veces máximo por sesión**: una al inicio (lista de fases) y otra al final (marcar todas completadas).
99
+
100
+ ## 10. Compresión y Recuperación de Contexto (ZCS)
101
+ Para maximizar la eficiencia en el uso de la ventana de contexto y ahorrar tokens de API, el arnés implementa Zugzbot Context Shaper (ZCS) con un esquema de recuperación reversible (CCR):
102
+ - **Placeholders de Compresión:** Si detectas bloques de texto en el brief o en los logs de error con la estructura `[CONTEXTO_COMPRIMIDO: hash (Tipo: Categoria, Líneas: N)]`, significa que el contenido original ha sido archivado localmente para no saturar tu contexto.
103
+ - **Recuperación bajo demanda (MANDATORIO):** Si necesitas ver el contenido completo y exacto sin comprimir (por ejemplo, para depurar un stack trace de error largo o leer patrones de mock enteros), debes invocar obligatoriamente la herramienta `sdd_retrieve_raw({ hash: "hash_identificador" })`. No asumas ni inventes código o logs que estén comprimidos; recupéralos de forma dirigida.
104
+
@@ -40,14 +40,14 @@ F4 → deployer genera Dockerfile + build + verify
40
40
  ## Reglas duras
41
41
 
42
42
  1. **Coordinación pura**: el orquestador NO escribe/edita código. Solo delega vía `task`.
43
- 2. **Una pregunta por sesión**: solo Console vs Visual en F0. Todo lo demás por default.
43
+ 2. **Hasta 5 preguntas SECUENCIALES en F0 (HIL)**: Propósito Track (Fast/SDD) → Stack (con recomendaciones priorizadas) → Diseño (con `preview_url` del catálogo unificado) → Verificación (Console/Visual). Cada paso es 1 llamada separada a `question` (PROHIBIDO arrays múltiples — causa errores JSON). `/loop` y Fast-Track Dashboard bypasean TODAS las preguntas y asumen defaults. El usuario puede escribir `default`/`skip`/`recomienda` en cualquier paso para aceptar el default y continuar.
44
44
  3. **Fast-Track Dashboard**: si el usuario pide dashboard/admin/panel → usar `@shadcn/dashboard-01` directamente sin preguntar más.
45
45
  4. **Brain obligatorio**: save 1-3 lecciones al cerrar cada sesión, read learnings+errors en F0.
46
46
  5. **Tests con assertions reales**: nunca stubs con `expect(true).toBe(true)`.
47
47
 
48
48
  ## Métricas objetivo de una sesión
49
49
 
50
- - F0: 1 ronda de `question` con 1 sola decisión
50
+ - F0 HIL: hasta 5 rondas de `question` (Propósito Track → Stack → Diseño → Verificación); 0 rondas si `/loop` o Fast-Track Dashboard
51
51
  - F1: contract.json 80-120 líneas, 3-5 test_scenarios
52
52
  - F2: self-audit limpio en 1 intento
53
53
  - F3: lint + tests verdes en 1 intento
@@ -0,0 +1,242 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import fs from "fs"
3
+ import path from "path"
4
+ import crypto from "crypto"
5
+
6
+ // Helper to safely resolve root directory
7
+ const getRoot = (context: any) => {
8
+ if (context?.directory && context.directory !== "/") return context.directory;
9
+ if (context?.worktree && context.worktree !== "/") return context.worktree;
10
+ if (context?.cwd && context.cwd !== "/") return context.cwd;
11
+ return process.cwd();
12
+ };
13
+
14
+ /**
15
+ * Comprime código fuente eliminando comentarios, docstrings y líneas vacías.
16
+ */
17
+ export function compressCode(code: string, commentStyle: "slash" | "hash" = "slash"): string {
18
+ if (!code) return ""
19
+
20
+ let inString: string | null = null
21
+ let inComment: "single" | "multi" | null = null
22
+ let result = ""
23
+
24
+ for (let i = 0; i < code.length; i++) {
25
+ const char = code[i]
26
+ const nextChar = code[i + 1] || ""
27
+
28
+ if (inComment === "multi") {
29
+ if (char === "*" && nextChar === "/") {
30
+ inComment = null
31
+ i++ // Skip '/'
32
+ }
33
+ continue
34
+ }
35
+
36
+ if (inComment === "single") {
37
+ if (char === "\n" || char === "\r") {
38
+ inComment = null
39
+ result += char
40
+ }
41
+ continue
42
+ }
43
+
44
+ if (inString) {
45
+ if (char === inString && code[i - 1] !== "\\") {
46
+ inString = null
47
+ }
48
+ result += char
49
+ continue
50
+ }
51
+
52
+ // Check for comments based on style
53
+ if (commentStyle === "slash") {
54
+ if (char === "/" && nextChar === "*") {
55
+ inComment = "multi"
56
+ i++
57
+ continue
58
+ }
59
+ if (char === "/" && nextChar === "/") {
60
+ inComment = "single"
61
+ i++
62
+ continue
63
+ }
64
+ } else if (commentStyle === "hash") {
65
+ if (char === "#") {
66
+ inComment = "single"
67
+ continue
68
+ }
69
+ }
70
+
71
+ // Check for string start
72
+ if (char === "'" || char === '"' || char === "`") {
73
+ inString = char
74
+ }
75
+
76
+ result += char
77
+ }
78
+
79
+ // Clean up empty lines
80
+ return result
81
+ .split(/\r?\n/)
82
+ .filter(line => line.trim().length > 0)
83
+ .join("\n")
84
+ }
85
+
86
+ /**
87
+ * Comprime cadenas JSON grandes recortando arrays a un tamaño máximo.
88
+ */
89
+ export function compressJson(jsonStr: string, maxItems = 3): string {
90
+ if (!jsonStr) return ""
91
+ try {
92
+ const obj = JSON.parse(jsonStr)
93
+
94
+ const prune = (o: any): any => {
95
+ if (Array.isArray(o)) {
96
+ if (o.length > maxItems) {
97
+ const sliced = o.slice(0, maxItems).map(prune)
98
+ sliced.push(`... [ZCS: ${o.length - maxItems} elementos omitidos]`)
99
+ return sliced
100
+ }
101
+ return o.map(prune)
102
+ } else if (o !== null && typeof o === "object") {
103
+ const res: any = {}
104
+ for (const k of Object.keys(o)) {
105
+ res[k] = prune(o[k])
106
+ }
107
+ return res
108
+ }
109
+ return o
110
+ }
111
+
112
+ return JSON.stringify(prune(obj), null, 2)
113
+ } catch (e) {
114
+ // Si no es JSON válido, devolver el texto original
115
+ return jsonStr
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Comprime logs de consola de tests unitarios (Vitest/pytest),
121
+ * preservando los fallos, errores y resúmenes de suites.
122
+ */
123
+ export function compressTestLogs(logs: string): string {
124
+ if (!logs) return ""
125
+ const lines = logs.split(/\r?\n/)
126
+ const result: string[] = []
127
+
128
+ let recordingError = false
129
+ let errorLinesCount = 0
130
+
131
+ for (const line of lines) {
132
+ const upper = line.toUpperCase()
133
+
134
+ // Detectar inicio de fallos o errores
135
+ const isErrorIndicator =
136
+ line.includes("FAIL") ||
137
+ line.includes("Error:") ||
138
+ line.includes("AssertionError") ||
139
+ line.includes("❌") ||
140
+ upper.includes("EXCEPTION") ||
141
+ upper.includes("FAILED")
142
+
143
+ if (isErrorIndicator) {
144
+ recordingError = true
145
+ errorLinesCount = 0
146
+ result.push(line)
147
+ continue
148
+ }
149
+
150
+ if (recordingError) {
151
+ // Guardar las primeras 8 líneas del error/stack trace
152
+ if (errorLinesCount < 8) {
153
+ result.push(line)
154
+ errorLinesCount++
155
+ } else {
156
+ // Truncar stack traces largos del framework (node_modules/vitest, etc.)
157
+ if (line.trim() === "" || line.includes("PASS") || line.includes("Test Suites:")) {
158
+ recordingError = false
159
+ result.push(" [... ZCS: stack trace del framework omitido para ahorrar tokens ...]")
160
+ result.push(line)
161
+ }
162
+ }
163
+ } else {
164
+ // Siempre conservar resúmenes e información de suites exitosas
165
+ const isSummaryIndicator =
166
+ line.includes("PASS") ||
167
+ line.includes("Summary") ||
168
+ line.includes("Test Suites:") ||
169
+ line.includes("Tests:") ||
170
+ line.includes("Snapshots:") ||
171
+ line.includes("Time:") ||
172
+ line.includes("Ran all test suites")
173
+
174
+ if (isSummaryIndicator || line.trim().startsWith("✓")) {
175
+ result.push(line)
176
+ }
177
+ }
178
+ }
179
+
180
+ // Si no se capturaron líneas o todo pasó muy rápido, retornar resumido
181
+ if (result.length === 0) {
182
+ return logs.length > 500 ? `${logs.slice(0, 500)}\n... [ZCS: truncado por longitud]` : logs
183
+ }
184
+
185
+ return result.join("\n")
186
+ }
187
+
188
+ /**
189
+ * Registra el contenido crudo en disco (CCR) y devuelve un placeholder comprimido.
190
+ */
191
+ export function ccrCompress(content: string, category: string, context: any): string {
192
+ if (!content) return ""
193
+
194
+ const root = getRoot(context)
195
+ const rawContextDir = path.resolve(root, ".openspec/.raw_context")
196
+
197
+ if (!fs.existsSync(rawContextDir)) {
198
+ fs.mkdirSync(rawContextDir, { recursive: true })
199
+ }
200
+
201
+ // Calcular hash del contenido para evitar duplicados
202
+ const hash = crypto.createHash("sha256").update(content).digest("hex").slice(0, 12)
203
+ const destFile = path.join(rawContextDir, `${hash}.txt`)
204
+
205
+ // Escribir solo si no existe ya
206
+ if (!fs.existsSync(destFile)) {
207
+ fs.writeFileSync(destFile, content, "utf8")
208
+ }
209
+
210
+ const lineCount = content.split(/\r?\n/).length
211
+ return `[CONTEXTO_COMPRIMIDO: ${hash} (Tipo: ${category}, Líneas: ${lineCount}). Si necesitas ver el contenido completo sin compresión para editar o depurar, ejecuta la herramienta sdd_retrieve_raw({ hash: "${hash}" })]`
212
+ }
213
+
214
+ // Tool: sdd_retrieve_raw
215
+ export const retrieve_raw = tool({
216
+ description: "Recupera un bloque de contexto original sin comprimir (logs, código, JSON) a partir de su hash identificador.",
217
+ args: {
218
+ hash: tool.schema.string().describe("El hash de 12 caracteres del contexto que deseas recuperar (ej. 'a1b2c3d4e5f6').")
219
+ },
220
+ async execute(args, context) {
221
+ try {
222
+ const root = getRoot(context)
223
+ const hash = args.hash.trim()
224
+ const rawPath = path.resolve(root, `.openspec/.raw_context/${hash}.txt`)
225
+
226
+ if (!fs.existsSync(rawPath)) {
227
+ return JSON.stringify({
228
+ success: false,
229
+ message: `No se encontró el contexto original para el hash '${hash}'. El caché podría haber sido purgado.`
230
+ }, null, 2)
231
+ }
232
+
233
+ const content = fs.readFileSync(rawPath, "utf8")
234
+ return content
235
+ } catch (e: any) {
236
+ return JSON.stringify({
237
+ success: false,
238
+ message: `Error al recuperar contexto crudo: ${e.message || e}`
239
+ }, null, 2)
240
+ }
241
+ }
242
+ })
@@ -2,6 +2,7 @@ import { tool } from "@opencode-ai/plugin"
2
2
  import fs from "fs"
3
3
  import path from "path"
4
4
  import { execSync } from "child_process"
5
+ import { ccrCompress } from "./sdd_compress.ts"
5
6
 
6
7
  // Helper to safely resolve root directory (avoiding OpenCode bug where worktree is '/' in non-git repos)
7
8
  const getRoot = (context: any) => {
@@ -362,6 +363,15 @@ export const set_phase = tool({
362
363
  currentState.loopTargetIterations = 1
363
364
  currentState.loopCurrentIteration = 1
364
365
  }
366
+ // Clean up raw_context files (ZCS)
367
+ try {
368
+ const rawContextDir = path.resolve(root, ".openspec/.raw_context")
369
+ if (fs.existsSync(rawContextDir)) {
370
+ fs.rmSync(rawContextDir, { recursive: true, force: true })
371
+ }
372
+ } catch (e) {
373
+ // ignore
374
+ }
365
375
  // Clean up running servers
366
376
  const pidFile = getPidFilePath(root)
367
377
  if (fs.existsSync(pidFile)) {
@@ -624,16 +634,17 @@ export const save_active_brief = tool({
624
634
  const errors = last("errors", 5)
625
635
  const design = last("design", 3)
626
636
 
627
- if (learnings || errors || design) {
637
+ let brainMemoryStr = ""
638
+ if (design) brainMemoryStr += `\n### Design (aplicar en estilo):\n${design}\n`
639
+ if (learnings) brainMemoryStr += `\n### Learnings (patrones validados):\n${learnings}\n`
640
+ if (errors) brainMemoryStr += `\n### Errors / Regresiones (evitar repetir):\n${errors}\n`
641
+
642
+ if (brainMemoryStr) {
628
643
  enrichedBrief += `\n\n## [AUTO-INJECTED] Brain Memory (lecciones de sesiones previas)\n`
629
- if (design) {
630
- enrichedBrief += `\n### Design (aplicar en estilo):\n${design}\n`
631
- }
632
- if (learnings) {
633
- enrichedBrief += `\n### Learnings (patrones validados):\n${learnings}\n`
634
- }
635
- if (errors) {
636
- enrichedBrief += `\n### Errors / Regresiones (evitar repetir):\n${errors}\n`
644
+ if (brainMemoryStr.length > 1500) {
645
+ enrichedBrief += ccrCompress(brainMemoryStr, "Brain Memory", context) + "\n"
646
+ } else {
647
+ enrichedBrief += brainMemoryStr
637
648
  }
638
649
  }
639
650
  }
@@ -645,31 +656,35 @@ export const save_active_brief = tool({
645
656
  try {
646
657
  const isNext = contract?.stack?.core?.some((c: string) => /next\.?js/i.test(c))
647
658
  if (isNext) {
659
+ let mockPatterns = `// Mock dinámico de lucide-react (cualquier icono)\n`
660
+ mockPatterns += `vi.mock("lucide-react", () => new Proxy({}, {\n`
661
+ mockPatterns += ` get: (_t, prop) => {\n`
662
+ mockPatterns += ` const Icon = (props: any) => null;\n`
663
+ mockPatterns += ` Icon.displayName = String(prop);\n`
664
+ mockPatterns += ` return Icon;\n`
665
+ mockPatterns += ` },\n`
666
+ mockPatterns += `}));\n\n`
667
+ mockPatterns += `// Mock reactivo de next-themes para ThemeToggle\n`
668
+ mockPatterns += `let theme = "light";\n`
669
+ mockPatterns += `vi.mock("next-themes", () => ({\n`
670
+ mockPatterns += ` useTheme: () => ({\n`
671
+ mockPatterns += ` get theme() { return theme; },\n`
672
+ mockPatterns += ` setTheme: (t: string) => { theme = t; },\n`
673
+ mockPatterns += ` get resolvedTheme() { return theme; },\n`
674
+ mockPatterns += ` }),\n`
675
+ mockPatterns += `}));\n\n`
676
+ mockPatterns += `// Mock de next/navigation\n`
677
+ mockPatterns += `vi.mock("next/navigation", () => ({\n`
678
+ mockPatterns += ` useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }),\n`
679
+ mockPatterns += ` usePathname: () => "/",\n`
680
+ mockPatterns += `}));\n`
681
+
648
682
  enrichedBrief += `\n\n## [AUTO-INJECTED] Mock Patterns (Vitest + Testing Library)\n`
649
- enrichedBrief += `\`\`\`typescript\n`
650
- enrichedBrief += `// Mock dinámico de lucide-react (cualquier icono)\n`
651
- enrichedBrief += `vi.mock("lucide-react", () => new Proxy({}, {\n`
652
- enrichedBrief += ` get: (_t, prop) => {\n`
653
- enrichedBrief += ` const Icon = (props: any) => null;\n`
654
- enrichedBrief += ` Icon.displayName = String(prop);\n`
655
- enrichedBrief += ` return Icon;\n`
656
- enrichedBrief += ` },\n`
657
- enrichedBrief += `}));\n\n`
658
- enrichedBrief += `// Mock reactivo de next-themes para ThemeToggle\n`
659
- enrichedBrief += `let theme = "light";\n`
660
- enrichedBrief += `vi.mock("next-themes", () => ({\n`
661
- enrichedBrief += ` useTheme: () => ({\n`
662
- enrichedBrief += ` get theme() { return theme; },\n`
663
- enrichedBrief += ` setTheme: (t: string) => { theme = t; },\n`
664
- enrichedBrief += ` get resolvedTheme() { return theme; },\n`
665
- enrichedBrief += ` }),\n`
666
- enrichedBrief += `}));\n\n`
667
- enrichedBrief += `// Mock de next/navigation\n`
668
- enrichedBrief += `vi.mock("next/navigation", () => ({\n`
669
- enrichedBrief += ` useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }),\n`
670
- enrichedBrief += ` usePathname: () => "/",\n`
671
- enrichedBrief += `}));\n`
672
- enrichedBrief += `\`\`\`\n`
683
+ if (mockPatterns.length > 1000) {
684
+ enrichedBrief += ccrCompress(mockPatterns, "Mock Patterns", context) + "\n"
685
+ } else {
686
+ enrichedBrief += `\`\`\`typescript\n${mockPatterns}\`\`\`\n`
687
+ }
673
688
  }
674
689
  } catch (e) {
675
690
  // ignore
@@ -2,6 +2,7 @@ import { tool } from "@opencode-ai/plugin"
2
2
  import fs from "fs"
3
3
  import path from "path"
4
4
  import { execSync } from "child_process"
5
+ import { compressTestLogs, ccrCompress } from "./sdd_compress.ts"
5
6
 
6
7
  // Helper to safely resolve root directory (avoiding OpenCode bug where worktree is '/' in non-git repos)
7
8
  const getRoot = (context: any) => {
@@ -669,3 +670,62 @@ export const save_playwright_artifacts = tool({
669
670
  }
670
671
  }
671
672
  })
673
+
674
+ // Tool: sdd_run_tests
675
+ export const run_tests = tool({
676
+ description: "Ejecuta las pruebas unitarias y de integración del proyecto (Vitest o pytest), comprimiendo la salida para ahorrar tokens.",
677
+ args: {
678
+ file: tool.schema.string().optional().describe("La ruta específica del archivo de test a ejecutar (relativa al root). Si no se pasa, corre todos los tests."),
679
+ timeout: tool.schema.number().optional().default(10000).describe("Timeout en milisegundos para la ejecución de cada test (solo aplicable a Vitest). Default: 10000.")
680
+ },
681
+ async execute(args, context) {
682
+ const root = getRoot(context)
683
+ const targetDir = getTargetDir(root)
684
+ const targetPath = targetDir === "." ? root : path.resolve(root, targetDir)
685
+
686
+ // Detectar si es un proyecto Python
687
+ const isPython = fs.existsSync(path.resolve(targetPath, "requirements.txt")) || fs.existsSync(path.resolve(targetPath, "pyproject.toml"))
688
+
689
+ // Obtener ruta del archivo relativa al directorio de destino si aplica
690
+ let testPathArg = ""
691
+ if (args.file) {
692
+ const fullPath = path.resolve(root, args.file)
693
+ testPathArg = path.relative(targetPath, fullPath)
694
+ }
695
+
696
+ let cmd = ""
697
+ if (isPython) {
698
+ cmd = `pytest ${testPathArg || "tests/"}`
699
+ } else {
700
+ const testTimeout = args.timeout ?? 10000
701
+ cmd = `npx vitest run --testTimeout=${testTimeout} --bail=1 --reporter=verbose ${testPathArg}`
702
+ }
703
+
704
+ try {
705
+ const out = execSync(cmd, {
706
+ cwd: targetPath,
707
+ encoding: "utf8",
708
+ timeout: 120_000, // timeout global del proceso de 2 minutos
709
+ stdio: "pipe"
710
+ })
711
+
712
+ const compressed = compressTestLogs(out)
713
+
714
+ return JSON.stringify({
715
+ status: "SUCCESS",
716
+ message: "Las pruebas se ejecutaron y pasaron exitosamente.",
717
+ output: compressed.length > 1500 ? ccrCompress(out, "Test Success Output", context) : compressed
718
+ }, null, 2)
719
+ } catch (e: any) {
720
+ const rawOutput = e.stdout?.toString() || e.stderr?.toString() || e.message || ""
721
+ const compressed = compressTestLogs(rawOutput)
722
+
723
+ return JSON.stringify({
724
+ status: "FAIL",
725
+ message: "La ejecución de pruebas falló. Corrige los errores mostrados abajo.",
726
+ output: compressed.length > 1500 ? ccrCompress(rawOutput, "Test Failure Output", context) : compressed
727
+ }, null, 2)
728
+ }
729
+ }
730
+ })
731
+
@@ -9,7 +9,7 @@ DB_PATH = "/Users/wavesbyte/.local/share/opencode/opencode.db"
9
9
 
10
10
  # Escribe aquí el ID de la sesión que deseas exportar (ejemplo: "ses_1234...")
11
11
  # Si se deja vacío, el script requerirá el ID como argumento al ejecutarlo
12
- TARGET_SESSION_ID = "ses_1174"
12
+ TARGET_SESSION_ID = "ses_1130"
13
13
 
14
14
 
15
15
  def format_timestamp(ts):
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "zugzbot",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
+
4
5
  "description": "Fácil instalador del arnés SDD de Zugzbot para proyectos OpenCode",
5
6
  "type": "module",
6
7
  "bin": {