zugzbot 1.0.4 → 1.0.6

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.
@@ -52,7 +52,10 @@ const discardLastFailedAttempt = (projectRoot: string): { success: boolean; mess
52
52
  }
53
53
 
54
54
  export const LoopEnforcerPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
55
- const projectRoot = worktree || directory || process.cwd()
55
+ let projectRoot = process.cwd();
56
+ if (directory && directory !== "/") projectRoot = directory;
57
+ else if (worktree && worktree !== "/") projectRoot = worktree;
58
+
56
59
  const stateFilePath = path.resolve(projectRoot, ".openspec/sdd_state.json")
57
60
 
58
61
  const getStateFilePath = () => stateFilePath
@@ -33,7 +33,10 @@ const emptyMetrics = (): SessionMetrics => ({
33
33
  })
34
34
 
35
35
  export const SddBridgePlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
36
- const projectRoot = worktree || directory || process.cwd()
36
+ let projectRoot = process.cwd();
37
+ if (directory && directory !== "/") projectRoot = directory;
38
+ else if (worktree && worktree !== "/") projectRoot = worktree;
39
+
37
40
  const stateFilePath = path.resolve(projectRoot, ".openspec/sdd_state.json")
38
41
  const metricsFilePath = path.resolve(projectRoot, ".openspec/.sdd_session_metrics.json")
39
42
 
@@ -2,8 +2,15 @@ import { tool } from "@opencode-ai/plugin"
2
2
  import fs from "fs"
3
3
  import path from "path"
4
4
 
