refacil-sdd-ai 4.0.9 → 4.0.10
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 +2 -2
- package/bin/cli.js +1 -1
- 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/package.json +1 -1
- package/skills/ask/SKILL.md +3 -1
- package/templates/methodology-guide.md +1 -1
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ npm uninstall -g refacil-sdd-ai
|
|
|
99
99
|
| `refacil-sdd-ai bus join --room <sala> [--session <s>] [--intro "..."]` | Unirse a sala (las skills lo hacen) |
|
|
100
100
|
| `refacil-sdd-ai bus leave [--session <s>]` | Salir de la sala |
|
|
101
101
|
| `refacil-sdd-ai bus say --text "..." [--session <s>]` | Broadcast (las skills lo hacen) |
|
|
102
|
-
| `refacil-sdd-ai bus ask --to <
|
|
102
|
+
| `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
103
|
| `refacil-sdd-ai bus reply --text "..." [--correlation <id>]` | Responder (las skills lo hacen) |
|
|
104
104
|
| `refacil-sdd-ai bus attend [--timeout N]` | Escucha preguntas dirigidas (las skills lo hacen) |
|
|
105
105
|
| `refacil-sdd-ai bus inbox [--session <s>]` | Ver mensajes nuevos |
|
|
@@ -149,7 +149,7 @@ Algunos skills delegan su trabajo pesado a **sub-agentes** que corren en context
|
|
|
149
149
|
|---|---|
|
|
150
150
|
| `/refacil:join <sala>` | Unirse o crear sala |
|
|
151
151
|
| `/refacil:say "..."` | Broadcast |
|
|
152
|
-
| `/refacil:ask @nombre "..." [--wait N]` | Pregunta dirigida (bloquea con `--wait`) |
|
|
152
|
+
| `/refacil:ask @nombre "..." [--wait N]` | Pregunta dirigida; `@all` pregunta a todos en la sala (bloquea con `--wait` hasta la **primera** respuesta) |
|
|
153
153
|
| `/refacil:reply "..."` | Responder ultima pregunta (autocompleta `correlationId`) |
|
|
154
154
|
| `/refacil:attend` | Modo escucha activa |
|
|
155
155
|
| `/refacil:inbox` | Mensajes nuevos desde la ultima lectura |
|
package/bin/cli.js
CHANGED
|
@@ -418,7 +418,7 @@ function help() {
|
|
|
418
418
|
bus join --room <sala> [--session <s>] [--intro "..."]
|
|
419
419
|
bus leave [--session <s>]
|
|
420
420
|
bus say --text "..." [--session <s>]
|
|
421
|
-
bus ask --to <name> --text "..." [--wait N] [--session <s>]
|
|
421
|
+
bus ask --to <name|all|*|everyone> --text "..." [--wait N] [--session <s>]
|
|
422
422
|
bus reply --text "..." [--correlation <id>] [--to <name>]
|
|
423
423
|
bus history [--n N] [--session <s>]
|
|
424
424
|
bus inbox [--session <s>]
|
|
@@ -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
|
}
|
package/package.json
CHANGED
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:
|
|
@@ -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. |
|