refacil-sdd-ai 4.0.9 → 4.0.11

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/README.md CHANGED
@@ -64,6 +64,7 @@ npm uninstall -g refacil-sdd-ai
64
64
  |---|---|
65
65
  | `refacil-sdd-ai init` | Instala skills y hooks en el repo actual |
66
66
  | `refacil-sdd-ai update` | Re-copia skills y hooks a la ultima version |
67
+ | `refacil-sdd-ai migration-pending [--json]` | Misma deteccion que hooks/`notify-update`; exit 1 si hay migracion de metodologia pendiente (uso en `/refacil:update`) |
67
68
  | `refacil-sdd-ai clean` | Elimina skills y hooks SDD-AI del repo |
68
69
  | `refacil-sdd-ai help` | Ver ayuda |
69
70
 
@@ -72,7 +73,7 @@ npm uninstall -g refacil-sdd-ai
72
73
  | Comando | Descripcion |
73
74
  |---|---|
74
75
  | `refacil-sdd-ai check-update` | (`SessionStart`) Verifica nueva version, sincroniza skills y `compact-guidance` en AGENTS.md |
75
- | `refacil-sdd-ai notify-update` | (`UserPromptSubmit` / `beforeSubmitPrompt`) Pregunta al usuario si desea ejecutar `/refacil:update` cuando hay migraciones pendientes |
76
+ | `refacil-sdd-ai notify-update` | (`UserPromptSubmit` / `beforeSubmitPrompt`) Solo actua si hay migracion de metodologia pendiente (misma logica que `/refacil:update`); si no, no interrumpe |
76
77
  | `refacil-sdd-ai check-review` | (`PreToolUse`) Bloquea `git push` si falta `.review-passed` en algun cambio activo |
77
78
  | `refacil-sdd-ai compact-bash` | (`PreToolUse`) Reescribe comandos Bash bare via `updatedInput` |
78
79
 
@@ -99,7 +100,7 @@ npm uninstall -g refacil-sdd-ai
99
100
  | `refacil-sdd-ai bus join --room <sala> [--session <s>] [--intro "..."]` | Unirse a sala (las skills lo hacen) |
100
101
  | `refacil-sdd-ai bus leave [--session <s>]` | Salir de la sala |
101
102
  | `refacil-sdd-ai bus say --text "..." [--session <s>]` | Broadcast (las skills lo hacen) |
102
- | `refacil-sdd-ai bus ask --to <name> --text "..." [--wait N]` | Pregunta dirigida (las skills lo hacen) |
103
+ | `refacil-sdd-ai bus ask --to <sesión> --text "..." [--wait N]` | Pregunta dirigida; `--to all` (también `*` o `everyone`) envía un ask a cada miembro de la sala excepto tú |
103
104
  | `refacil-sdd-ai bus reply --text "..." [--correlation <id>]` | Responder (las skills lo hacen) |
104
105
  | `refacil-sdd-ai bus attend [--timeout N]` | Escucha preguntas dirigidas (las skills lo hacen) |
105
106
  | `refacil-sdd-ai bus inbox [--session <s>]` | Ver mensajes nuevos |
@@ -149,7 +150,7 @@ Algunos skills delegan su trabajo pesado a **sub-agentes** que corren en context
149
150
  |---|---|
150
151
  | `/refacil:join <sala>` | Unirse o crear sala |
151
152
  | `/refacil:say "..."` | Broadcast |
152
- | `/refacil:ask @nombre "..." [--wait N]` | Pregunta dirigida (bloquea con `--wait`) |
153
+ | `/refacil:ask @nombre "..." [--wait N]` | Pregunta dirigida; `@all` pregunta a todos en la sala (bloquea con `--wait` hasta la **primera** respuesta) |
153
154
  | `/refacil:reply "..."` | Responder ultima pregunta (autocompleta `correlationId`) |
154
155
  | `/refacil:attend` | Modo escucha activa |
155
156
  | `/refacil:inbox` | Mensajes nuevos desde la ultima lectura |