5
+ const getRoot = (context: any) => {
6
+ if (context?.directory && context.directory !== "/") return context.directory;
7
+ if (context?.worktree && context.worktree !== "/") return context.worktree;
8
+ if (context?.cwd && context.cwd !== "/") return context.cwd;
9
+ return process.cwd();
10
+ };
11
+
5
12
  const getBrainFilePath = (context: any) => {
6
- const root = context?.worktree || context?.directory || process.cwd()
13
+ const root = getRoot(context);
7
14
  const openspecDir = path.resolve(root, ".openspec")
8
15
  if (!fs.existsSync(openspecDir)) {
9
16
  fs.mkdirSync(openspecDir, { recursive: true })
@@ -3,6 +3,14 @@ import fs from "fs"
3
3
  import path from "path"
4
4
  import { execSync, spawn } from "child_process"
5
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
+
6
14
 
7
15
  // Helper to parse semantic errors from compiler and linter outputs (reducing raw trace log bloat for the LLM)
8
16
  const parseSemanticErrors = (rawOutput: string, type: "eslint" | "tsc"): any[] => {
@@ -43,7 +51,7 @@ const parseSemanticErrors = (rawOutput: string, type: "eslint" | "tsc"): any[] =
43
51
 
44
52
  // Helper to resolve state path
45
53
  const getStateFilePath = (context: any) => {
46
- const root = context.worktree || context.directory || process.cwd()
54
+ const root = getRoot(context)
47
55
  return path.resolve(root, ".openspec/sdd_state.json")
48
56
  }
49
57
 
@@ -285,7 +293,7 @@ export const set_phase = tool({
285
293
  loopCurrentIteration: tool.schema.number().optional().describe("Número de la iteración autónoma actual (empieza en 1)."),
286
294
  },
287
295
  async execute(args, context) {
288
- const root = context.worktree || context.directory || process.cwd()
296
+ const root = getRoot(context)
289
297
  const filePath = getStateFilePath(context)
290
298
  const currentState = readState(filePath)
291
299
 
@@ -472,7 +480,7 @@ export const get_initial_session_data = tool({
472
480
  description: "Obtiene atómicamente todos los datos de inicio de sesión: el estado actual del arnés, las memorias históricas clave del Brain ('learnings', 'design', 'routing'), y la lista curada de recomendaciones de diseño de Oh My Design. Reemplaza las llamadas secuenciales a sdd_get_state, brain_read_memory y sdd_list_design_recommendations.",
473
481
  args: {},
474
482
  async execute(args, context) {
475
- const root = context.worktree || context.directory || process.cwd()
483
+ const root = getRoot(context)
476
484
  const statePath = getStateFilePath(context)
477
485
  const currentState = readState(statePath)
478
486
 
@@ -509,7 +517,7 @@ export const save_active_brief = tool({
509
517
  brief: tool.schema.string().describe("Contenido en formato Markdown con el resumen del spec activo, componentes, diseño y dependencias.")
510
518
  },
511
519
  async execute(args, context) {
512
- const root = context?.worktree || context?.directory || process.cwd()
520
+ const root = getRoot(context)
513
521
  const openspecDir = path.resolve(root, ".openspec")
514
522
  if (!fs.existsSync(openspecDir)) {
515
523
  fs.mkdirSync(openspecDir, { recursive: true })
@@ -531,7 +539,7 @@ export const create_spec_folder = tool({
531
539
  name: tool.schema.string().describe("Nombre del cambio en minúsculas y separado por guiones (ej. sumar-endpoint)")
532
540
  },
533
541
  async execute(args, context) {
534
- const root = context.worktree || context.directory || process.cwd()
542
+ const root = getRoot(context)
535
543
  const specsDir = path.resolve(root, ".openspec/specs")
536
544
 
537
545
  if (!fs.existsSync(specsDir)) {
@@ -635,7 +643,7 @@ export const start_server = tool({
635
643
  cwd: tool.schema.string().optional().describe("Directorio de trabajo para ejecutar el comando")
636
644
  },
637
645
  async execute(args, context) {
638
- const root = context.worktree || context.directory || process.cwd()
646
+ const root = getRoot(context)
639
647
  const targetCwd = args.cwd ? path.resolve(root, args.cwd) : root
640
648
  const pidFile = getPidFilePath(root)
641
649
 
@@ -693,7 +701,7 @@ export const stop_server = tool({
693
701
  description: "Detiene el servidor en segundo plano usando el PID registrado",
694
702
  args: {},
695
703
  async execute(args, context) {
696
- const root = context.worktree || context.directory || process.cwd()
704
+ const root = getRoot(context)
697
705
  const pidFile = getPidFilePath(root)
698
706
 
699
707
  if (fs.existsSync(pidFile)) {
@@ -738,7 +746,7 @@ export const select_design = tool({
738
746
  brandId: tool.schema.string().describe("ID exacto del diseño en oh-my-design (ej: 'linear.app', 'vercel', 'stripe')")
739
747
  },
740
748
  async execute(args, context) {
741
- const root = context.worktree || context.directory || process.cwd()
749
+ const root = getRoot(context)
742
750
  const brandId = args.brandId.trim()
743
751
  const sourceDir = path.resolve(root, ".opencode/oh-my-design/design-md", brandId)
744
752
  const targetDir = path.resolve(root, ".openspec")
@@ -767,7 +775,7 @@ export const select_design = tool({
767
775
 
768
776
  // Helper extracted to allow substring-match recursion without `this.execute` typing issues.
769
777
  async function selectDesignHelper(brandId: string, context: any) {
770
- const root = context.worktree || context.directory || process.cwd()
778
+ const root = getRoot(context)
771
779
  const sourceDir = path.resolve(root, ".opencode/oh-my-design/design-md", brandId)
772
780
  const targetDir = path.resolve(root, ".openspec")
773
781
 
@@ -907,7 +915,7 @@ export const apply_brand_tokens = tool({
907
915
  tokens: tool.schema.string().describe("JSON stringificado con {colors: {...}, typography: {...}, radius: {...}} extraído de contract.json design.tokens"),
908
916
  },
909
917
  async execute(args, context) {
910
- const root = context.worktree || context.directory || process.cwd()
918
+ const root = getRoot(context)
911
919
  const globalsPath = path.resolve(root, "src/app/globals.css")
912
920
 
913
921
  if (!fs.existsSync(globalsPath)) {
@@ -981,7 +989,7 @@ export const generate_dockerfile = tool({
981
989
  port: tool.schema.number().default(3000).describe("Puerto de la aplicación"),
982
990
  },
983
991
  async execute(args, context) {
984
- const root = context.worktree || context.directory || process.cwd()
992
+ const root = getRoot(context)
985
993
 
986
994
  if (args.stack === "nextjs") {
987
995
  // Detect package manager
@@ -1157,7 +1165,7 @@ export const quick_lint = tool({
1157
1165
  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.",
1158
1166
  args: {},
1159
1167
  async execute(args, context) {
1160
- const root = context.worktree || context.directory || process.cwd()
1168
+ const root = getRoot(context)
1161
1169
 
1162
1170
  // Detect package manager scripts
1163
1171
  const pkgPath = path.resolve(root, "package.json")
@@ -1195,7 +1203,7 @@ export const shift_left_verify = tool({
1195
1203
  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.",
1196
1204
  args: {},
1197
1205
  async execute(args, context) {
1198
- const root = context.worktree || context.directory || process.cwd()
1206
+ const root = getRoot(context)
1199
1207
  const result: { tsc: { status: string, errors?: any[] }, eslint: { status: string, errors?: any[] } } = {
1200
1208
  tsc: { status: "SUCCESS" },
1201
1209
  eslint: { status: "SUCCESS" }
@@ -1346,7 +1354,7 @@ export const bootstrap_status = tool({
1346
1354
  description: "Reporta el estado de bootstrap del proyecto (qué plantilla se usó, cuándo, qué componentes shadcn están instalados, versión de Node y package manager). Si el proyecto no está bootstrapped, retorna NOT_BOOTSTRAPPED. Útil para que el coder verifique antes de empezar a codear.",
1347
1355
  args: {},
1348
1356
  async execute(args, context) {
1349
- const root = context.worktree || context.directory || process.cwd()
1357
+ const root = getRoot(context)
1350
1358
  const status = readBootstrapStatus(root)
1351
1359
 
1352
1360
  if (!status) {
@@ -1396,7 +1404,7 @@ export const bootstrap_nextjs_shadcn = tool({
1396
1404
  force: tool.schema.boolean().default(false).describe("Si true, sobrescribe archivos existentes. Si false, los salta (mergea package.json)."),
1397
1405
  },
1398
1406
  async execute(args, context) {
1399
- const root = context.worktree || context.directory || process.cwd()
1407
+ const root = getRoot(context)
1400
1408
  const start = Date.now()
1401
1409
  const templateDir = path.resolve(root, ".opencode/templates/nextjs-shadcn")
1402
1410
 
@@ -1619,7 +1627,7 @@ export const bootstrap_fastapi = tool({
1619
1627
  force: tool.schema.boolean().default(false).describe("Si true, sobrescribe archivos existentes. Si false, los salta."),
1620
1628
  },
1621
1629
  async execute(args, context) {
1622
- const root = context.worktree || context.directory || process.cwd()
1630
+ const root = getRoot(context)
1623
1631
  const start = Date.now()
1624
1632
  const templateDir = path.resolve(root, ".opencode/templates/fastapi-sdd")
1625
1633
 
@@ -1816,7 +1824,7 @@ export const validate_lucide_icons_batch = tool({
1816
1824
  icons: tool.schema.array(tool.schema.string()).describe("Lista de nombres de iconos a validar (ej: ['Sun', 'Moon', 'Plus'])"),
1817
1825
  },
1818
1826
  async execute(args, context) {
1819
- const root = context.worktree || context.directory || process.cwd()
1827
+ const root = getRoot(context)
1820
1828
  const results: Record<string, { valid: boolean; source: string }> = {}
1821
1829
 
1822
1830
  // Lista de iconos comunes como fallback
@@ -1887,7 +1895,7 @@ export const generate_tests = tool({
1887
1895
  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.",
1888
1896
  args: {},
1889
1897
  async execute(args, context) {
1890
- const root = context.worktree || context.directory || process.cwd()
1898
+ const root = getRoot(context)
1891
1899
  const stateFile = path.resolve(root, ".openspec/sdd_state.json")
1892
1900
  if (!fs.existsSync(stateFile)) {
1893
1901
  return JSON.stringify({ success: false, error: "sdd_state.json no existe. Inicia una sesión SDD primero." }, null, 2)
@@ -1977,7 +1985,7 @@ export const save_playwright_artifacts = tool({
1977
1985
  move: tool.schema.boolean().default(false).describe("Si true, mueve los archivos en lugar de copiarlos.")
1978
1986
  },
1979
1987
  async execute(args, context) {
1980
- const root = context.worktree || context.directory || process.cwd()
1988
+ const root = getRoot(context)
1981
1989
  const stateFile = path.resolve(root, ".openspec/sdd_state.json")
1982
1990
  if (!fs.existsSync(stateFile)) {
1983
1991
  return JSON.stringify({ success: false, error: "sdd_state.json no existe. Inicia una sesión SDD primero." }, null, 2)
package/bin/init.js CHANGED
@@ -68,6 +68,23 @@ for (const item of itemsToCopy) {
68
68
  console.error(`${red}❌ Error copying ${item.name}: ${error.message}${reset}`);
69
69
  }
70
70
  }
71
+ // Ensure .openspec directory and active-brief.md exist on target
72
+ try {
73
+ const openspecDir = join(targetDir, '.openspec');
74
+ if (!fs.existsSync(openspecDir)) {
75
+ fs.mkdirSync(openspecDir, { recursive: true });
76
+ }
77
+ const activeBriefPath = join(openspecDir, 'active-brief.md');
78
+ if (!fs.existsSync(activeBriefPath)) {
79
+ fs.writeFileSync(
80
+ activeBriefPath,
81
+ "# SDD Active Brief\n\nNo hay ninguna sesión activa o el spec actual no ha sido iniciado.\n",
82
+ "utf8"
83
+ );
84
+ }
85
+ } catch (error) {
86
+ console.error(`${red}❌ Error creating .openspec directory: ${error.message}${reset}`);
87
+ }
71
88
 
72
89
  if (copiedCount > 0) {
73
90
  console.log(`\n${bold}${green}✨ Zugzbot Harness successfully installed/updated!${reset}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zugzbot",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "Fácil instalador del arnés SDD de Zugzbot para proyectos OpenCode",
5
5
  "type": "module",
6
6
  "bin": {