refacil-sdd-ai 4.2.4 → 4.4.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.
Files changed (47) hide show
  1. package/README.md +239 -214
  2. package/agents/auditor.md +189 -184
  3. package/agents/debugger.md +201 -204
  4. package/agents/implementer.md +150 -149
  5. package/agents/investigator.md +80 -89
  6. package/agents/proposer.md +219 -124
  7. package/agents/tester.md +140 -144
  8. package/agents/validator.md +153 -145
  9. package/bin/cli.js +158 -116
  10. package/lib/bus/askFulfillment.js +17 -17
  11. package/lib/bus/broker.js +599 -599
  12. package/lib/bus/ui/app.js +318 -318
  13. package/lib/commands/sdd.js +447 -0
  14. package/lib/hooks.js +236 -236
  15. package/lib/installer.js +58 -2
  16. package/lib/methodology-migration-pending.js +101 -136
  17. package/package.json +4 -6
  18. package/skills/apply/SKILL.md +139 -120
  19. package/skills/archive/SKILL.md +105 -107
  20. package/skills/ask/SKILL.md +78 -78
  21. package/skills/attend/SKILL.md +70 -70
  22. package/skills/bug/SKILL.md +121 -128
  23. package/skills/explore/SKILL.md +73 -63
  24. package/skills/guide/SKILL.md +79 -79
  25. package/skills/inbox/SKILL.md +43 -43
  26. package/skills/join/SKILL.md +82 -82
  27. package/skills/prereqs/BUS-CROSS-REPO.md +55 -55
  28. package/skills/prereqs/METHODOLOGY-CONTRACT.md +122 -115
  29. package/skills/prereqs/SKILL.md +30 -37
  30. package/skills/propose/SKILL.md +103 -102
  31. package/skills/reply/SKILL.md +44 -44
  32. package/skills/review/SKILL.md +163 -126
  33. package/skills/review/checklist-back.md +92 -92
  34. package/skills/review/checklist-front.md +72 -72
  35. package/skills/review/checklist.md +114 -114
  36. package/skills/say/SKILL.md +38 -38
  37. package/skills/setup/SKILL.md +85 -141
  38. package/skills/setup/troubleshooting.md +38 -35
  39. package/skills/test/SKILL.md +104 -94
  40. package/skills/test/testing-patterns.md +63 -63
  41. package/skills/up-code/SKILL.md +108 -108
  42. package/skills/update/SKILL.md +109 -132
  43. package/skills/verify/SKILL.md +159 -132
  44. package/templates/compact-guidance.md +45 -45
  45. package/templates/methodology-guide.md +46 -42
  46. package/config/openspec-config.yaml +0 -8
  47. package/skills/prereqs/OPENSPEC-DELTAS.md +0 -51