@@ -234,7 +235,7 @@ Se instalan en `.claude/settings.json` **y** `.cursor/settings.json` durante `in
234
235
  | Hook | Evento | Que hace |
235
236
  |---|---|---|
236
237
  | `check-update` | `SessionStart` | Chequea nueva version en npm, la instala, sincroniza skills y **sincroniza el bloque `compact-guidance`** en `AGENTS.md`. Si hubo actualizacion, escribe un flag de notificacion para el siguiente hook. |
237
- | `notify-update` | `UserPromptSubmit` (Claude Code) / `beforeSubmitPrompt` (Cursor) | Antes de procesar el siguiente mensaje del usuario, lee el flag de actualizacion pendiente. Si existe, inyecta la instruccion para que el agente pregunte al usuario si desea ejecutar `/refacil:update`. El flag se borra tras notificar. |
238
+ | `notify-update` | `UserPromptSubmit` (Claude Code) / `beforeSubmitPrompt` (Cursor) | Si existe flag y **hay migracion de metodologia pendiente** (misma tabla que `/refacil:update`), inyecta la instruccion o pausa el primer mensaje; si solo hubo sync de skills sin migracion, el flag no se crea o se descarta sin preguntar. |
238
239
  | `compact-bash` | `PreToolUse` (Bash) | Reescribe silenciosamente comandos Bash bare via `updatedInput`. Sin turnos extra, sin que el IDE vea el cambio. Requiere Claude Code >= 2.1.89. |
239
240
  | `check-review` | `PreToolUse` (Bash) | Intercepta `git push` y bloquea si falta `.review-passed` en algun cambio activo. |
240
241
 
package/bin/cli.js CHANGED
@@ -25,6 +25,7 @@ const { installHooks, uninstallHooks, cleanLegacySettingsHooks } = require('../l
25
25
  const { handleCompact } = require('../lib/commands/compact');
26
26
  const { handleBus } = require('../lib/commands/bus');
27
27
  const { syncIgnoreFiles } = require('../lib/ignore-files');
28
+ const { methodologyMigrationPending } = require('../lib/methodology-migration-pending');
28
29
 
29
30
  const packageRoot = path.resolve(__dirname, '..');
30
31
  const projectRoot = process.cwd();
@@ -62,6 +63,12 @@ function notifyUpdate() {
62
63
  let info = {};
63
64
  try { info = JSON.parse(fs.readFileSync(flagPath, 'utf8')); } catch (_) {}
64
65
 
66
+ const mig = methodologyMigrationPending(projectRoot);
67
+ if (!mig.pending) {
68
+ try { fs.unlinkSync(flagPath); } catch (_) {}
69
+ return;
70
+ }
71
+
65
72
  const fromLabel = info.from ? `v${info.from}` : 'version anterior';
66
73
 
67
74
  // Si el usuario ya está ejecutando /refacil:update, dejar pasar y borrar el flag
@@ -145,6 +152,20 @@ function syncRepoSkillsIfStale(globalVersion) {
145
152
  }
146
153
  }
147
154
 
155
+ function migrationPendingCmd() {
156
+ const wantJson = process.argv.includes('--json');
157
+ const { pending, reasons } = methodologyMigrationPending(projectRoot);
158
+ if (wantJson) {
159
+ process.stdout.write(`${JSON.stringify({ pending, reasons })}\n`);
160
+ } else if (pending) {
161
+ console.log(' Migraciones de metodologia pendientes:');
162
+ for (const r of reasons) console.log(` - ${r}`);
163
+ } else {
164
+ console.log(' Sin migraciones de metodologia pendientes (criterio alineado con hooks y /refacil:update).');
165
+ }
166
+ process.exit(pending ? 1 : 0);
167
+ }
168
+
148
169
  function checkUpdate() {
149
170
  const { execSync } = require('child_process');
150
171
  let localVersion = getPackageVersion(packageRoot);
@@ -188,7 +209,9 @@ function checkUpdate() {
188
209
  `[refacil-sdd-ai] Skills de este repo sincronizadas (${fromLabel} -> v${syncResult.to}). ` +
189
210
  'Reinicia la sesion de Claude Code o Cursor para detectar los cambios.',
190
211
  );
191
- writePendingUpdateFlag(projectRoot, syncResult.from, syncResult.to);
212
+ if (methodologyMigrationPending(projectRoot).pending) {
213
+ writePendingUpdateFlag(projectRoot, syncResult.from, syncResult.to);
214
+ }
192
215
  } else if (syncResult && syncResult.failed) {
193
216
  console.log(
194
217
  `[refacil-sdd-ai] Skills de este repo estan desactualizadas respecto al paquete global (v${syncResult.to}) ` +
@@ -401,8 +424,9 @@ function help() {
401
424
  Comandos:
402
425
  init Instala skills en .claude/ y .cursor/, crea CLAUDE.md y .cursorrules
403
426
  update Re-copia skills (para actualizar a nueva version del paquete)
427
+ migration-pending Misma validacion que hooks/notify-update: lista migraciones (exit 1 si hay; --json)
404
428
  check-update Sincroniza skills y compact-guidance al inicio de sesion (hook SessionStart)
405
- notify-update Notifica al LLM sobre migraciones pendientes (hook UserPromptSubmit)
429
+ notify-update Notifica migracion de metodologia solo si aplica (hook UserPromptSubmit)
406
430
  check-review Verifica que el review se haya completado (usado por hook PreToolUse)
407
431
  compact-bash Reescribe comandos Bash bare para reducir tokens (usado por hook PreToolUse)
408
432
  compact Subcomandos del hook compact-bash:
@@ -418,7 +442,7 @@ function help() {
418
442
  bus join --room <sala> [--session <s>] [--intro "..."]
419
443
  bus leave [--session <s>]
420
444
  bus say --text "..." [--session <s>]
421
- bus ask --to <name> --text "..." [--wait N] [--session <s>]
445
+ bus ask --to <name|all|*|everyone> --text "..." [--wait N] [--session <s>]
422
446
  bus reply --text "..." [--correlation <id>] [--to <name>]
423
447
  bus history [--n N] [--session <s>]
424
448
  bus inbox [--session <s>]
@@ -452,6 +476,9 @@ switch (command) {
452
476
  case 'update':
453
477
  update();
454
478
  break;
479
+ case 'migration-pending':
480
+ migrationPendingCmd();
481
+ break;
455
482
  case 'check-update':
456
483
  checkUpdate();
457
484
  break;
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Indica si el `ask` ya tiene una respuesta que lo cierra para este hilo.
5
+ * Para el mismo correlationId con varios destinatarios (@all), solo cuenta
6
+ * la respuesta emitida por la sesión que recibió ese ask (ask.to).
7
+ */
8
+ function askHasMatchingReply(messages, ask) {
9
+ if (!ask || ask.kind !== 'ask' || !ask.correlationId) return true;
10
+ return messages.some((m) => {
11
+ if (m.kind !== 'reply' || m.correlationId !== ask.correlationId) return false;
12
+ if (ask.to) return m.from === ask.to;
13
+ return m.to === ask.from;
14
+ });
15
+ }
16
+
17
+ module.exports = { askHasMatchingReply };
package/lib/bus/broker.js CHANGED
@@ -12,6 +12,7 @@ const PORT_CANDIDATES = [7821, 7822, 7823];
12
12
  const HOST = '127.0.0.1';
13
13
 
14
14
  const storage = require('./storage');
15
+ const { askHasMatchingReply } = require('./askFulfillment');
15
16
 
16
17
  let WebSocketServer;
17
18
  try {
@@ -258,6 +259,24 @@ function handleLeave(state, ws, data) {
258
259
  persistSessions(state);
259
260
  }
260
261
 
262
+ /** Destinos especiales: un ask por cada miembro de la sala excepto el emisor. */
263
+ const ASK_ALL_ALIASES = new Set(['all', '*', 'everyone']);
264
+
265
+ function resolveAskTargets(state, room, fromSession, rawTo) {
266
+ if (rawTo === undefined || rawTo === null) return [null];
267
+ const trimmed = String(rawTo).trim();
268
+ if (trimmed === '') return [null];
269
+ const bare = trimmed.replace(/^@/, '');
270
+ const key = bare.toLowerCase();
271
+ if (ASK_ALL_ALIASES.has(key)) {
272
+ const members = state.rooms.get(room);
273
+ if (!members || members.size === 0) return [];
274
+ const others = Array.from(members).filter((n) => n !== fromSession);
275
+ return others.length ? others.sort() : [];
276
+ }
277
+ return [bare];
278
+ }
279
+
261
280
  function handleSay(state, ws, data) {
262
281
  const session = data.session || ws._refacilSession;
263
282
  const s = session && state.sessions.get(session);
@@ -284,19 +303,36 @@ function handleAsk(state, ws, data) {
284
303
  if (!s || !s.room) {
285
304
  return send(ws, { type: 'system', event: 'error', detail: 'sesión no está en ninguna sala' });
286
305
  }
287
- const msg = {
288
- id: newId(),
289
- ts: nowIso(),
290
- from: session,
291
- to: data.to || null,
292
- room: s.room,
293
- text: data.text || '',
294
- kind: 'ask',
295
- correlationId: data.correlationId || newId(),
296
- };
297
- appendHistory(state, s.room, msg);
298
- broadcast(state, s.room, msg);
299
- send(ws, { type: 'system', event: 'sent', detail: { id: msg.id, correlationId: msg.correlationId } });
306
+ const correlationId = data.correlationId || newId();
307
+ const targets = resolveAskTargets(state, s.room, session, data.to);
308
+ if (targets.length === 0) {
309
+ return send(ws, {
310
+ type: 'system',
311
+ event: 'error',
312
+ detail: 'sin destinatarios (@all en sala vacía o solo tú)',
313
+ });
314
+ }
315
+ let firstId = null;
316
+ for (const to of targets) {
317
+ const msg = {
318
+ id: newId(),
319
+ ts: nowIso(),
320
+ from: session,
321
+ to,
322
+ room: s.room,
323
+ text: data.text || '',
324
+ kind: 'ask',
325
+ correlationId,
326
+ };
327
+ if (!firstId) firstId = msg.id;
328
+ appendHistory(state, s.room, msg);
329
+ broadcast(state, s.room, msg);
330
+ }
331
+ send(ws, {
332
+ type: 'system',
333
+ event: 'sent',
334
+ detail: { id: firstId, correlationId, fanOut: targets.length },
335
+ });
300
336
  }
301
337
 
302
338
  function handleReply(state, ws, data) {
@@ -315,9 +351,7 @@ function handleReply(state, ws, data) {
315
351
  for (const ask of hist) {
316
352
  if (ask.kind !== 'ask') continue;
317
353
  if (ask.to && ask.to !== session) continue;
318
- const replied = hist.some(
319
- (m) => m.kind === 'reply' && m.correlationId === ask.correlationId,
320
- );
354
+ const replied = askHasMatchingReply(hist, ask);
321
355
  if (replied) continue;
322
356
  correlationId = ask.correlationId || null;
323
357
  if (!toOverride) toOverride = ask.from;
package/lib/bus/ui/app.js CHANGED
@@ -28,11 +28,23 @@
28
28
  rooms: {},
29
29
  knownRooms: new Set(),
30
30
  selectedRoom: '*',
31
- askIndex: new Map(),
32
- answeredCorrelations: new Set(),
31
+ answeredByResponder: new Set(),
33
32
  msgCount: 0,
34
33
  };
35
34
 
35
+ function cssEsc(s) {
36
+ if (typeof CSS !== 'undefined' && CSS.escape) return CSS.escape(String(s));
37
+ return String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
38
+ }
39
+
40
+ function isAskAnswered(m) {
41
+ if (m.kind !== 'ask' || !m.correlationId) return false;
42
+ if (m.to) return state.answeredByResponder.has(`${m.correlationId}\t${m.to}`);
43
+ return state.messages.some(
44
+ (r) => r.kind === 'reply' && r.correlationId === m.correlationId,
45
+ );
46
+ }
47
+
36
48
  els.port.textContent = 'puerto ' + state.port;
37
49
 
38
50
  function setConn(kind, label) {
@@ -79,6 +91,9 @@
79
91
  div.className = classes.join(' ');
80
92
  div.dataset.corr = m.correlationId || '';
81
93
  div.dataset.kind = m.kind || '';
94
+ if (m.kind === 'ask' && m.correlationId && m.to) {
95
+ div.dataset.askTo = m.to;
96
+ }
82
97
 
83
98
  const head = document.createElement('div');
84
99
  head.className = 'msg-head';
@@ -103,7 +118,7 @@
103
118
 
104
119
  if (m.correlationId) {
105
120
  const corr = document.createElement('div');
106
- const isAnswered = state.answeredCorrelations.has(m.correlationId);
121
+ const isAnswered = m.kind === 'ask' ? isAskAnswered(m) : false;
107
122
  corr.className = 'msg-corr ' + (m.kind === 'ask' ? (isAnswered ? 'answered' : 'pending') : '');
108
123
  corr.textContent = m.correlationId.slice(0, 8);
109
124
  div.appendChild(corr);
@@ -136,20 +151,21 @@
136
151
  state.messageIds.add(m.id);
137
152
  state.messages.push(m);
138
153
  state.msgCount++;
139
- if (m.kind === 'ask' && m.correlationId) {
140
- state.askIndex.set(m.correlationId, m);
141
- }
142
- if (m.kind === 'reply' && m.correlationId) {
143
- state.answeredCorrelations.add(m.correlationId);
154
+ if (m.kind === 'reply' && m.correlationId && m.from) {
155
+ state.answeredByResponder.add(`${m.correlationId}\t${m.from}`);
144
156
  }
145
157
  return true;
146
158
  }
147
159
 
148
160
  function appendMsg(m) {
149
161
  if (!ingestMsg(m)) return; // dedup
150
- if (m.kind === 'reply' && m.correlationId) {
151
- const prev = els.feedBody.querySelector(`.msg.ask[data-corr="${m.correlationId}"] .msg-corr`);
152
- if (prev) { prev.className = 'msg-corr answered'; prev.textContent = m.correlationId.slice(0, 8); }
162
+ if (m.kind === 'reply' && m.correlationId && m.from) {
163
+ const sel = `.msg.ask[data-corr="${cssEsc(m.correlationId)}"][data-ask-to="${cssEsc(m.from)}"] .msg-corr`;
164
+ const prev = els.feedBody.querySelector(sel);
165
+ if (prev) {
166
+ prev.className = 'msg-corr answered';
167
+ prev.textContent = m.correlationId.slice(0, 8);
168
+ }
153
169
  }
154
170
  const node = renderMsg(m);
155
171
  if (node) {
@@ -176,10 +192,10 @@
176
192
 
177
193
  function updateStats() {
178
194
  els.msgCount.textContent = state.msgCount;
179
- els.pairCount.textContent = state.answeredCorrelations.size;
195
+ els.pairCount.textContent = state.messages.filter((x) => x.kind === 'reply').length;
180
196
  let pending = 0;
181
- for (const [cid] of state.askIndex) {
182
- if (!state.answeredCorrelations.has(cid)) pending++;
197
+ for (const m of state.messages) {
198
+ if (m.kind === 'ask' && !isAskAnswered(m)) pending++;
183
199
  }
184
200
  els.pendingCount.textContent = pending;
185
201
  }
@@ -238,8 +254,8 @@
238
254
  });
239
255
  els.clearBtn.addEventListener('click', () => {
240
256
  state.messages = [];
241
- state.askIndex.clear();
242
- state.answeredCorrelations.clear();
257
+ state.messageIds.clear();
258
+ state.answeredByResponder.clear();
243
259
  state.msgCount = 0;
244
260
  rerenderFeed();
245
261
  updateStats();
@@ -7,6 +7,7 @@ const busSpawn = require('../bus/spawn');
7
7
  const busClient = require('../bus/client');
8
8
  const busWatch = require('../bus/watch');
9
9
  const busPresenter = require('../bus/presenter');
10
+ const { askHasMatchingReply } = require('../bus/askFulfillment');
10
11
 
11
12
  function parseBusArgs(argv) {
12
13
  const args = {};
@@ -185,7 +186,7 @@ async function busAsk(args, packageRoot) {
185
186
  const text = args.text;
186
187
  const waitSec = args.wait ? parseInt(args.wait, 10) : 0;
187
188
  if (!to || !text) {
188
- console.error(' Uso: refacil-sdd-ai bus ask --to <name> --text "..." [--wait N] [--session <s>]');
189
+ console.error(' Uso: refacil-sdd-ai bus ask --to <name|all> --text "..." [--wait N] [--session <s>]');
189
190
  process.exit(1);
190
191
  }
191
192
  const { ws } = await connectOrDie(packageRoot);
@@ -203,7 +204,9 @@ async function busAsk(args, packageRoot) {
203
204
  process.exit(1);
204
205
  }
205
206
  const correlationId = ack.detail.correlationId;
206
- console.log(` Pregunta enviada a @${to.replace(/^@/, '')} (correlationId ${correlationId}).`);
207
+ const fanOut = ack.detail && ack.detail.fanOut;
208
+ const destLabel = fanOut && fanOut > 1 ? `${fanOut} miembros (@all)` : `@${to.replace(/^@/, '')}`;
209
+ console.log(` Pregunta enviada a ${destLabel} (correlationId ${correlationId}).`);
207
210
 
208
211
  if (waitSec > 0) {
209
212
  console.log(` Esperando respuesta hasta ${waitSec}s...`);
@@ -306,10 +309,7 @@ async function busInbox(args, packageRoot) {
306
309
  function findFirstUnansweredAsk(messages, session) {
307
310
  const asks = messages.filter((m) => m.kind === 'ask' && m.to === session);
308
311
  for (const ask of asks) {
309
- const hasReply = messages.some(
310
- (m) => m.kind === 'reply' && m.correlationId === ask.correlationId,
311
- );
312
- if (!hasReply) return ask;
312
+ if (!askHasMatchingReply(messages, ask)) return ask;
313
313
  }
314
314
  return null;
315
315
  }
@@ -0,0 +1,136 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ /**
7
+ * Motor compartido con `refacil-sdd-ai migration-pending`, hooks `check-update` / `notify-update`
8
+ * y la skill `refacil:update`. Si ninguna condicion aplica, no hace falta migrar metodologia.
9
+ */
10
+ const REQUIRED_OPENSPEC_SKILLS = new Set([
11
+ 'openspec-propose',
12
+ 'openspec-explore',
13
+ 'openspec-apply-change',
14
+ 'openspec-archive-change',
15
+ 'openspec-verify-change',
16
+ ]);
17
+
18
+ const REQUIRED_OPSX_CLAUDE = new Set([
19
+ 'apply.md',
20
+ 'archive.md',
21
+ 'explore.md',
22
+ 'propose.md',
23
+ 'verify.md',
24
+ ]);
25
+
26
+ const REQUIRED_OPSX_CURSOR = new Set([
27
+ 'opsx-apply.md',
28
+ 'opsx-archive.md',
29
+ 'opsx-explore.md',
30
+ 'opsx-propose.md',
31
+ 'opsx-verify.md',
32
+ ]);
33
+
34
+ function legacyIndexDoc(filePath) {
35
+ if (!fs.existsSync(filePath)) return false;
36
+ const content = fs.readFileSync(filePath, 'utf8');
37
+ const lines = content.split(/\r?\n/).length;
38
+ const pointsToAgents = /AGENTS\.md/i.test(content);
39
+ return lines > 5 || !pointsToAgents;
40
+ }
41
+
42
+ function collectExtraOpenspecSkills(root) {
43
+ const out = [];
44
+ for (const rel of ['.claude/skills', '.cursor/skills']) {
45
+ const full = path.join(root, rel);
46
+ if (!fs.existsSync(full)) continue;
47
+ let names;
48
+ try {
49
+ names = fs.readdirSync(full, { withFileTypes: true });
50
+ } catch (_) {
51
+ continue;
52
+ }
53
+ for (const d of names) {
54
+ if (!d.isDirectory()) continue;
55
+ const name = d.name;
56
+ if (!name.startsWith('openspec-')) continue;
57
+ if (REQUIRED_OPENSPEC_SKILLS.has(name)) continue;
58
+ out.push(path.join(rel, name));
59
+ }
60
+ }
61
+ return out;
62
+ }
63
+
64
+ function collectExtraOpsxClaude(root) {
65
+ const dir = path.join(root, '.claude', 'commands', 'opsx');
66
+ if (!fs.existsSync(dir)) return [];
67
+ let names;
68
+ try {
69
+ names = fs.readdirSync(dir);
70
+ } catch (_) {
71
+ return [];
72
+ }
73
+ const extra = [];
74
+ for (const name of names) {
75
+ if (!name.endsWith('.md')) continue;
76
+ if (REQUIRED_OPSX_CLAUDE.has(name)) continue;
77
+ extra.push(path.join('.claude/commands/opsx', name));
78
+ }
79
+ return extra;
80
+ }
81
+
82
+ function collectExtraOpsxCursor(root) {
83
+ const dir = path.join(root, '.cursor', 'commands');
84
+ if (!fs.existsSync(dir)) return [];
85
+ let names;
86
+ try {
87
+ names = fs.readdirSync(dir);
88
+ } catch (_) {
89
+ return [];
90
+ }
91
+ const extra = [];
92
+ for (const name of names) {
93
+ if (!name.startsWith('opsx-') || !name.endsWith('.md')) continue;
94
+ if (REQUIRED_OPSX_CURSOR.has(name)) continue;
95
+ extra.push(path.join('.cursor/commands', name));
96
+ }
97
+ return extra;
98
+ }
99
+
100
+ /**
101
+ * @param {string} root - raíz del repo
102
+ * @returns {{ pending: boolean, reasons: string[] }}
103
+ */
104
+ function methodologyMigrationPending(root) {
105
+ const reasons = [];
106
+
107
+ const agentsMd = path.join(root, 'AGENTS.md');
108
+ const agentsDir = path.join(root, '.agents');
109
+ if (fs.existsSync(agentsMd) && !fs.existsSync(agentsDir)) {
110
+ reasons.push('AGENTS.md existe sin carpeta .agents/');
111
+ }
112
+
113
+ const claudeMd = path.join(root, 'CLAUDE.md');
114
+ if (legacyIndexDoc(claudeMd)) {
115
+ reasons.push('CLAUDE.md requiere normalización (índice mínimo → AGENTS.md)');
116
+ }
117
+
118
+ const cursorRules = path.join(root, '.cursorrules');
119
+ if (legacyIndexDoc(cursorRules)) {
120
+ reasons.push('.cursorrules requiere normalización (índice mínimo → AGENTS.md)');
121
+ }
122
+
123
+ const extraSkills = collectExtraOpenspecSkills(root);
124
+ if (extraSkills.length) {
125
+ reasons.push(`skills OpenSpec sobrantes: ${extraSkills.join(', ')}`);
126
+ }
127
+
128
+ const extraOpsx = [...collectExtraOpsxClaude(root), ...collectExtraOpsxCursor(root)];
129
+ if (extraOpsx.length) {
130
+ reasons.push(`commands opsx sobrantes: ${extraOpsx.join(', ')}`);
131
+ }
132
+
133
+ return { pending: reasons.length > 0, reasons };
134
+ }
135
+
136
+ module.exports = { methodologyMigrationPending };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "refacil-sdd-ai",
3
- "version": "4.0.9",
3
+ "version": "4.0.11",
4
4
  "description": "SDD-AI: Specification-Driven Development with AI — metodologia de desarrollo con IA usando OpenSpec, Claude Code y Cursor",
5
5
  "bin": {
6
6
  "refacil-sdd-ai": "./bin/cli.js"
@@ -37,7 +37,7 @@
37
37
  "node": ">=20.19.0"
38
38
  },
39
39
  "scripts": {
40
- "test": "node --test test/hooks.test.js test/installer.test.js test/ignore-files.test.js"
40
+ "test": "node --test test/hooks.test.js test/installer.test.js test/ignore-files.test.js test/methodology-migration-pending.test.js"
41
41
  },
42
42
  "dependencies": {
43
43
  "ws": "^8.18.0"
@@ -6,7 +6,7 @@ user-invocable: true
6
6
 
7
7
  # refacil:ask — Pregunta dirigida a otra sesión
8
8
 
9
- Envía una pregunta dirigida a una sesión específica de la sala. **$ARGUMENTS** incluye `@<destino>` y el texto.
9
+ Envía una pregunta dirigida a una sesión de la sala, o a **todas las demás** sesiones de la sala con `@all` (alias: `*`, `everyone`). **$ARGUMENTS** incluye `@<destino>` y el texto.
10
10
 
11
11
  ## Instrucciones
12
12
 
@@ -23,6 +23,8 @@ Extrae:
23
23
 
24
24
  Si el destino no es claro, usa `/refacil:rooms` (vía `refacil-sdd-ai bus rooms`) para listar sesiones activas y pide aclaración al usuario.
25
25
 
26
+ **Broadcast dirigido (`@all`)**: el broker emite un `ask` por cada miembro (excepto quien pregunta), mismo `correlationId`. Cada receptor puede `/refacil:reply` por su lado. Con `--wait`, el CLI sigue devolviendo **la primera** respuesta que llegue (no espera a todos).
27
+
26
28
  ### Paso 2: Decidir si usar `--wait`
27
29
 
28
30
  Dos modos:
@@ -8,26 +8,41 @@ user-invocable: true
8
8
 
9
9
  Detecta el estado actual del repo y aplica solo lo que este pendiente. No repite pasos de instalacion (OpenSpec, hooks, skills).
10
10
 
11
- ## Paso 1: Detectar migraciones pendientes
11
+ El hook `notify-update` usa el **mismo motor** que este comando; no re-evalues a mano el repo para decidir si hay trabajo.
12
12
 
13
- Evalua cada condicion y marca si aplica:
13
+ ## Paso 1: Validar con el CLI (obligatorio)
14
14
 
15
- | # | Condicion detectada | Migracion |
15
+ En la **raiz del repo** (donde esta `AGENTS.md` o `.claude/`), ejecuta con `Bash`:
16
+
17
+ ```bash
18
+ refacil-sdd-ai migration-pending
19
+ ```
20
+
21
+ - **Codigo de salida 0**: no hay migraciones de metodologia pendientes → informa al usuario que todo esta al dia respecto a este criterio y **termina** (sin tocar archivos).
22
+ - **Codigo de salida 1**: hay al menos una razon listada en stdout → continua al Paso 2.
23
+
24
+ Opcional para parseo estable: `refacil-sdd-ai migration-pending --json` → objeto `{ "pending": bool, "reasons": string[] }`.
25
+
26
+ No sustituyas este paso por inspeccion manual del arbol salvo que el comando falle (error de entorno); en ese caso documenta el error y pide reintentar.
27
+
28
+ ### Referencia (mapeo razon → accion en Paso 3)
29
+
30
+ La implementacion vive en `lib/methodology-migration-pending.js` del paquete; la tabla resume lo que detecta:
31
+
32
+ | # | Condicion (resumen) | Migracion en Paso 3 |
16
33
  |---|---|---|
17
34
  | 1 | `AGENTS.md` existe y no existe carpeta `.agents/` | Reestructurar en `.agents/` + reescribir como indice |
18
35
  | 2 | `CLAUDE.md` tiene mas de 5 lineas o no apunta a `AGENTS.md` | Reemplazar por indice minimo |
19
36
  | 3 | `.cursorrules` tiene mas de 5 lineas o no apunta a `AGENTS.md` | Reemplazar por indice minimo |
20
- | 4 | Existen skills `openspec-*` o commands `opsx-*` fuera del conjunto requerido en `.claude/` o `.cursor/` | Eliminar los sobrantes y reconfigurar OpenSpec |
21
-
22
- Si ninguna condicion aplica: informar al usuario que todo esta al dia y salir sin cambios.
37
+ | 4 | Skills `openspec-*` o commands `opsx-*` fuera del conjunto requerido | Eliminar sobrantes y reconfigurar OpenSpec |
23
38
 
24
39
  ## Paso 2: Confirmar con el usuario
25
40
 
26
- Mostrar la lista de migraciones detectadas y pedir confirmacion antes de aplicar cualquier cambio.
41
+ Mostrar al usuario las mismas lineas que imprimio `migration-pending` (o el array `reasons` del JSON) y pedir confirmacion antes de aplicar cualquier cambio.
27
42
 
28
43
  ## Paso 3: Aplicar migraciones confirmadas
29
44
 
30
- Ejecutar solo las migraciones marcadas en el Paso 1.
45
+ Ejecutar solo las migraciones que correspondan a las razones detectadas (ver tabla de referencia arriba).
31
46
 
32
47
  ### Migracion 1 — AGENTS.md → `.agents/` + indice
33
48
 
@@ -108,7 +123,8 @@ Informar que archivos se crearon o modificaron. Mencionar que el bloque `compact
108
123
 
