zugzbot 1.0.18 → 1.0.20

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.
@@ -0,0 +1,152 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import fs from "fs"
3
+ import path from "path"
4
+ import { execSync, spawn } from "child_process"
5
+
6
+ // Helper to safely resolve root directory (avoiding OpenCode bug where worktree is '/' in non-git repos)
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
+ // Helper to get PID file path
15
+ const getPidFilePath = (root: string) => {
16
+ return path.resolve(root, ".openspec/dev_server.pid")
17
+ }
18
+
19
+ // Tool: sdd_free_port
20
+ export const free_port = tool({
21
+ description: "Busca y termina de manera forzada cualquier proceso que esté escuchando en un puerto específico",
22
+ args: {
23
+ port: tool.schema.number().describe("El puerto a liberar (ej. 3000 o 8000)")
24
+ },
25
+ async execute(args, context) {
26
+ const port = args.port
27
+ try {
28
+ const pid = execSync(`lsof -t -i :${port}`).toString().trim()
29
+ if (pid) {
30
+ execSync(`kill -9 ${pid.split('\n').join(' ')}`)
31
+ return JSON.stringify({
32
+ status: "SUCCESS",
33
+ message: `Puerto ${port} liberado. Procesos terminados: ${pid.split('\n').join(', ')}`
34
+ }, null, 2)
35
+ }
36
+ return JSON.stringify({
37
+ status: "SUCCESS",
38
+ message: `Puerto ${port} ya estaba libre (no se encontraron procesos)`
39
+ }, null, 2)
40
+ } catch (e) {
41
+ return JSON.stringify({
42
+ status: "SUCCESS",
43
+ message: `Puerto ${port} está libre`
44
+ }, null, 2)
45
+ }
46
+ }
47
+ })
48
+
49
+ // Tool: sdd_start_server
50
+ export const start_server = tool({
51
+ description: "Inicia un servidor de desarrollo o producción en segundo plano y registra su PID para limpieza posterior",
52
+ args: {
53
+ command: tool.schema.string().describe("Comando para iniciar el servidor (ej. 'yarn dev' o 'npm run dev')"),
54
+ port: tool.schema.number().optional().default(3000).describe("Puerto esperado del servidor (default: 3000)"),
55
+ cwd: tool.schema.string().optional().describe("Directorio de trabajo para ejecutar el comando")
56
+ },
57
+ async execute(args, context) {
58
+ const root = getRoot(context)
59
+ const targetCwd = args.cwd ? path.resolve(root, args.cwd) : root
60
+ const pidFile = getPidFilePath(root)
61
+
62
+ try {
63
+ const existingPid = execSync(`lsof -t -i :${args.port}`).toString().trim()
64
+ if (existingPid) {
65
+ execSync(`kill -9 ${existingPid.split('\n').join(' ')}`)
66
+ }
67
+ } catch (e) {
68
+ // ignore
69
+ }
70
+
71
+ const parts = args.command.split(" ")
72
+ const cmd = parts[0]
73
+ const cmdArgs = parts.slice(1)
74
+
75
+ const child = spawn(cmd, cmdArgs, {
76
+ cwd: targetCwd,
77
+ detached: true,
78
+ stdio: "ignore",
79
+ env: {
80
+ ...process.env,
81
+ PORT: String(args.port)
82
+ }
83
+ })
84
+
85
+ child.unref()
86
+
87
+ const pid = child.pid
88
+ if (pid) {
89
+ const dir = path.dirname(pidFile)
90
+ if (!fs.existsSync(dir)) {
91
+ fs.mkdirSync(dir, { recursive: true })
92
+ }
93
+ fs.writeFileSync(pidFile, String(pid), "utf8")
94
+
95
+ await new Promise(resolve => setTimeout(resolve, 3000))
96
+
97
+ return JSON.stringify({
98
+ status: "SUCCESS",
99
+ message: `Servidor iniciado con PID ${pid} ejecutando '${args.command}' en puerto ${args.port}`,
100
+ pid
101
+ }, null, 2)
102
+ }
103
+
104
+ return JSON.stringify({
105
+ status: "ERROR",
106
+ message: `No se pudo obtener el PID del proceso para el comando '${args.command}'`
107
+ }, null, 2)
108
+ }
109
+ })
110
+
111
+ // Tool: sdd_stop_server
112
+ export const stop_server = tool({
113
+ description: "Detiene el servidor en segundo plano usando el PID registrado",
114
+ args: {},
115
+ async execute(args, context) {
116
+ const root = getRoot(context)
117
+ const pidFile = getPidFilePath(root)
118
+
119
+ if (fs.existsSync(pidFile)) {
120
+ try {
121
+ const pidStr = fs.readFileSync(pidFile, "utf8").trim()
122
+ const pid = parseInt(pidStr, 10)
123
+ if (!isNaN(pid)) {
124
+ try {
125
+ process.kill(-pid, "SIGKILL")
126
+ } catch (err) {
127
+ try {
128
+ process.kill(pid, "SIGKILL")
129
+ } catch (err2) {
130
+ // ignore
131
+ }
132
+ }
133
+ fs.unlinkSync(pidFile)
134
+ return JSON.stringify({
135
+ status: "SUCCESS",
136
+ message: `Servidor con PID ${pid} terminado exitosamente`
137
+ }, null, 2)
138
+ }
139
+ } catch (e) {
140
+ return JSON.stringify({
141
+ status: "ERROR",
142
+ message: `Error al intentar detener el servidor: ${(e as Error).message}`
143
+ }, null, 2)
144
+ }
145
+ }
146
+
147
+ return JSON.stringify({
148
+ status: "SUCCESS",
149
+ message: "No hay ningún servidor registrado activo"
150
+ }, null, 2)
151
+ }
152
+ })
@@ -0,0 +1,362 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import fs from "fs"
3
+ import path from "path"
4
+ import { execSync } from "child_process"
5
+
6
+ // Helper to safely resolve root directory (avoiding OpenCode bug where worktree is '/' in non-git repos)
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
+ // Helper to parse semantic errors from compiler and linter outputs (reducing raw trace log bloat for the LLM)
15
+ const parseSemanticErrors = (rawOutput: string, type: "eslint" | "tsc"): any[] => {
16
+ const parsed: any[] = []
17
+ if (type === "eslint") {
18
+ const lines = rawOutput.split("\n")
19
+ for (const line of lines) {
20
+ const match = line.match(/^([^:]+):line\s+(\d+),\s+col\s+(\d+),\s+(.+)$/i) || line.match(/^([^\s]+):(\d+):(\d+):\s+(.+)$/)
21
+ if (match) {
22
+ parsed.push({
23
+ file: match[1].trim(),
24
+ line: parseInt(match[2], 10),
25
+ column: parseInt(match[3], 10),
26
+ error: match[4].trim()
27
+ })
28
+ } else if (line.includes("error") && line.trim().length > 0) {
29
+ parsed.push({ raw: line.trim() })
30
+ }
31
+ }
32
+ } else if (type === "tsc") {
33
+ const lines = rawOutput.split("\n")
34
+ for (const line of lines) {
35
+ const match = line.match(/^([^(]+)\((\d+),(\d+)\):\s+(error\s+TS\d+:\s+.+)$/)
36
+ if (match) {
37
+ parsed.push({
38
+ file: match[1].trim(),
39
+ line: parseInt(match[2], 10),
40
+ column: parseInt(match[3], 10),
41
+ error: match[4].trim()
42
+ })
43
+ } else if (line.includes("error TS") && line.trim().length > 0) {
44
+ parsed.push({ raw: line.trim() })
45
+ }
46
+ }
47
+ }
48
+ return parsed.length > 0 ? parsed : [{ raw: rawOutput.slice(0, 1500) }]
49
+ }
50
+
51
+ // Tool: sdd_quick_lint
52
+ export const quick_lint = tool({
53
+ description: "Ejecuta el linter del proyecto (eslint) restringido a src/. Devuelve exit code y warnings. Usado como gate automático antes de transicionar a F3.",
54
+ args: {},
55
+ async execute(args, context) {
56
+ const root = getRoot(context)
57
+
58
+ const pkgPath = path.resolve(root, "package.json")
59
+ if (!fs.existsSync(pkgPath)) {
60
+ return JSON.stringify({
61
+ status: "ERROR",
62
+ message: "No se encontró package.json. ¿Estás en un proyecto Next.js?",
63
+ }, null, 2)
64
+ }
65
+
66
+ let eslintFiles = "src/"
67
+ try {
68
+ const stateFile = path.resolve(root, ".openspec/sdd_state.json")
69
+ if (fs.existsSync(stateFile)) {
70
+ const state = JSON.parse(fs.readFileSync(stateFile, "utf8"))
71
+ if (state.activeContract) {
72
+ const contractPath = path.resolve(root, state.activeContract)
73
+ if (fs.existsSync(contractPath)) {
74
+ const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"))
75
+ if (contract && Array.isArray(contract.files_affected) && contract.files_affected.length > 0) {
76
+ const existingFiles = contract.files_affected.filter((f: string) => fs.existsSync(path.resolve(root, f)))
77
+ if (existingFiles.length > 0) {
78
+ eslintFiles = existingFiles.join(" ")
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+ } catch (e) {
85
+ // ignore
86
+ }
87
+
88
+ try {
89
+ const out = execSync(`npx eslint ${eslintFiles} --quiet --max-warnings 0 2>&1 || true`, {
90
+ cwd: root,
91
+ encoding: "utf8",
92
+ timeout: 120_000,
93
+ })
94
+ const hasErrors = out.toLowerCase().includes("error") || out.trim().length > 0
95
+ return JSON.stringify({
96
+ status: hasErrors ? "FAIL" : "SUCCESS",
97
+ message: hasErrors ? `Lint encontró errores en: ${eslintFiles}. Corrígelos antes de transicionar a F3.` : "Lint limpio.",
98
+ output: out.slice(0, 2000),
99
+ }, null, 2)
100
+ } catch (e: any) {
101
+ return JSON.stringify({
102
+ status: "FAIL",
103
+ message: `Lint falló: ${e.message?.slice(0, 500) || "unknown error"}`,
104
+ }, null, 2)
105
+ }
106
+ }
107
+ })
108
+
109
+ // Tool: sdd_shift_left_verify
110
+ export const shift_left_verify = tool({
111
+ description: "Ejecuta validaciones estáticas Shift-Left completas en el proyecto: ejecuta el compilador de TypeScript (tsc) y el linter (eslint) de manera combinada. Limpia y parsea semánticamente los stack traces y logs de error crudos, devolviendo un JSON limpio y estructurado de errores que el LLM puede digerir y solucionar de inmediato sin perder atención.",
112
+ args: {},
113
+ async execute(args, context) {
114
+ const root = getRoot(context)
115
+ const result: { tsc: { status: string, errors?: any[] }, eslint: { status: string, errors?: any[] } } = {
116
+ tsc: { status: "SUCCESS" },
117
+ eslint: { status: "SUCCESS" }
118
+ }
119
+
120
+ // 1. Run tsc --noEmit
121
+ try {
122
+ execSync("npx tsc --noEmit", { cwd: root, stdio: "pipe", timeout: 60000 })
123
+ } catch (e: any) {
124
+ const rawOutput = e.stdout?.toString() || e.stderr?.toString() || ""
125
+ const errors = parseSemanticErrors(rawOutput, "tsc")
126
+ result.tsc = {
127
+ status: "FAIL",
128
+ errors
129
+ }
130
+ }
131
+
132
+ // 2. Run eslint targeting modified files dynamically
133
+ let eslintFiles = "src/"
134
+ try {
135
+ const stateFile = path.resolve(root, ".openspec/sdd_state.json")
136
+ if (fs.existsSync(stateFile)) {
137
+ const state = JSON.parse(fs.readFileSync(stateFile, "utf8"))
138
+ if (state.activeContract) {
139
+ const contractPath = path.resolve(root, state.activeContract)
140
+ if (fs.existsSync(contractPath)) {
141
+ const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"))
142
+ if (contract && Array.isArray(contract.files_affected) && contract.files_affected.length > 0) {
143
+ const existingFiles = contract.files_affected.filter((f: string) => fs.existsSync(path.resolve(root, f)))
144
+ if (existingFiles.length > 0) {
145
+ eslintFiles = existingFiles.join(" ")
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ } catch (e) {
152
+ // ignore
153
+ }
154
+
155
+ try {
156
+ const out = execSync(`npx eslint ${eslintFiles} --quiet 2>&1 || true`, {
157
+ cwd: root,
158
+ encoding: "utf8",
159
+ timeout: 60000,
160
+ })
161
+ const isCircularError = out.toLowerCase().includes("converting circular structure")
162
+ if ((out.toLowerCase().includes("error") || out.trim().length > 0) && !isCircularError) {
163
+ const errors = parseSemanticErrors(out, "eslint")
164
+ result.eslint = {
165
+ status: "FAIL",
166
+ errors
167
+ }
168
+ }
169
+ } catch (e: any) {
170
+ result.eslint = {
171
+ status: "FAIL",
172
+ errors: [{ raw: e.message || "Eslint execution error" }]
173
+ }
174
+ }
175
+
176
+ const overallSuccess = result.tsc.status === "SUCCESS" && result.eslint.status === "SUCCESS"
177
+
178
+ return JSON.stringify({
179
+ status: overallSuccess ? "SUCCESS" : "FAIL",
180
+ message: overallSuccess ? "Shift-Left Verification exitosa: Cero errores de compilación y cero errores de lint." : "Validación fallida: Se encontraron errores estáticos.",
181
+ verification: result
182
+ }, null, 2)
183
+ }
184
+ })
185
+
186
+ // Tool: sdd_generate_tests
187
+ export const generate_tests = tool({
188
+ description: "Autogenera plantillas de pruebas unitarias/integración en tests/unit/ a partir de los escenarios de prueba descritos en el contrato activo de sdd_state.json. No pisa archivos de pruebas existentes.",
189
+ args: {},
190
+ async execute(args, context) {
191
+ const root = getRoot(context)
192
+ const stateFile = path.resolve(root, ".openspec/sdd_state.json")
193
+ if (!fs.existsSync(stateFile)) {
194
+ return JSON.stringify({ success: false, error: "sdd_state.json no existe. Inicia una sesión SDD primero." }, null, 2)
195
+ }
196
+ const state = JSON.parse(fs.readFileSync(stateFile, "utf8"))
197
+ const activeContract = state.activeContract
198
+ if (!activeContract) {
199
+ return JSON.stringify({ success: false, error: "El estado activo no tiene un activeContract. El orquestador debe fijar la sesión." }, null, 2)
200
+ }
201
+ const contractPath = path.resolve(root, activeContract)
202
+ if (!fs.existsSync(contractPath)) {
203
+ return JSON.stringify({ success: false, error: `El archivo del contrato '${contractPath}' no existe.` }, null, 2)
204
+ }
205
+ const contract = JSON.parse(fs.readFileSync(contractPath, "utf8"))
206
+ const test_scenarios = contract.test_scenarios || []
207
+ if (test_scenarios.length === 0) {
208
+ return JSON.stringify({ success: true, message: "No se encontraron test_scenarios en el contrato. No hay plantillas que generar." }, null, 2)
209
+ }
210
+
211
+ const grouped_tests: Record<string, any[]> = {}
212
+ for (const ts of test_scenarios) {
213
+ const feature = ts.feature_ref || "general"
214
+ if (!grouped_tests[feature]) grouped_tests[feature] = []
215
+ grouped_tests[feature].push(ts)
216
+ }
217
+
218
+ const created: string[] = []
219
+ const skipped: string[] = []
220
+
221
+ for (const [feature, scenarios] of Object.entries(grouped_tests)) {
222
+ const clean_feature = feature.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "")
223
+ const hasReact = scenarios.some(s => s.type === "unit" || s.type === "visual")
224
+ const ext = hasReact ? "tsx" : "ts"
225
+ const test_dir = path.resolve(root, "tests", "unit")
226
+ if (!fs.existsSync(test_dir)) {
227
+ fs.mkdirSync(test_dir, { recursive: true })
228
+ }
229
+ const test_file = path.join(test_dir, `${clean_feature}.test.${ext}`)
230
+
231
+ if (fs.existsSync(test_file)) {
232
+ skipped.push(`tests/unit/${clean_feature}.test.${ext}`)
233
+ continue
234
+ }
235
+
236
+ const lines: string[] = []
237
+ lines.push('import { describe, it, expect } from "vitest";')
238
+ if (hasReact) {
239
+ lines.push('import { render, screen } from "@testing-library/react";')
240
+ lines.push('import userEvent from "@testing-library/user-event";')
241
+ lines.push(`// import ${feature} from "@/components/blocks/${clean_feature}";`)
242
+ }
243
+ lines.push("")
244
+ lines.push(`describe("${feature} Tests (Contract Scenarios)", () => {`)
245
+
246
+ for (const s of scenarios) {
247
+ const tid = s.id || "TS-XX"
248
+ const name = s.name || "Test case"
249
+ lines.push(` // ${tid}: ${name}`)
250
+ lines.push(` // Given: ${s.given || ""}`)
251
+ lines.push(` // When: ${s.when || ""}`)
252
+ lines.push(` // Then: ${s.then || ""}`)
253
+ lines.push(` it("${tid}: ${name}", async () => {`)
254
+ lines.push(' // TODO: Implement actual contract assertions')
255
+ lines.push(' expect(true).toBe(true);')
256
+ lines.push(' });')
257
+ lines.push("")
258
+ }
259
+ lines.push("});")
260
+
261
+ fs.writeFileSync(test_file, lines.join("\n").trim() + "\n", "utf8")
262
+ created.push(`tests/unit/${clean_feature}.test.${ext}`)
263
+ }
264
+
265
+ return JSON.stringify({
266
+ status: "SUCCESS",
267
+ message: "Generación de plantillas de prueba completada.",
268
+ created,
269
+ skipped
270
+ }, null, 2)
271
+ }
272
+ })
273
+
274
+ // Tool: sdd_save_playwright_artifacts
275
+ export const save_playwright_artifacts = tool({
276
+ description: "Promueve y archiva los artefactos de captura visual/videos (.openspec/.playwright/) generados por Playwright a la carpeta del spec/contrato activo de forma ordenada, evitando ensuciar la raíz.",
277
+ args: {
278
+ callId: tool.schema.string().optional().describe("ID de llamada de Playwright MCP específica. Si no se pasa, toma la última ejecución de forma automática."),
279
+ move: tool.schema.boolean().default(false).describe("Si true, mueve los archivos en lugar de copiarlos.")
280
+ },
281
+ async execute(args, context) {
282
+ const root = getRoot(context)
283
+ const stateFile = path.resolve(root, ".openspec/sdd_state.json")
284
+ if (!fs.existsSync(stateFile)) {
285
+ return JSON.stringify({ success: false, error: "sdd_state.json no existe. Inicia una sesión SDD primero." }, null, 2)
286
+ }
287
+ const state = JSON.parse(fs.readFileSync(stateFile, "utf8"))
288
+ const activeContract = state.activeContract
289
+ if (!activeContract) {
290
+ return JSON.stringify({ success: false, error: "El estado activo no tiene un activeContract. El orquestador debe fijar la sesión." }, null, 2)
291
+ }
292
+ const contractPath = path.resolve(root, activeContract)
293
+ if (!fs.existsSync(contractPath)) {
294
+ return JSON.stringify({ success: false, error: `El archivo del contrato '${contractPath}' no existe.` }, null, 2)
295
+ }
296
+ const activeDir = path.dirname(contractPath)
297
+ const sourceBase = path.resolve(root, ".openspec/.playwright")
298
+ if (!fs.existsSync(sourceBase)) {
299
+ return JSON.stringify({ success: true, message: "No existen carpetas de Playwright para archivar en .openspec/.playwright." }, null, 2)
300
+ }
301
+
302
+ let callId = args.callId
303
+ if (!callId) {
304
+ const files = fs.readdirSync(sourceBase).filter(f => fs.statSync(path.join(sourceBase, f)).isDirectory())
305
+ if (files.length === 0) {
306
+ return JSON.stringify({ success: true, message: "No hay carpetas de artefactos de Playwright disponibles en .openspec/.playwright" }, null, 2)
307
+ }
308
+ // sort by mtime descending to get the newest
309
+ files.sort((a, b) => {
310
+ return fs.statSync(path.join(sourceBase, b)).mtimeMs - fs.statSync(path.join(sourceBase, a)).mtimeMs
311
+ })
312
+ callId = files[0]
313
+ }
314
+
315
+ const sourceDir = path.join(sourceBase, callId)
316
+ if (!fs.existsSync(sourceDir)) {
317
+ return JSON.stringify({ success: false, error: `La ruta de origen '${sourceDir}' no existe.` }, null, 2)
318
+ }
319
+
320
+ const destDir = path.join(activeDir, "playwright", callId)
321
+ if (!fs.existsSync(destDir)) {
322
+ fs.mkdirSync(destDir, { recursive: true })
323
+ }
324
+
325
+ const copyRecursiveSync = (src: string, dest: string, moveMode: boolean) => {
326
+ const exists = fs.existsSync(src)
327
+ const stats = exists ? fs.statSync(src) : null
328
+ const isDirectory = stats ? stats.isDirectory() : false
329
+ if (isDirectory) {
330
+ if (!fs.existsSync(dest)) {
331
+ fs.mkdirSync(dest, { recursive: true })
332
+ }
333
+ fs.readdirSync(src).forEach((childItemName) => {
334
+ copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName), moveMode)
335
+ })
336
+ if (moveMode) {
337
+ fs.rmdirSync(src)
338
+ }
339
+ } else {
340
+ fs.copyFileSync(src, dest)
341
+ if (moveMode) {
342
+ fs.unlinkSync(src)
343
+ }
344
+ }
345
+ }
346
+
347
+ try {
348
+ copyRecursiveSync(sourceDir, destDir, args.move)
349
+ return JSON.stringify({
350
+ status: "SUCCESS",
351
+ message: `Artefactos archivados con éxito en la sesión activa.`,
352
+ callId,
353
+ destination: destDir
354
+ }, null, 2)
355
+ } catch (e: any) {
356
+ return JSON.stringify({
357
+ status: "ERROR",
358
+ error: e.message
359
+ }, null, 2)
360
+ }
361
+ }
362
+ })
package/opencode.json CHANGED
@@ -1,6 +1,5 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
- "theme": "tokyonight",
4
3
  "permission": {
5
4
  "*": "allow",
6
5
  "skill": {
@@ -92,112 +91,5 @@
92
91
  "enabled": true,
93
92
  "purpose": "Permite buscar, filtrar y obtener ejemplos de uso JSX de más de 1,500 iconos de Lucide React de forma exacta mediante herramientas del MCP."
94
93
  }
95
- },
96
- "agent": {
97
- "sdd-orchestrator": {
98
- "description": "Coordinador principal del flujo SDD y del stack cerrado",
99
- "mode": "primary",
100
- "model": "deepseek/deepseek-v4-flash",
101
- "prompt": "{file:./.opencode/rules/sdd-global.md}\n\n{file:./.opencode/agents/sdd-orchestrator.md}",
102
- "permission": {
103
- "*": "allow",
104
- "write": {
105
- ".openspec/*": "allow",
106
- ".openspec/**": "allow",
107
- ".opencode/*": "allow",
108
- ".opencode/**": "allow",
109
- "*": "deny"
110
- },
111
- "edit": "deny",
112
- "bash": "deny",
113
- "task": {
114
- "*": "deny",
115
- "sdd-*": "allow"
116
- }
117
- }
118
- },
119
- "sdd-spec-writer": {
120
- "description": "Redacta y valida contratos OpenAPI e interfaces en specs/",
121
- "mode": "subagent",
122
- "hidden": true,
123
- "model": "deepseek/deepseek-v4-flash",
124
- "prompt": "{file:./.opencode/rules/sdd-global.md}\n\n{file:./.opencode/agents/sdd-spec-writer.md}",
125
- "permission": {
126
- "*": "allow",
127
- "bash": "deny"
128
- }
129
- },
130
- "sdd-coder": {
131
- "description": "Implementa código ajustándose estrictamente a los contratos en specs/",
132
- "mode": "subagent",
133
- "hidden": true,
134
- "steps": 10,
135
- "model": "deepseek/deepseek-v4-flash",
136
- "temperature": 0.35,
137
- "frequency_penalty": 0.5,
138
- "presence_penalty": 0.2,
139
- "prompt": "{file:./.opencode/rules/sdd-global.md}\n\n{file:./.opencode/agents/sdd-coder.md}",
140
- "permission": {
141
- "*": "allow",
142
- "bash": {
143
- "*": "ask",
144
- "npm *": "allow",
145
- "pnpm *": "allow",
146
- "yarn *": "allow",
147
- "vitest *": "allow",
148
- "npx eslint *": "allow",
149
- "eslint *": "allow",
150
- "pytest *": "allow",
151
- "python3 *": "allow",
152
- "python *": "allow",
153
- "uv *": "allow",
154
- "pip *": "allow",
155
- "npx *": "allow",
156
- "npx shadcn*": "allow",
157
- "cat *": "allow"
158
- }
159
- }
160
- },
161
- "sdd-tester": {
162
- "description": "Diseña y corre pruebas unitarias, integración y visuales (Playwright)",
163
- "mode": "subagent",
164
- "hidden": true,
165
- "steps": 15,
166
- "model": "deepseek/deepseek-v4-flash",
167
- "temperature": 0.35,
168
- "frequency_penalty": 0.5,
169
- "presence_penalty": 0.2,
170
- "prompt": "{file:./.opencode/rules/sdd-global.md}\n\n{file:./.opencode/agents/sdd-tester.md}",
171
- "permission": {
172
- "*": "allow",
173
- "bash": {
174
- "*": "ask",
175
- "npm *": "allow",
176
- "pnpm *": "allow",
177
- "yarn *": "allow",
178
- "vitest *": "allow",
179
- "npx eslint *": "allow",
180
- "eslint *": "allow",
181
- "pytest *": "allow",
182
- "python3 *": "allow",
183
- "python *": "allow",
184
- "uv *": "allow",
185
- "pip *": "allow",
186
- "npx *": "allow",
187
- "npx shadcn*": "allow",
188
- "cat *": "allow"
189
- }
190
- }
191
- },
192
- "sdd-deployer": {
193
- "description": "Limpia Docker y realiza el despliegue local del sistema",
194
- "mode": "subagent",
195
- "hidden": true,
196
- "model": "deepseek/deepseek-v4-flash",
197
- "prompt": "{file:./.opencode/agents/sdd-deployer.md}",
198
- "permission": {
199
- "*": "allow"
200
- }
201
- }
202
94
  }
203
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zugzbot",
3
- "version": "1.0.18",
3
+ "version": "1.0.20",
4
4
  "description": "Fácil instalador del arnés SDD de Zugzbot para proyectos OpenCode",
5
5
  "type": "module",
6
6
  "bin": {
package/tui.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/tui.json",
3
+ "theme": "tokyonight",
3
4
  "plugin": [
4
5
  "./.opencode/plugins/plugin_tui.tsx"
5
- ]
6
+ ],
7
+ "keybinds": {
8
+ "leader": "ctrl+x",
9
+ "sidebar_toggle": "leader,b",
10
+ "theme_list": "leader,t",
11
+ "session_list": "leader,l",
12
+ "editor_open": "leader,e"
13
+ }
6
14
  }