zugzbot 1.1.6 → 1.2.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.
- package/.opencode/agents/sdd-orchestrator.md +24 -3
- package/.opencode/scripts/lint-charts.js +194 -0
- package/.opencode/skills/sdd-quickstart/SKILL.md +35 -0
- package/.opencode/skills/shadcn-templates/SKILL.md +45 -11
- package/.opencode/tools/akash_catalog.ts +596 -0
- package/.opencode/tools/sdd_network.ts +206 -41
- package/.opencode/tools/sdd_testing.ts +129 -4
- package/.utils/export_opencode_session.py +1 -1
- package/opencode.json +9 -1
- package/package.json +2 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin"
|
|
2
2
|
import fs from "fs"
|
|
3
3
|
import path from "path"
|
|
4
|
+
import crypto from "crypto"
|
|
4
5
|
import { execSync, spawn } from "child_process"
|
|
5
6
|
|
|
6
7
|
// Helper to safely resolve root directory (avoiding OpenCode bug where worktree is '/' in non-git repos)
|
|
@@ -20,7 +21,7 @@ const getTargetDir = (root: string): string => {
|
|
|
20
21
|
if (data && data.targetDir) return data.targetDir
|
|
21
22
|
}
|
|
22
23
|
} catch (e) {}
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
try {
|
|
25
26
|
const statePath = path.resolve(root, ".openspec/sdd_state.json")
|
|
26
27
|
if (fs.existsSync(statePath)) {
|
|
@@ -32,9 +33,68 @@ const getTargetDir = (root: string): string => {
|
|
|
32
33
|
return "."
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
//
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Unix-style PID/PGID storage in /tmp (bugfix sesion 1176: servidor caia al
|
|
38
|
+
// cerrar subagente; ademas chocaba entre proyectos zugzbot simultaneos).
|
|
39
|
+
//
|
|
40
|
+
// Esquema:
|
|
41
|
+
// /tmp/zugzbot-dev-server-<hash8> -> PID (referencia principal)
|
|
42
|
+
// /tmp/zugzbot-dev-server-<hash8>.pgid -> PGID (para kill -SIGTERM -PGID)
|
|
43
|
+
//
|
|
44
|
+
// hash8 = primeros 8 chars de md5(root) para evitar colisiones entre
|
|
45
|
+
// distintos proyectos zugzbot abiertos en paralelo.
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
const projectHash = (root: string): string => {
|
|
49
|
+
return crypto.createHash("md5").update(root).digest("hex").slice(0, 8)
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
const getPidFilePath = (root: string) => {
|
|
37
|
-
return
|
|
53
|
+
return `/tmp/zugzbot-dev-server-${projectHash(root)}`
|
|
54
|
+
}
|
|
55
|
+
const getPgidFilePath = (root: string) => {
|
|
56
|
+
return `/tmp/zugzbot-dev-server-${projectHash(root)}.pgid`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const readPidFiles = (root: string): { pid: number | null; pgid: number | null } => {
|
|
60
|
+
let pid: number | null = null
|
|
61
|
+
let pgid: number | null = null
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(getPidFilePath(root))) {
|
|
64
|
+
pid = parseInt(fs.readFileSync(getPidFilePath(root), "utf8").trim(), 10)
|
|
65
|
+
if (!Number.isFinite(pid)) pid = null
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {}
|
|
68
|
+
try {
|
|
69
|
+
if (fs.existsSync(getPgidFilePath(root))) {
|
|
70
|
+
pgid = parseInt(fs.readFileSync(getPgidFilePath(root), "utf8").trim(), 10)
|
|
71
|
+
if (!Number.isFinite(pgid)) pgid = null
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {}
|
|
74
|
+
return { pid, pgid }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const clearPidFiles = (root: string) => {
|
|
78
|
+
try { fs.unlinkSync(getPidFilePath(root)) } catch (e) {}
|
|
79
|
+
try { fs.unlinkSync(getPgidFilePath(root)) } catch (e) {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const killProcessGroup = (pgid: number, signal: NodeJS.Signals = "SIGTERM"): boolean => {
|
|
83
|
+
try {
|
|
84
|
+
process.kill(-pgid, signal)
|
|
85
|
+
return true
|
|
86
|
+
} catch (e) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const isPidAlive = (pid: number): boolean => {
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid, 0)
|
|
94
|
+
return true
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
38
98
|
}
|
|
39
99
|
|
|
40
100
|
// Tool: sdd_free_port
|
|
@@ -68,8 +128,17 @@ export const free_port = tool({
|
|
|
68
128
|
})
|
|
69
129
|
|
|
70
130
|
// Tool: sdd_start_server
|
|
131
|
+
//
|
|
132
|
+
// Bugfix sesion 1176: el dev server caia al cerrar el subagente coder porque
|
|
133
|
+
// el proceso hijo hereda el process group del opencode padre y moría con él.
|
|
134
|
+
// Solucion: envolver el spawn con `setsid` (disponible en macOS/Linux) para
|
|
135
|
+
// crear un nuevo session leader completamente independiente, y guardar tanto
|
|
136
|
+
// el PID como el PGID en /tmp para poder matar todo el grupo limpiamente.
|
|
71
137
|
export const start_server = tool({
|
|
72
|
-
description:
|
|
138
|
+
description:
|
|
139
|
+
"Inicia un servidor en segundo plano y registra su PID/PGID en /tmp. " +
|
|
140
|
+
"El proceso se desacopla del opencode padre con `setsid`, de modo que " +
|
|
141
|
+
"sobrevive al cierre del subagente que lo lanzó (bugfix sesion 1176).",
|
|
73
142
|
args: {
|
|
74
143
|
command: tool.schema.string().describe("Comando para iniciar el servidor (ej. 'yarn dev' o 'npm run dev')"),
|
|
75
144
|
port: tool.schema.number().optional().default(3000).describe("Puerto esperado del servidor (default: 3000)"),
|
|
@@ -80,21 +149,45 @@ export const start_server = tool({
|
|
|
80
149
|
const targetDir = getTargetDir(root)
|
|
81
150
|
const targetCwd = args.cwd ? path.resolve(root, args.cwd) : (targetDir === "." ? root : path.resolve(root, targetDir))
|
|
82
151
|
const pidFile = getPidFilePath(root)
|
|
152
|
+
const pgidFile = getPgidFilePath(root)
|
|
83
153
|
|
|
154
|
+
// Liberar puerto si esta ocupado (limpieza previa)
|
|
84
155
|
try {
|
|
85
156
|
const existingPid = execSync(`lsof -t -i :${args.port}`).toString().trim()
|
|
86
157
|
if (existingPid) {
|
|
87
158
|
execSync(`kill -9 ${existingPid.split('\n').join(' ')}`)
|
|
88
159
|
}
|
|
89
160
|
} catch (e) {
|
|
90
|
-
// ignore
|
|
161
|
+
// ignore: puerto libre
|
|
91
162
|
}
|
|
92
163
|
|
|
164
|
+
// Si hay un PID previo registrado, intentar matarlo limpiamente primero
|
|
165
|
+
const prev = readPidFiles(root)
|
|
166
|
+
if (prev.pgid && isPidAlive(prev.pgid)) {
|
|
167
|
+
killProcessGroup(prev.pgid, "SIGTERM")
|
|
168
|
+
await new Promise(r => setTimeout(r, 500))
|
|
169
|
+
if (isPidAlive(prev.pgid)) {
|
|
170
|
+
killProcessGroup(prev.pgid, "SIGKILL")
|
|
171
|
+
}
|
|
172
|
+
} else if (prev.pid && isPidAlive(prev.pid)) {
|
|
173
|
+
try { process.kill(prev.pid, "SIGKILL") } catch (e) {}
|
|
174
|
+
}
|
|
175
|
+
clearPidFiles(root)
|
|
176
|
+
|
|
93
177
|
const parts = args.command.split(" ")
|
|
94
178
|
const cmd = parts[0]
|
|
95
179
|
const cmdArgs = parts.slice(1)
|
|
96
180
|
|
|
97
|
-
|
|
181
|
+
// Usar setsid para crear un nuevo session leader. Asi, cuando el subagente
|
|
182
|
+
// coder termine, el dev server no es killed por SIGTERM en cascada.
|
|
183
|
+
// setsid existe en macOS (coreutils) y Linux por defecto.
|
|
184
|
+
const useSetsid = process.platform !== "win32"
|
|
185
|
+
|
|
186
|
+
const spawnArgs = useSetsid
|
|
187
|
+
? { cmd: "setsid", cmdArgs: [cmd, ...cmdArgs] }
|
|
188
|
+
: { cmd, cmdArgs }
|
|
189
|
+
|
|
190
|
+
const child = spawn(spawnArgs.cmd, spawnArgs.cmdArgs, {
|
|
98
191
|
cwd: targetCwd,
|
|
99
192
|
detached: true,
|
|
100
193
|
stdio: "ignore",
|
|
@@ -104,22 +197,37 @@ export const start_server = tool({
|
|
|
104
197
|
}
|
|
105
198
|
})
|
|
106
199
|
|
|
107
|
-
child.unref()
|
|
200
|
+
// CRITICO: NO usar child.unref() — necesitamos mantener la referencia
|
|
201
|
+
// para que el proceso no sea reaped inmediatamente y para tener el PGID
|
|
202
|
+
// disponible. Sin embargo, el spawn fue `detached: true` con setsid, asi
|
|
203
|
+
// que el proceso ya esta en su propio session group.
|
|
204
|
+
//
|
|
205
|
+
// El handler exit solo borra los PID files si el proceso muere solo.
|
|
206
|
+
child.on('exit', () => {
|
|
207
|
+
clearPidFiles(root)
|
|
208
|
+
})
|
|
209
|
+
child.on('error', () => {
|
|
210
|
+
clearPidFiles(root)
|
|
211
|
+
})
|
|
108
212
|
|
|
109
213
|
const pid = child.pid
|
|
110
214
|
if (pid) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
fs.mkdirSync(dir, { recursive: true })
|
|
114
|
-
}
|
|
215
|
+
// En setsid, el PID del proceso spawned es el PGID del nuevo grupo
|
|
216
|
+
const pgid = pid
|
|
115
217
|
fs.writeFileSync(pidFile, String(pid), "utf8")
|
|
218
|
+
fs.writeFileSync(pgidFile, String(pgid), "utf8")
|
|
116
219
|
|
|
220
|
+
// Esperar a que arranque
|
|
117
221
|
await new Promise(resolve => setTimeout(resolve, 3000))
|
|
118
222
|
|
|
119
223
|
return JSON.stringify({
|
|
120
224
|
status: "SUCCESS",
|
|
121
|
-
message: `Servidor iniciado con PID ${pid} ejecutando '${args.command}' en puerto ${args.port}`,
|
|
122
|
-
pid
|
|
225
|
+
message: `Servidor iniciado con PID ${pid} (PGID ${pgid}) ejecutando '${args.command}' en puerto ${args.port}. PID file: ${pidFile}`,
|
|
226
|
+
pid,
|
|
227
|
+
pgid,
|
|
228
|
+
pid_file: pidFile,
|
|
229
|
+
pgid_file: pgidFile,
|
|
230
|
+
detached_via: useSetsid ? "setsid" : "none"
|
|
123
231
|
}, null, 2)
|
|
124
232
|
}
|
|
125
233
|
|
|
@@ -131,44 +239,101 @@ export const start_server = tool({
|
|
|
131
239
|
})
|
|
132
240
|
|
|
133
241
|
// Tool: sdd_stop_server
|
|
242
|
+
//
|
|
243
|
+
// Mata el grupo de procesos completo (SIGTERM graceful, luego SIGKILL si
|
|
244
|
+
// no responde en 2s). Esto libera el puerto automaticamente.
|
|
134
245
|
export const stop_server = tool({
|
|
135
|
-
description:
|
|
246
|
+
description:
|
|
247
|
+
"Detiene el servidor en segundo plano registrado. Mata el process group " +
|
|
248
|
+
"completo (SIGTERM, fallback SIGKILL tras 2s) usando el PGID guardado en /tmp.",
|
|
136
249
|
args: {},
|
|
137
250
|
async execute(args, context) {
|
|
138
251
|
const root = getRoot(context)
|
|
139
|
-
const
|
|
252
|
+
const { pid, pgid } = readPidFiles(root)
|
|
253
|
+
|
|
254
|
+
if (!pid && !pgid) {
|
|
255
|
+
return JSON.stringify({
|
|
256
|
+
status: "SUCCESS",
|
|
257
|
+
message: "No hay ningún servidor registrado activo"
|
|
258
|
+
}, null, 2)
|
|
259
|
+
}
|
|
140
260
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return JSON.stringify({
|
|
157
|
-
status: "SUCCESS",
|
|
158
|
-
message: `Servidor con PID ${pid} terminado exitosamente`
|
|
159
|
-
}, null, 2)
|
|
261
|
+
let killed = false
|
|
262
|
+
let signal_used: string | null = null
|
|
263
|
+
|
|
264
|
+
if (pgid && isPidAlive(pgid)) {
|
|
265
|
+
// SIGTERM graceful primero
|
|
266
|
+
if (killProcessGroup(pgid, "SIGTERM")) {
|
|
267
|
+
signal_used = "SIGTERM"
|
|
268
|
+
killed = true
|
|
269
|
+
}
|
|
270
|
+
// Esperar hasta 2s
|
|
271
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
272
|
+
// Si sigue vivo, SIGKILL
|
|
273
|
+
if (isPidAlive(pgid)) {
|
|
274
|
+
if (killProcessGroup(pgid, "SIGKILL")) {
|
|
275
|
+
signal_used = signal_used ? `${signal_used}+SIGKILL` : "SIGKILL"
|
|
160
276
|
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
277
|
+
}
|
|
278
|
+
} else if (pid && isPidAlive(pid)) {
|
|
279
|
+
// Fallback: matar solo el PID
|
|
280
|
+
try { process.kill(pid, "SIGTERM"); signal_used = "SIGTERM"; killed = true } catch (e) {}
|
|
281
|
+
await new Promise(r => setTimeout(r, 2000))
|
|
282
|
+
if (isPidAlive(pid)) {
|
|
283
|
+
try { process.kill(pid, "SIGKILL"); signal_used = "SIGKILL" } catch (e) {}
|
|
166
284
|
}
|
|
167
285
|
}
|
|
168
286
|
|
|
287
|
+
clearPidFiles(root)
|
|
288
|
+
|
|
169
289
|
return JSON.stringify({
|
|
170
|
-
status: "SUCCESS",
|
|
171
|
-
message:
|
|
290
|
+
status: killed ? "SUCCESS" : "WARNING",
|
|
291
|
+
message: killed
|
|
292
|
+
? `Servidor (PID ${pid}, PGID ${pgid}) terminado via ${signal_used}`
|
|
293
|
+
: `PID/PGID ${pid}/${pgid} no estaba vivo (limpieza de archivos)`,
|
|
294
|
+
pid,
|
|
295
|
+
pgid,
|
|
296
|
+
signal_used
|
|
172
297
|
}, null, 2)
|
|
173
298
|
}
|
|
174
299
|
})
|
|
300
|
+
|
|
301
|
+
// Tool: sdd_server_status (utilidad: NUEVO)
|
|
302
|
+
//
|
|
303
|
+
// Inspecciona si el dev server sigue vivo leyendo /tmp.
|
|
304
|
+
export const server_status = tool({
|
|
305
|
+
description:
|
|
306
|
+
"Verifica si el dev server registrado sigue vivo. Lee los archivos PID/PGID " +
|
|
307
|
+
"de /tmp y consulta el puerto objetivo.",
|
|
308
|
+
args: {
|
|
309
|
+
port: tool.schema.number().optional().default(3000).describe("Puerto esperado del servidor (default: 3000)")
|
|
310
|
+
},
|
|
311
|
+
async execute(args, context) {
|
|
312
|
+
const root = getRoot(context)
|
|
313
|
+
const { pid, pgid } = readPidFiles(root)
|
|
314
|
+
const pidAlive = pid ? isPidAlive(pid) : false
|
|
315
|
+
const pgidAlive = pgid ? isPidAlive(pgid) : false
|
|
316
|
+
|
|
317
|
+
let portListening = false
|
|
318
|
+
try {
|
|
319
|
+
const out = execSync(`lsof -i :${args.port} -sTCP:LISTEN -t 2>/dev/null`).toString().trim()
|
|
320
|
+
portListening = !!out
|
|
321
|
+
} catch (e) {
|
|
322
|
+
portListening = false
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return JSON.stringify({
|
|
326
|
+
status: portListening ? "RUNNING" : "STOPPED",
|
|
327
|
+
message: portListening
|
|
328
|
+
? `Dev server respondiendo en puerto ${args.port}`
|
|
329
|
+
: `No hay dev server activo en puerto ${args.port}`,
|
|
330
|
+
pid_file: getPidFilePath(root),
|
|
331
|
+
pgid_file: getPgidFilePath(root),
|
|
332
|
+
pid,
|
|
333
|
+
pgid,
|
|
334
|
+
pid_alive: pidAlive,
|
|
335
|
+
pgid_alive: pgidAlive,
|
|
336
|
+
port_listening: portListening
|
|
337
|
+
}, null, 2)
|
|
338
|
+
}
|
|
339
|
+
})
|
|
@@ -248,9 +248,14 @@ export const shift_left_verify = tool({
|
|
|
248
248
|
}, null, 2)
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
const result: {
|
|
251
|
+
const result: {
|
|
252
|
+
tsc: { status: string, errors?: any[] }
|
|
253
|
+
eslint: { status: string, errors?: any[] }
|
|
254
|
+
lintCharts: { status: string, violations?: any[], summary?: string }
|
|
255
|
+
} = {
|
|
252
256
|
tsc: { status: "SUCCESS" },
|
|
253
|
-
eslint: { status: "SUCCESS" }
|
|
257
|
+
eslint: { status: "SUCCESS" },
|
|
258
|
+
lintCharts: { status: "SUCCESS" }
|
|
254
259
|
}
|
|
255
260
|
|
|
256
261
|
// 1. Run tsc --noEmit
|
|
@@ -265,6 +270,59 @@ export const shift_left_verify = tool({
|
|
|
265
270
|
}
|
|
266
271
|
}
|
|
267
272
|
|
|
273
|
+
// 1.5 Run lint-charts (bugfix sesion 1176: detectar hsl(var(--chart-N)) en
|
|
274
|
+
// chartConfig que es CSS invalido cuando --chart-N esta definido en OKLCH).
|
|
275
|
+
// BLOQUEANTE: cualquier violacion hace fallar shift-left.
|
|
276
|
+
try {
|
|
277
|
+
const linterScript = path.resolve(root, ".opencode/scripts/lint-charts.js")
|
|
278
|
+
if (fs.existsSync(linterScript)) {
|
|
279
|
+
const lintOut = execSync(`node "${linterScript}" --json`, {
|
|
280
|
+
cwd: targetPath,
|
|
281
|
+
encoding: "utf8",
|
|
282
|
+
timeout: 30000
|
|
283
|
+
})
|
|
284
|
+
try {
|
|
285
|
+
const parsed = JSON.parse(lintOut)
|
|
286
|
+
if (parsed.status === "FAIL") {
|
|
287
|
+
result.lintCharts = {
|
|
288
|
+
status: "FAIL",
|
|
289
|
+
violations: parsed.violations || [],
|
|
290
|
+
summary: parsed.summary || "violaciones detectadas"
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
result.lintCharts = {
|
|
294
|
+
status: "SUCCESS",
|
|
295
|
+
summary: parsed.summary || "ok"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (parseErr) {
|
|
299
|
+
result.lintCharts = {
|
|
300
|
+
status: "WARN",
|
|
301
|
+
summary: "no se pudo parsear salida del linter: " + lintOut.slice(0, 200)
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
} catch (e: any) {
|
|
306
|
+
// execSync lanza si exit code != 0 (linter encontro violaciones)
|
|
307
|
+
const rawOutput = e.stdout?.toString() || e.stderr?.toString() || ""
|
|
308
|
+
try {
|
|
309
|
+
const parsed = JSON.parse(rawOutput)
|
|
310
|
+
if (parsed.status === "FAIL") {
|
|
311
|
+
result.lintCharts = {
|
|
312
|
+
status: "FAIL",
|
|
313
|
+
violations: parsed.violations || [],
|
|
314
|
+
summary: parsed.summary || "violaciones detectadas"
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} catch (parseErr) {
|
|
318
|
+
result.lintCharts = {
|
|
319
|
+
status: "FAIL",
|
|
320
|
+
violations: [{ raw: rawOutput.slice(0, 1000) }],
|
|
321
|
+
summary: "linter ejecuto con codigo no-cero y salida no parseable"
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
268
326
|
// 2. Run eslint targeting modified files dynamically
|
|
269
327
|
let eslintFiles = "src/"
|
|
270
328
|
try {
|
|
@@ -311,16 +369,83 @@ export const shift_left_verify = tool({
|
|
|
311
369
|
}
|
|
312
370
|
}
|
|
313
371
|
|
|
314
|
-
const overallSuccess =
|
|
372
|
+
const overallSuccess =
|
|
373
|
+
result.tsc.status === "SUCCESS" &&
|
|
374
|
+
result.eslint.status === "SUCCESS" &&
|
|
375
|
+
result.lintCharts.status === "SUCCESS"
|
|
315
376
|
|
|
316
377
|
return JSON.stringify({
|
|
317
378
|
status: overallSuccess ? "SUCCESS" : "FAIL",
|
|
318
|
-
message: overallSuccess
|
|
379
|
+
message: overallSuccess
|
|
380
|
+
? "Shift-Left Verification exitosa: Cero errores de compilación, lint y lint-charts."
|
|
381
|
+
: "Validación fallida: Se encontraron errores estáticos.",
|
|
319
382
|
verification: result
|
|
320
383
|
}, null, 2)
|
|
321
384
|
}
|
|
322
385
|
})
|
|
323
386
|
|
|
387
|
+
// Tool: sdd_lint_charts
|
|
388
|
+
//
|
|
389
|
+
// Linter BLOQUEANTE independiente para invocación directa (debugging o
|
|
390
|
+
// pre-commit). Detecta uso incorrecto de `hsl(var(--chart-N))` cuando
|
|
391
|
+
// `--chart-N` esta definido en OKLCH en globals.css (sesion 1176).
|
|
392
|
+
export const lint_charts = tool({
|
|
393
|
+
description:
|
|
394
|
+
"Ejecuta el linter BLOQUEANTE de chartConfigs Recharts. Detecta " +
|
|
395
|
+
"'hsl(var(--chart-N))' y patrones similares que producen CSS invalido " +
|
|
396
|
+
"cuando --chart-N esta definido en OKLCH. Usar desde F3 antes de " +
|
|
397
|
+
"transicionar a F4 o como pre-commit hook.",
|
|
398
|
+
args: {
|
|
399
|
+
json: tool.schema
|
|
400
|
+
.boolean()
|
|
401
|
+
.optional()
|
|
402
|
+
.default(false)
|
|
403
|
+
.describe("Si true, devuelve la salida en formato JSON estructurado."),
|
|
404
|
+
paths: tool.schema
|
|
405
|
+
.array(tool.schema.string())
|
|
406
|
+
.optional()
|
|
407
|
+
.describe("Paths adicionales a escanear (default: src/ y app/).")
|
|
408
|
+
},
|
|
409
|
+
async execute(args, context) {
|
|
410
|
+
const root = getRoot(context)
|
|
411
|
+
const linterScript = path.resolve(root, ".opencode/scripts/lint-charts.js")
|
|
412
|
+
if (!fs.existsSync(linterScript)) {
|
|
413
|
+
return JSON.stringify({
|
|
414
|
+
status: "ERROR",
|
|
415
|
+
message: `Linter no encontrado en ${linterScript}. Verifica que .opencode/scripts/lint-charts.js existe.`
|
|
416
|
+
}, null, 2)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const targetDir = getTargetDir(root)
|
|
420
|
+
const targetPath = targetDir === "." ? root : path.resolve(root, targetDir)
|
|
421
|
+
|
|
422
|
+
const extraArgs: string[] = []
|
|
423
|
+
if (args.json) extraArgs.push("--json")
|
|
424
|
+
if (args.paths && args.paths.length > 0) extraArgs.push(...args.paths)
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const out = execSync(`node "${linterScript}" ${extraArgs.join(" ")}`, {
|
|
428
|
+
cwd: targetPath,
|
|
429
|
+
encoding: "utf8",
|
|
430
|
+
timeout: 30000
|
|
431
|
+
})
|
|
432
|
+
return JSON.stringify({
|
|
433
|
+
status: "PASS",
|
|
434
|
+
message: out.trim() || "lint-charts PASS",
|
|
435
|
+
raw_output: out
|
|
436
|
+
}, null, 2)
|
|
437
|
+
} catch (e: any) {
|
|
438
|
+
const out = e.stdout?.toString() || e.stderr?.toString() || ""
|
|
439
|
+
return JSON.stringify({
|
|
440
|
+
status: "FAIL",
|
|
441
|
+
message: "lint-charts detecto violaciones.",
|
|
442
|
+
raw_output: out,
|
|
443
|
+
exit_code: e.status
|
|
444
|
+
}, null, 2)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
|
|
324
449
|
// Tool: sdd_generate_tests
|
|
325
450
|
export const generate_tests = tool({
|
|
326
451
|
description: "Autogenera plantillas de pruebas a partir de los escenarios del contrato activo.",
|
|
@@ -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 = "
|
|
12
|
+
TARGET_SESSION_ID = "ses_1174"
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def format_timestamp(ts):
|
package/opencode.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://opencode.ai/config.json",
|
|
3
3
|
"agent": {
|
|
4
|
+
"sdd-orchestrator": {
|
|
5
|
+
"permission": {
|
|
6
|
+
"edit": "deny",
|
|
7
|
+
"write": "deny",
|
|
8
|
+
"bash": "deny"
|
|
9
|
+
},
|
|
10
|
+
"model": "deepseek/deepseek-v4-flash"
|
|
11
|
+
},
|
|
4
12
|
"sdd-spec-writer": {
|
|
5
13
|
"mode": "subagent",
|
|
6
14
|
"permission": {
|
|
7
|
-
"webfetch": "
|
|
15
|
+
"webfetch": "allow"
|
|
8
16
|
},
|
|
9
17
|
"model": "deepseek/deepseek-v4-flash"
|
|
10
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zugzbot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Fácil instalador del arnés SDD de Zugzbot para proyectos OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
".opencode/skills/",
|
|
19
19
|
".opencode/templates/",
|
|
20
20
|
".opencode/tools/",
|
|
21
|
+
".opencode/scripts/",
|
|
21
22
|
".opencode/contract-schema.json",
|
|
22
23
|
".opencode/oh-my-design/design-md/",
|
|
23
24
|
".opencode/oh-my-design/packages/mcp/dist/",
|