109
124
  ## Reglas
110
125
 
126
+ - **Deteccion**: confiar en `refacil-sdd-ai migration-pending` (mismo criterio que `check-update` / `notify-update`).
111
127
  - Solo aplica lo que este pendiente — no toca archivos que ya cumplen el patron.
112
128
  - No inventa contenido: distribuye lo que ya existe en AGENTS.md sin agregar ni eliminar informacion.
113
129
  - No ejecuta pasos de OpenSpec, hooks ni instalacion de skills.
114
- - Extensible: futuras versiones de la metodologia agregaran filas a la tabla del Paso 1 sin cambiar la estructura de la skill.
130
+ - Extensible: nuevas reglas de deteccion se agregan en el paquete (`methodology-migration-pending.js`); esta skill actualiza la tabla de referencia y los pasos de aplicacion cuando cambie el contrato.
@@ -29,7 +29,7 @@ Canal local de texto plano entre sesiones de Claude Code / Cursor corriendo en d
29
29
  |---------|-------------|
30
30
  | `/refacil:join <sala>` | Crear o unirse a una sala. La primera vez genera un bloque de presentacion en `AGENTS.md`. |
31
31
  | `/refacil:say "..."` | Anuncio a toda la sala. |
32
- | `/refacil:ask @nombre "..." [--wait N]` | Pregunta dirigida a otra sesion. `--wait N` bloquea hasta respuesta o N seg. |
32
+ | `/refacil:ask @nombre "..." [--wait N]` | Pregunta dirigida; `@all` (o `*`) pregunta a todos en la sala. `--wait N` bloquea hasta la primera respuesta o N seg. |
33
33
  | `/refacil:reply "..."` | Responde la ultima pregunta dirigida (autocompleta `correlationId`). |
34
34
  | `/refacil:attend` | Modo escucha activa: recibe preguntas y el LLM las responde, luego re-invoca para seguir escuchando. |
35
35
  | `/refacil:inbox` | Ver mensajes nuevos desde la ultima lectura. |