@@ -0,0 +1,447 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function findProjectRoot() {
7
+ let dir = process.cwd();
8
+ const { root } = path.parse(dir);
9
+ while (dir !== root) {
10
+ if (fs.existsSync(path.join(dir, 'refacil-sdd')) || fs.existsSync(path.join(dir, '.git'))) {
11
+ return dir;
12
+ }
13
+ const parent = path.dirname(dir);
14
+ if (parent === dir) break;
15
+ dir = parent;
16
+ }
17
+ return process.cwd();
18
+ }
19
+
20
+ const projectRoot = findProjectRoot();
21
+
22
+ // --- Helpers ---
23
+
24
+ function parseArgs(argv) {
25
+ const args = { _positional: [] };
26
+ for (let i = 0; i < argv.length; i++) {
27
+ const token = argv[i];
28
+ if (!token) continue;
29
+ if (!token.startsWith('--')) {
30
+ args._positional.push(token);
31
+ continue;
32
+ }
33
+ const key = token.slice(2);
34
+ const next = argv[i + 1];
35
+ if (next === undefined || next.startsWith('--')) {
36
+ args[key] = true;
37
+ } else {
38
+ args[key] = next;
39
+ i++;
40
+ }
41
+ }
42
+ return args;
43
+ }
44
+
45
+ function validateChangeName(name) {
46
+ if (!name || name.trim() === '') {
47
+ return { valid: false, reason: 'El nombre del cambio no puede estar vacío.' };
48
+ }
49
+ if (/[A-Z]/.test(name[0])) {
50
+ return { valid: false, reason: 'El nombre no puede empezar con mayúscula. Usa solo minúsculas, números y guiones.' };
51
+ }
52
+ if (/^[0-9]/.test(name)) {
53
+ return { valid: false, reason: 'El nombre no puede empezar con un número. Debe comenzar con una letra minúscula.' };
54
+ }
55
+ if (name.includes('_')) {
56
+ return { valid: false, reason: 'El nombre no puede contener guiones bajos (_). Usa guiones medios (-).' };
57
+ }
58
+ if (name.includes('/') || name.includes('.')) {
59
+ return { valid: false, reason: 'El nombre no puede contener / ni . (puntos). Usa solo letras, números y guiones medios.' };
60
+ }
61
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) {
62
+ return { valid: false, reason: 'El nombre solo puede contener letras minúsculas, números y guiones medios, y debe comenzar con una letra minúscula.' };
63
+ }
64
+ return { valid: true };
65
+ }
66
+
67
+ function autoMigrateOpenspec(root) {
68
+ const oldDir = path.join(root, 'openspec');
69
+ const newDir = path.join(root, 'refacil-sdd');
70
+ const oldExists = fs.existsSync(oldDir);
71
+ const newExists = fs.existsSync(newDir);
72
+ if (oldExists && !newExists) {
73
+ try {
74
+ fs.renameSync(oldDir, newDir);
75
+ } catch (err) {
76
+ throw new Error(`No se pudo migrar openspec/ a refacil-sdd/: ${err.message}`);
77
+ }
78
+ }
79
+ // Si ambos existen o ninguno existe → no hacer nada
80
+ }
81
+
82
+ // --- Subcomandos ---
83
+
84
+ function cmdValidateName(argv) {
85
+ const args = parseArgs(argv);
86
+ const name = args._positional[0];
87
+ const result = validateChangeName(name);
88
+ if (result.valid) {
89
+ process.exit(0);
90
+ } else {
91
+ console.log(result.reason);
92
+ process.exit(1);
93
+ }
94
+ }
95
+
96
+ function cmdNewChange(argv) {
97
+ const args = parseArgs(argv);
98
+ const name = args._positional[0];
99
+
100
+ const validation = validateChangeName(name);
101
+ if (!validation.valid) {
102
+ console.error(validation.reason);
103
+ process.exit(1);
104
+ }
105
+
106
+ autoMigrateOpenspec(projectRoot);
107
+
108
+ const changeDir = path.join(projectRoot, 'refacil-sdd', 'changes', name);
109
+ if (fs.existsSync(changeDir)) {
110
+ console.error(`Ya existe un cambio con el nombre '${name}' en refacil-sdd/changes/${name}/`);
111
+ process.exit(1);
112
+ }
113
+
114
+ fs.mkdirSync(changeDir, { recursive: true });
115
+
116
+ const artifacts = ['proposal', 'design', 'tasks', 'specs'];
117
+ for (const artifact of artifacts) {
118
+ fs.writeFileSync(path.join(changeDir, `${artifact}.md`), `# ${artifact}: ${name}\n`);
119
+ }
120
+
121
+ console.log(`Cambio '${name}' creado en refacil-sdd/changes/${name}/`);
122
+ }
123
+
124
+ function cmdArchive(argv) {
125
+ const args = parseArgs(argv);
126
+ const name = args._positional[0];
127
+
128
+ const validation = validateChangeName(name);
129
+ if (!validation.valid) {
130
+ console.error(validation.reason);
131
+ process.exit(1);
132
+ }
133
+
134
+ autoMigrateOpenspec(projectRoot);
135
+
136
+ const sourceDir = path.join(projectRoot, 'refacil-sdd', 'changes', name);
137
+ if (!fs.existsSync(sourceDir)) {
138
+ console.error(`No existe el cambio '${name}' en refacil-sdd/changes/${name}/`);
139
+ process.exit(1);
140
+ }
141
+
142
+ const date = new Date().toISOString().slice(0, 10);
143
+ const archiveDir = path.join(projectRoot, 'refacil-sdd', 'changes', 'archive');
144
+ const destDir = path.join(archiveDir, `${date}-${name}`);
145
+
146
+ if (fs.existsSync(destDir)) {
147
+ console.error(`Ya existe un archivo con ese nombre: refacil-sdd/changes/archive/${date}-${name}/`);
148
+ process.exit(1);
149
+ }
150
+
151
+ fs.mkdirSync(archiveDir, { recursive: true });
152
+ fs.renameSync(sourceDir, destDir);
153
+
154
+ if (!(!fs.existsSync(sourceDir) && fs.existsSync(destDir))) {
155
+ console.error('Error: la operación de archivado no se completó correctamente.');
156
+ process.exit(1);
157
+ }
158
+
159
+ console.log(`Cambio '${name}' archivado en refacil-sdd/changes/archive/${date}-${name}/`);
160
+ }
161
+
162
+ function cmdList(argv) {
163
+ const args = parseArgs(argv);
164
+ const wantJson = args.json === true;
165
+
166
+ autoMigrateOpenspec(projectRoot);
167
+
168
+ const changesDir = path.join(projectRoot, 'refacil-sdd', 'changes');
169
+ if (!fs.existsSync(changesDir)) {
170
+ if (wantJson) {
171
+ process.stdout.write(JSON.stringify([]) + '\n');
172
+ } else {
173
+ console.log('Sin cambios activos.');
174
+ }
175
+ return;
176
+ }
177
+
178
+ const entries = fs.readdirSync(changesDir, { withFileTypes: true })
179
+ .filter((e) => e.isDirectory() && e.name !== 'archive');
180
+
181
+ const result = entries.map((e) => {
182
+ const reviewPassed = fs.existsSync(path.join(changesDir, e.name, '.review-passed'));
183
+ return { name: e.name, reviewPassed };
184
+ });
185
+
186
+ if (wantJson) {
187
+ process.stdout.write(JSON.stringify(result) + '\n');
188
+ } else {
189
+ if (result.length === 0) {
190
+ console.log('Sin cambios activos.');
191
+ return;
192
+ }
193
+ console.log('Cambios activos en refacil-sdd/changes/:');
194
+ for (const item of result) {
195
+ const badge = item.reviewPassed ? '[reviewed]' : '[pending-review]';
196
+ console.log(` ${item.name} ${badge}`);
197
+ }
198
+ }
199
+ }
200
+
201
+ function cmdStatus(argv) {
202
+ const args = parseArgs(argv);
203
+ const name = args._positional[0];
204
+ const wantJson = args.json === true;
205
+
206
+ if (!name) {
207
+ console.error('Uso: refacil-sdd-ai sdd status <nombre-cambio> [--json]');
208
+ process.exit(1);
209
+ }
210
+
211
+ autoMigrateOpenspec(projectRoot);
212
+
213
+ const changeDir = path.join(projectRoot, 'refacil-sdd', 'changes', name);
214
+ if (!fs.existsSync(changeDir)) {
215
+ console.error(`No existe el cambio '${name}' en refacil-sdd/changes/${name}/`);
216
+ process.exit(1);
217
+ }
218
+
219
+ // Verificar artefactos
220
+ const hasProposal = fs.existsSync(path.join(changeDir, 'proposal.md'));
221
+ const hasDesign = fs.existsSync(path.join(changeDir, 'design.md'));
222
+ const hasTasks = fs.existsSync(path.join(changeDir, 'tasks.md'));
223
+
224
+ // specs: specs.md existe OR specs/ dir con al menos un .md
225
+ let hasSpecs = false;
226
+ const specsMd = path.join(changeDir, 'specs.md');
227
+ const specsDir = path.join(changeDir, 'specs');
228
+ if (fs.existsSync(specsMd)) {
229
+ hasSpecs = true;
230
+ } else if (fs.existsSync(specsDir) && fs.statSync(specsDir).isDirectory()) {
231
+ const mdFiles = fs.readdirSync(specsDir).filter((f) => f.endsWith('.md'));
232
+ hasSpecs = mdFiles.length > 0;
233
+ }
234
+
235
+ const artifacts = {
236
+ proposal: hasProposal,
237
+ design: hasDesign,
238
+ tasks: hasTasks,
239
+ specs: hasSpecs,
240
+ };
241
+
242
+ // Parsear tasks.md
243
+ let taskStats = { total: 0, done: 0, pending: 0 };
244
+ if (hasTasks) {
245
+ const tasksContent = fs.readFileSync(path.join(changeDir, 'tasks.md'), 'utf8');
246
+ const matches = [...tasksContent.matchAll(/^- \[([ x])\]/gm)];
247
+ taskStats.total = matches.length;
248
+ taskStats.done = matches.filter((m) => m[1] === 'x').length;
249
+ taskStats.pending = taskStats.total - taskStats.done;
250
+ }
251
+
252
+ const reviewPassed = fs.existsSync(path.join(changeDir, '.review-passed'));
253
+
254
+ const ready = {
255
+ forApply: artifacts.proposal && artifacts.tasks,
256
+ forArchive: reviewPassed && taskStats.total > 0 && taskStats.pending === 0,
257
+ };
258
+
259
+ const status = {
260
+ name,
261
+ artifacts,
262
+ tasks: taskStats,
263
+ reviewPassed,
264
+ ready,
265
+ };
266
+
267
+ if (wantJson) {
268
+ process.stdout.write(JSON.stringify(status) + '\n');
269
+ } else {
270
+ console.log(`Estado del cambio '${name}':`);
271
+ console.log('');
272
+ console.log(' Artefactos:');
273
+ for (const [key, val] of Object.entries(artifacts)) {
274
+ console.log(` ${key.padEnd(10)} ${val ? 'OK' : 'FALTANTE'}`);
275
+ }
276
+ console.log('');
277
+ console.log(' Tasks:');
278
+ console.log(` total: ${taskStats.total}`);
279
+ console.log(` hechas: ${taskStats.done}`);
280
+ console.log(` pendientes: ${taskStats.pending}`);
281
+ console.log('');
282
+ console.log(` Review aprobado: ${reviewPassed ? 'Si' : 'No'}`);
283
+ console.log('');
284
+ console.log(' Listo para:');
285
+ console.log(` apply: ${ready.forApply ? 'Si' : 'No'}`);
286
+ console.log(` archive: ${ready.forArchive ? 'Si' : 'No'}`);
287
+ }
288
+ }
289
+
290
+ function cmdMarkReviewed(argv) {
291
+ const args = parseArgs(argv);
292
+ const name = args._positional[0];
293
+
294
+ if (!name) {
295
+ console.error('Uso: refacil-sdd-ai sdd mark-reviewed <nombre-cambio> --verdict <verdict> --summary "<resumen>" [--fail-count N] [--preexisting-count N] [--blockers]');
296
+ process.exit(1);
297
+ }
298
+
299
+ if (!args.verdict) {
300
+ console.error('Error: --verdict es requerido (ej: approved, approved-with-notes, rejected)');
301
+ process.exit(1);
302
+ }
303
+
304
+ if (!args.summary) {
305
+ console.error('Error: --summary es requerido');
306
+ process.exit(1);
307
+ }
308
+
309
+ autoMigrateOpenspec(projectRoot);
310
+
311
+ const changeDir = path.join(projectRoot, 'refacil-sdd', 'changes', name);
312
+ if (!fs.existsSync(changeDir)) {
313
+ console.error(`No existe el cambio '${name}' en refacil-sdd/changes/${name}/`);
314
+ process.exit(1);
315
+ }
316
+
317
+ const payload = {
318
+ verdict: args.verdict,
319
+ changeName: name,
320
+ summary: args.summary,
321
+ failCount: Number(args['fail-count'] || 0),
322
+ preexistingCount: Number(args['preexisting-count'] || 0),
323
+ blockers: args.blockers === true,
324
+ date: new Date().toISOString(),
325
+ };
326
+
327
+ fs.writeFileSync(path.join(changeDir, '.review-passed'), JSON.stringify(payload, null, 2));
328
+ console.log(`Review marcado como aprobado para '${name}' (verdict: ${payload.verdict})`);
329
+ }
330
+
331
+ function cmdTasksUpdate(argv) {
332
+ const args = parseArgs(argv);
333
+ const name = args._positional[0];
334
+
335
+ if (!name) {
336
+ console.error('Uso: refacil-sdd-ai sdd tasks-update <nombre-cambio> --task N --done');
337
+ process.exit(1);
338
+ }
339
+
340
+ const taskN = Number(args.task);
341
+ if (!args.task || !Number.isInteger(taskN) || taskN <= 0) {
342
+ console.error('Error: --task debe ser un entero positivo (ej: --task 1)');
343
+ process.exit(1);
344
+ }
345
+
346
+ if (args.done !== true) {
347
+ console.error('Error: --done es requerido para marcar una task como completada');
348
+ process.exit(1);
349
+ }
350
+
351
+ autoMigrateOpenspec(projectRoot);
352
+
353
+ const tasksFile = path.join(projectRoot, 'refacil-sdd', 'changes', name, 'tasks.md');
354
+ if (!fs.existsSync(tasksFile)) {
355
+ console.error(`No existe tasks.md para el cambio '${name}'`);
356
+ process.exit(1);
357
+ }
358
+
359
+ const content = fs.readFileSync(tasksFile, 'utf8');
360
+ const matches = [...content.matchAll(/^(- \[[ x]\].*)/gm)];
361
+
362
+ if (taskN > matches.length) {
363
+ console.error(`Error: task ${taskN} no encontrada (el archivo tiene ${matches.length} task(s))`);
364
+ process.exit(1);
365
+ }
366
+
367
+ const targetMatch = matches[taskN - 1];
368
+ const originalLine = targetMatch[1];
369
+ // Reemplazar [ ] por [x] (idempotente: [x] queda [x])
370
+ const updatedLine = originalLine.replace(/^- \[ \]/, '- [x]');
371
+
372
+ // Reemplazar solo la primera ocurrencia exacta de la línea original (en la posición correcta)
373
+ let replaced = false;
374
+ let count = 0;
375
+ const newContent = content.replace(/^(- \[[ x]\].*)/gm, (match) => {
376
+ count++;
377
+ if (count === taskN && !replaced) {
378
+ replaced = true;
379
+ return updatedLine;
380
+ }
381
+ return match;
382
+ });
383
+
384
+ fs.writeFileSync(tasksFile, newContent, 'utf8');
385
+ console.log(`Task ${taskN} de '${name}' marcada como completada.`);
386
+ }
387
+
388
+ function sddHelp() {
389
+ console.log(`
390
+ refacil-sdd-ai sdd — Gestión de artefactos SDD-AI
391
+
392
+ Subcomandos:
393
+ sdd new-change <nombre> Crea un nuevo cambio con los 4 artefactos scaffold
394
+ sdd archive <nombre> Archiva un cambio completado a refacil-sdd/changes/archive/
395
+ sdd list [--json] Lista cambios activos con estado de review
396
+ sdd status <nombre> [--json] Muestra estado de artefactos y tasks de un cambio
397
+ sdd mark-reviewed <nombre> Escribe .review-passed con veredicto y resumen
398
+ --verdict <v> Veredicto (ej: approved, approved-with-notes, rejected)
399
+ --summary "<texto>" Resumen del review (requerido)
400
+ [--fail-count N] Número de fallos encontrados
401
+ [--preexisting-count N] Número de issues preexistentes
402
+ [--blockers] Indica si hay blockers
403
+ sdd tasks-update <nombre> Marca una task como completada en tasks.md
404
+ --task N Número de task (1-indexed)
405
+ --done Confirma que la task está hecha
406
+ sdd validate-name <nombre> Valida el formato del nombre de un cambio
407
+
408
+ Notas:
409
+ - Los nombres de cambio deben empezar con minúscula y usar solo [a-z0-9-]
410
+ - Si existe openspec/ y no existe refacil-sdd/, se migra automáticamente
411
+ `);
412
+ }
413
+
414
+ // --- Dispatcher ---
415
+
416
+ function handleSdd(sub, argv) {
417
+ const args = argv || [];
418
+
419
+ switch (sub) {
420
+ case 'new-change':
421
+ cmdNewChange(args);
422
+ break;
423
+ case 'archive':
424
+ cmdArchive(args);
425
+ break;
426
+ case 'list':
427
+ cmdList(args);
428
+ break;
429
+ case 'status':
430
+ cmdStatus(args);
431
+ break;
432
+ case 'mark-reviewed':
433
+ cmdMarkReviewed(args);
434
+ break;
435
+ case 'tasks-update':
436
+ cmdTasksUpdate(args);
437
+ break;
438
+ case 'validate-name':
439
+ cmdValidateName(args);
440
+ break;
441
+ default:
442
+ sddHelp();
443
+ process.exit(1);
444
+ }
445
+ }
446
+
447
+ module.exports = { handleSdd, parseArgs, autoMigrateOpenspec, validateChangeName };