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 +5 -4
- package/bin/cli.js +30 -3
- package/lib/bus/askFulfillment.js +17 -0
- package/lib/bus/broker.js +50 -16
- package/lib/bus/ui/app.js +32 -16
- package/lib/commands/bus.js +6 -6
- package/lib/methodology-migration-pending.js +136 -0
- package/package.json +2 -2
- package/skills/ask/SKILL.md +3 -1
- package/skills/update/SKILL.md +25 -9
- package/templates/methodology-guide.md +1 -1
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`)
|
|
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 <
|
|
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) |
|
|
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
|
-
|
|
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
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 === '
|
|
140
|
-
state.
|
|
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
|
|
152
|
-
|
|
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.
|
|
195
|
+
els.pairCount.textContent = state.messages.filter((x) => x.kind === 'reply').length;
|
|
180
196
|
let pending = 0;
|
|
181
|
-
for (const
|
|
182
|
-
if (!
|
|
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.
|
|
242
|
-
state.
|
|
257
|
+
state.messageIds.clear();
|
|
258
|
+
state.answeredByResponder.clear();
|
|
243
259
|
state.msgCount = 0;
|
|
244
260
|
rerenderFeed();
|
|
245
261
|
updateStats();
|
package/lib/commands/bus.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|
package/skills/ask/SKILL.md
CHANGED
|
@@ -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
|
|
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:
|
package/skills/update/SKILL.md
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
13
|
+
## Paso 1: Validar con el CLI (obligatorio)
|
|
14
14
|
|
|
15
|
-
|
|
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 |
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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. |
|