sentinelayer-cli 0.8.12 → 0.9.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.
- package/package.json +7 -2
- package/src/agents/devtestbot/config/definition.js +100 -0
- package/src/agents/devtestbot/config/system-prompt.js +92 -0
- package/src/agents/devtestbot/index.js +9 -0
- package/src/agents/devtestbot/runner.js +769 -0
- package/src/agents/devtestbot/tool.js +707 -0
- package/src/commands/legacy-args.js +4 -0
- package/src/commands/omargate.js +4 -0
- package/src/commands/session.js +415 -147
- package/src/commands/swarm.js +11 -2
- package/src/guide/generator.js +14 -0
- package/src/legacy-cli.js +34 -17
- package/src/prompt/generator.js +4 -16
- package/src/review/ai-review.js +95 -6
- package/src/review/dd-report-email-client.js +148 -0
- package/src/review/investor-dd-devtestbot.js +599 -0
- package/src/review/investor-dd-orchestrator.js +135 -3
- package/src/review/omargate-orchestrator.js +20 -2
- package/src/review/persona-prompts.js +34 -1
- package/src/review/report.js +61 -2
- package/src/session/coordination-guidance.js +48 -0
- package/src/session/daemon.js +3 -2
- package/src/session/listener.js +236 -0
- package/src/session/setup-guides.js +3 -15
- package/src/session/store.js +54 -5
- package/src/spec/generator.js +8 -10
- package/src/swarm/registry.js +20 -0
- package/src/swarm/runtime.js +139 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
|
|
3
|
+
import { pollSessionEvents } from "./sync.js";
|
|
4
|
+
import { readSyncCursor, writeSyncCursor } from "./sync-cursor.js";
|
|
5
|
+
|
|
6
|
+
const BROADCAST_RECIPIENTS = new Set([
|
|
7
|
+
"*",
|
|
8
|
+
"all",
|
|
9
|
+
"broadcast",
|
|
10
|
+
"everyone",
|
|
11
|
+
"anyone",
|
|
12
|
+
"agents",
|
|
13
|
+
"all-agents",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function normalizeString(value) {
|
|
17
|
+
return String(value || "").trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeComparableId(value) {
|
|
21
|
+
return normalizeString(value)
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
24
|
+
.replace(/^-+|-+$/g, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizePositiveInteger(value, fallbackValue) {
|
|
28
|
+
if (value === undefined || value === null || String(value).trim() === "") {
|
|
29
|
+
return fallbackValue;
|
|
30
|
+
}
|
|
31
|
+
const normalized = Number(value);
|
|
32
|
+
if (!Number.isFinite(normalized) || normalized <= 0) return fallbackValue;
|
|
33
|
+
return Math.floor(normalized);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isPlainObject(value) {
|
|
37
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function addRecipientValue(values, value) {
|
|
41
|
+
if (value === undefined || value === null) return;
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
for (const item of value) addRecipientValue(values, item);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (isPlainObject(value)) {
|
|
47
|
+
addRecipientValue(values, value.id || value.agentId || value.name);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const raw = normalizeString(value);
|
|
51
|
+
if (!raw) return;
|
|
52
|
+
for (const token of raw.split(/[\s,;]+/g)) {
|
|
53
|
+
const normalized = normalizeString(token);
|
|
54
|
+
if (normalized) values.push(normalized);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function collectSessionEventRecipients(event = {}) {
|
|
59
|
+
const values = [];
|
|
60
|
+
if (!isPlainObject(event)) return values;
|
|
61
|
+
const payload = isPlainObject(event.payload) ? event.payload : {};
|
|
62
|
+
for (const source of [
|
|
63
|
+
event.to,
|
|
64
|
+
event.recipient,
|
|
65
|
+
event.recipients,
|
|
66
|
+
event.targetAgent,
|
|
67
|
+
event.targetAgentId,
|
|
68
|
+
payload.to,
|
|
69
|
+
payload.recipient,
|
|
70
|
+
payload.recipients,
|
|
71
|
+
payload.targetAgent,
|
|
72
|
+
payload.targetAgentId,
|
|
73
|
+
]) {
|
|
74
|
+
addRecipientValue(values, source);
|
|
75
|
+
}
|
|
76
|
+
return values;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function eventMatchesAgent(event = {}, agentId = "") {
|
|
80
|
+
if (!isPlainObject(event)) return false;
|
|
81
|
+
const normalizedAgentId = normalizeComparableId(agentId);
|
|
82
|
+
if (!normalizedAgentId) return false;
|
|
83
|
+
|
|
84
|
+
const payload = isPlainObject(event.payload) ? event.payload : {};
|
|
85
|
+
if (event.broadcast === true || payload.broadcast === true) return true;
|
|
86
|
+
|
|
87
|
+
const recipients = collectSessionEventRecipients(event);
|
|
88
|
+
if (recipients.length === 0) return true;
|
|
89
|
+
|
|
90
|
+
for (const recipient of recipients) {
|
|
91
|
+
const rawRecipient = normalizeString(recipient).toLowerCase();
|
|
92
|
+
if (BROADCAST_RECIPIENTS.has(rawRecipient)) return true;
|
|
93
|
+
const normalizedRecipient = normalizeComparableId(recipient);
|
|
94
|
+
if (!normalizedRecipient) continue;
|
|
95
|
+
if (BROADCAST_RECIPIENTS.has(normalizedRecipient)) return true;
|
|
96
|
+
if (normalizedRecipient === normalizedAgentId) return true;
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function listenCursorSuffix(agentId = "") {
|
|
102
|
+
return `listen-${normalizeComparableId(agentId) || "agent"}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function defaultSleep(ms, { signal } = {}) {
|
|
106
|
+
await delay(ms, undefined, { signal });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function shouldAbort(error, signal) {
|
|
110
|
+
return Boolean(signal?.aborted || error?.name === "AbortError" || error?.code === "ABORT_ERR");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function cursorFromEvents(events = [], fallbackCursor = null) {
|
|
114
|
+
let cursor = normalizeString(fallbackCursor) || null;
|
|
115
|
+
for (const event of events) {
|
|
116
|
+
const candidate = normalizeString(event?.cursor);
|
|
117
|
+
if (candidate) cursor = candidate;
|
|
118
|
+
}
|
|
119
|
+
return cursor;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function eventTimestampMs(event = {}) {
|
|
123
|
+
for (const key of ["ts", "timestamp", "createdAt", "at"]) {
|
|
124
|
+
const epoch = Date.parse(normalizeString(event?.[key]));
|
|
125
|
+
if (Number.isFinite(epoch)) return epoch;
|
|
126
|
+
}
|
|
127
|
+
return 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Poll session events in the background and emit only events addressed to
|
|
132
|
+
* the current agent or broadcast to everyone. The loop advances its cursor
|
|
133
|
+
* across non-matching events so direct listeners do not replay unrelated
|
|
134
|
+
* traffic forever.
|
|
135
|
+
*/
|
|
136
|
+
export async function listenSessionEvents({
|
|
137
|
+
sessionId,
|
|
138
|
+
targetPath = process.cwd(),
|
|
139
|
+
agentId = "cli-user",
|
|
140
|
+
intervalSeconds = 60,
|
|
141
|
+
limit = 200,
|
|
142
|
+
since = undefined,
|
|
143
|
+
replay = false,
|
|
144
|
+
maxPolls = null,
|
|
145
|
+
signal,
|
|
146
|
+
onEvent = async () => {},
|
|
147
|
+
onError = async () => {},
|
|
148
|
+
_poll = pollSessionEvents,
|
|
149
|
+
_readCursor = readSyncCursor,
|
|
150
|
+
_writeCursor = writeSyncCursor,
|
|
151
|
+
_sleep = defaultSleep,
|
|
152
|
+
_nowMs = Date.now,
|
|
153
|
+
} = {}) {
|
|
154
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
155
|
+
const normalizedAgentId = normalizeComparableId(agentId) || "cli-user";
|
|
156
|
+
if (!normalizedSessionId) {
|
|
157
|
+
throw new Error("session id is required.");
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const cursorSuffix = listenCursorSuffix(normalizedAgentId);
|
|
161
|
+
let cursor =
|
|
162
|
+
typeof since === "string" || since === null
|
|
163
|
+
? normalizeString(since) || null
|
|
164
|
+
: await _readCursor(normalizedSessionId, { targetPath, suffix: cursorSuffix });
|
|
165
|
+
let primed = Boolean(cursor) || Boolean(replay);
|
|
166
|
+
let pollCount = 0;
|
|
167
|
+
let emitted = 0;
|
|
168
|
+
let matched = 0;
|
|
169
|
+
let persistedCursor = false;
|
|
170
|
+
let lastReason = "";
|
|
171
|
+
const maxPollCount = normalizePositiveInteger(maxPolls, 0);
|
|
172
|
+
const pollLimit = normalizePositiveInteger(limit, 200);
|
|
173
|
+
const sleepMs = Math.max(1, normalizePositiveInteger(intervalSeconds, 60)) * 1000;
|
|
174
|
+
const startedAtMs = Number(_nowMs()) || Date.now();
|
|
175
|
+
|
|
176
|
+
while (!signal?.aborted) {
|
|
177
|
+
pollCount += 1;
|
|
178
|
+
const result = await _poll(normalizedSessionId, {
|
|
179
|
+
targetPath,
|
|
180
|
+
since: cursor,
|
|
181
|
+
limit: pollLimit,
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (result?.ok) {
|
|
185
|
+
lastReason = "";
|
|
186
|
+
const events = Array.isArray(result.events) ? result.events : [];
|
|
187
|
+
const shouldEmitBatch = primed || Boolean(replay);
|
|
188
|
+
for (const event of events) {
|
|
189
|
+
if (!eventMatchesAgent(event, normalizedAgentId)) continue;
|
|
190
|
+
matched += 1;
|
|
191
|
+
if (!shouldEmitBatch && eventTimestampMs(event) < startedAtMs) continue;
|
|
192
|
+
await onEvent(event);
|
|
193
|
+
emitted += 1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const nextCursor = normalizeString(result.cursor) || cursorFromEvents(events, cursor);
|
|
197
|
+
if (nextCursor && nextCursor !== cursor) {
|
|
198
|
+
const writeResult = await _writeCursor(normalizedSessionId, nextCursor, {
|
|
199
|
+
targetPath,
|
|
200
|
+
suffix: cursorSuffix,
|
|
201
|
+
}).catch(() => null);
|
|
202
|
+
persistedCursor = Boolean(writeResult?.written) || persistedCursor;
|
|
203
|
+
cursor = nextCursor;
|
|
204
|
+
}
|
|
205
|
+
primed = true;
|
|
206
|
+
} else {
|
|
207
|
+
lastReason = normalizeString(result?.reason) || "poll_failed";
|
|
208
|
+
await onError({
|
|
209
|
+
ok: false,
|
|
210
|
+
reason: lastReason,
|
|
211
|
+
cursor: result?.cursor || cursor || null,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (maxPollCount > 0 && pollCount >= maxPollCount) break;
|
|
216
|
+
try {
|
|
217
|
+
await _sleep(sleepMs, { signal });
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (shouldAbort(error, signal)) break;
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
ok: true,
|
|
226
|
+
sessionId: normalizedSessionId,
|
|
227
|
+
agentId: normalizedAgentId,
|
|
228
|
+
cursor,
|
|
229
|
+
cursorSuffix,
|
|
230
|
+
pollCount,
|
|
231
|
+
matched,
|
|
232
|
+
emitted,
|
|
233
|
+
persistedCursor,
|
|
234
|
+
reason: lastReason,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
|
|
5
5
|
import { buildAgentsSessionGuideContent } from "../legacy-cli.js";
|
|
6
|
+
import { renderCoordinationBulletList } from "./coordination-guidance.js";
|
|
6
7
|
import { getSession } from "./store.js";
|
|
7
8
|
|
|
8
9
|
const SECTION_START_MARKER = "<!-- SENTINELAYER_SESSION_COORDINATION:START -->";
|
|
@@ -26,21 +27,8 @@ function buildSessionCoordinationSection() {
|
|
|
26
27
|
return `${SECTION_START_MARKER}
|
|
27
28
|
${SECTION_HEADING}
|
|
28
29
|
|
|
29
|
-
###
|
|
30
|
-
|
|
31
|
-
- Join if one exists: \`sl session join <id> --name <your-name>\`
|
|
32
|
-
- Read recent context: \`sl session read <id> --tail 20\`
|
|
33
|
-
|
|
34
|
-
### While Working
|
|
35
|
-
- Emit status every 5 min: \`sl session say <id> "status: <what you're doing>"\`
|
|
36
|
-
- Lock files before editing: \`sl session say <id> "lock: <file> - <intent>"\`
|
|
37
|
-
- Unlock when done: \`sl session say <id> "unlock: <file> - done"\`
|
|
38
|
-
- Post findings: \`sl session say <id> "finding: [P2] <title> in <file>:<line>"\`
|
|
39
|
-
|
|
40
|
-
### On Problems
|
|
41
|
-
- Unexpected file change? Ask: \`sl session say <id> "help: unexpected change in <file>"\`
|
|
42
|
-
- Need another agent's work? Request: \`sl session say <id> "handoff: @<agent> <description>"\`
|
|
43
|
-
- Found issues for others? Assign: \`sl session say <id> "assign: @<agent> <task>"\`
|
|
30
|
+
### Required Etiquette
|
|
31
|
+
${renderCoordinationBulletList()}
|
|
44
32
|
|
|
45
33
|
### What Not To Do
|
|
46
34
|
- Do not break your autonomous loop on unexpected file changes; ask in session first.
|
package/src/session/store.js
CHANGED
|
@@ -43,6 +43,21 @@ function normalizeNonNegativeInteger(value, fallbackValue = 0) {
|
|
|
43
43
|
return Math.floor(normalized);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function normalizeCreateSessionId(value) {
|
|
47
|
+
const normalized = normalizeString(value);
|
|
48
|
+
if (!normalized) return randomUUID();
|
|
49
|
+
if (
|
|
50
|
+
normalized === "." ||
|
|
51
|
+
normalized === ".." ||
|
|
52
|
+
normalized.includes("/") ||
|
|
53
|
+
normalized.includes("\\") ||
|
|
54
|
+
normalized.includes("..")
|
|
55
|
+
) {
|
|
56
|
+
throw new Error("sessionId must not contain path traversal segments.");
|
|
57
|
+
}
|
|
58
|
+
return normalized;
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
function normalizeIsoTimestamp(value, fallbackIso = new Date().toISOString()) {
|
|
47
62
|
const normalized = normalizeString(value);
|
|
48
63
|
if (!normalized) {
|
|
@@ -148,7 +163,7 @@ function toRelativePosix(baseDir, absolutePath) {
|
|
|
148
163
|
|
|
149
164
|
function normalizeDateKeyFromCloseoutPath(closeoutPath = "", fallbackIso = new Date().toISOString()) {
|
|
150
165
|
const normalized = toPosixPath(closeoutPath);
|
|
151
|
-
const match = /\/observability\/(\d{4}-\d{2}-\d{2})\//.exec(
|
|
166
|
+
const match = /\/observability\/(\d{4}-\d{2}-\d{2})\//.exec("/" + normalized);
|
|
152
167
|
if (match) {
|
|
153
168
|
return match[1];
|
|
154
169
|
}
|
|
@@ -330,6 +345,7 @@ function normalizeMetadata(raw = {}, { sessionId, targetPath, nowIso } = {}) {
|
|
|
330
345
|
createdAt,
|
|
331
346
|
updatedAt: normalizeIsoTimestamp(raw.updatedAt, nowIso),
|
|
332
347
|
expiresAt,
|
|
348
|
+
title: normalizeString(raw.title) || null,
|
|
333
349
|
ttlSeconds,
|
|
334
350
|
renewalCount: Math.max(0, Number(raw.renewalCount || 0)),
|
|
335
351
|
maxLifetimeSeconds: normalizePositiveInteger(raw.maxLifetimeSeconds, MAX_SESSION_LIFETIME_SECONDS),
|
|
@@ -364,7 +380,10 @@ function buildSessionPayload(metadata, paths, nowIso = new Date().toISOString())
|
|
|
364
380
|
metadataPath: paths.metadataPath,
|
|
365
381
|
streamPath: paths.streamPath,
|
|
366
382
|
createdAt: metadata.createdAt,
|
|
383
|
+
updatedAt: metadata.updatedAt,
|
|
367
384
|
expiresAt: metadata.expiresAt,
|
|
385
|
+
lastInteractionAt: metadata.lastInteractionAt,
|
|
386
|
+
title: metadata.title,
|
|
368
387
|
elapsedTimer: buildElapsedTimer(metadata.createdAt, nowIso),
|
|
369
388
|
renewalCount: metadata.renewalCount,
|
|
370
389
|
status: metadata.status,
|
|
@@ -406,11 +425,22 @@ export async function createSession({
|
|
|
406
425
|
targetPath = process.cwd(),
|
|
407
426
|
ttlSeconds = DEFAULT_TTL_SECONDS,
|
|
408
427
|
template = null,
|
|
428
|
+
sessionId: requestedSessionId = "",
|
|
429
|
+
title = "",
|
|
430
|
+
createdAt = "",
|
|
431
|
+
expiresAt = "",
|
|
432
|
+
lastInteractionAt = "",
|
|
409
433
|
} = {}) {
|
|
410
434
|
const resolvedTargetPath = path.resolve(String(targetPath || "."));
|
|
411
435
|
const normalizedTtlSeconds = normalizePositiveInteger(ttlSeconds, DEFAULT_TTL_SECONDS);
|
|
412
|
-
const sessionId =
|
|
436
|
+
const sessionId = normalizeCreateSessionId(requestedSessionId);
|
|
413
437
|
const nowIso = new Date().toISOString();
|
|
438
|
+
const createdIso = normalizeIsoTimestamp(createdAt, nowIso);
|
|
439
|
+
const expiresIso = normalizeIsoTimestamp(
|
|
440
|
+
expiresAt,
|
|
441
|
+
toIsoAfterSeconds(createdIso, normalizedTtlSeconds)
|
|
442
|
+
);
|
|
443
|
+
const interactionIso = normalizeIsoTimestamp(lastInteractionAt, createdIso);
|
|
414
444
|
const paths = resolveSessionPaths(sessionId, { targetPath: resolvedTargetPath });
|
|
415
445
|
const codebaseContext = await collectSessionCodebaseContext(resolvedTargetPath);
|
|
416
446
|
|
|
@@ -419,14 +449,15 @@ export async function createSession({
|
|
|
419
449
|
schemaVersion: SESSION_SCHEMA_VERSION,
|
|
420
450
|
sessionId,
|
|
421
451
|
targetPath: resolvedTargetPath,
|
|
422
|
-
createdAt:
|
|
452
|
+
createdAt: createdIso,
|
|
423
453
|
updatedAt: nowIso,
|
|
424
|
-
expiresAt:
|
|
454
|
+
expiresAt: expiresIso,
|
|
455
|
+
title: normalizeString(title) || null,
|
|
425
456
|
ttlSeconds: normalizedTtlSeconds,
|
|
426
457
|
renewalCount: 0,
|
|
427
458
|
maxLifetimeSeconds: MAX_SESSION_LIFETIME_SECONDS,
|
|
428
459
|
status: SESSION_STATUS_ACTIVE,
|
|
429
|
-
lastInteractionAt:
|
|
460
|
+
lastInteractionAt: interactionIso,
|
|
430
461
|
expiredAt: null,
|
|
431
462
|
archivedAt: null,
|
|
432
463
|
s3Path: null,
|
|
@@ -449,6 +480,24 @@ export async function createSession({
|
|
|
449
480
|
return buildSessionPayload(metadata, paths, nowIso);
|
|
450
481
|
}
|
|
451
482
|
|
|
483
|
+
export async function updateSessionTitle(
|
|
484
|
+
sessionId,
|
|
485
|
+
{ targetPath = process.cwd(), title = "" } = {}
|
|
486
|
+
) {
|
|
487
|
+
const loaded = await loadMetadata(sessionId, { targetPath });
|
|
488
|
+
if (!loaded) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
const nowIso = new Date().toISOString();
|
|
492
|
+
const metadata = {
|
|
493
|
+
...loaded.metadata,
|
|
494
|
+
title: normalizeString(title) || null,
|
|
495
|
+
updatedAt: nowIso,
|
|
496
|
+
};
|
|
497
|
+
const saved = await saveMetadata(metadata, loaded.paths);
|
|
498
|
+
return buildSessionPayload(saved, loaded.paths, nowIso);
|
|
499
|
+
}
|
|
500
|
+
|
|
452
501
|
export async function getSession(sessionId, { targetPath = process.cwd() } = {}) {
|
|
453
502
|
const loaded = await loadMetadata(sessionId, { targetPath });
|
|
454
503
|
if (!loaded) {
|
package/src/spec/generator.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
+
import { getCoordinationEtiquetteItems } from "../session/coordination-guidance.js";
|
|
3
4
|
import { getDefaultTemplate, getTemplateById } from "./templates.js";
|
|
4
5
|
|
|
5
6
|
const VALID_PROJECT_TYPES = new Set(["greenfield", "add_feature", "bugfix"]);
|
|
@@ -494,7 +495,11 @@ function shouldIncludeCoordinationPhase({
|
|
|
494
495
|
description = "",
|
|
495
496
|
agentsMarkdown = "",
|
|
496
497
|
sessionActive = false,
|
|
498
|
+
sessionToolsAvailable = true,
|
|
497
499
|
} = {}) {
|
|
500
|
+
if (sessionToolsAvailable === true) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
498
503
|
if (sessionActive === true) {
|
|
499
504
|
return true;
|
|
500
505
|
}
|
|
@@ -507,16 +512,7 @@ function shouldIncludeCoordinationPhase({
|
|
|
507
512
|
function buildCoordinationPhase(phaseNumber, previousPhaseTitle = "") {
|
|
508
513
|
return {
|
|
509
514
|
title: `Phase ${phaseNumber}: Multi-Agent Coordination Protocol`,
|
|
510
|
-
items:
|
|
511
|
-
"Check for active sessions: `sl session list`.",
|
|
512
|
-
"If a session exists, join it: `sl session join <id> --name <your-name> --role coder`.",
|
|
513
|
-
"Emit status updates every 5 minutes: `sl session say <id> \"status: <what you're doing>\"`.",
|
|
514
|
-
"Before modifying a shared file, check recent session activity for that file.",
|
|
515
|
-
"On unexpected file changes, ask in-session instead of stopping: `sl session say <id> \"help: <question>\"`.",
|
|
516
|
-
"Post findings in-session: `sl session say <id> \"finding: [P2] <title> in <file>:<line>\"`.",
|
|
517
|
-
"On completion, update `tasks/todo.md` and emit completion status in-session.",
|
|
518
|
-
"Leave the session when done: `sl session leave <id>`.",
|
|
519
|
-
],
|
|
515
|
+
items: getCoordinationEtiquetteItems(),
|
|
520
516
|
dependencies: previousPhaseTitle ? [previousPhaseTitle] : [],
|
|
521
517
|
effort: "4-8 hours",
|
|
522
518
|
acceptanceCriteria: [
|
|
@@ -535,6 +531,7 @@ export function generateSpecMarkdown({
|
|
|
535
531
|
projectType,
|
|
536
532
|
agentsMarkdown = "",
|
|
537
533
|
sessionActive = false,
|
|
534
|
+
sessionToolsAvailable = true,
|
|
538
535
|
generatedAt = new Date().toISOString(),
|
|
539
536
|
} = {}) {
|
|
540
537
|
const resolvedTemplate = template || getDefaultTemplate();
|
|
@@ -566,6 +563,7 @@ export function generateSpecMarkdown({
|
|
|
566
563
|
description,
|
|
567
564
|
agentsMarkdown,
|
|
568
565
|
sessionActive,
|
|
566
|
+
sessionToolsAvailable,
|
|
569
567
|
})
|
|
570
568
|
) {
|
|
571
569
|
phases.push(buildCoordinationPhase(phases.length + 1, phases[phases.length - 1]?.title || ""));
|
package/src/swarm/registry.js
CHANGED
|
@@ -204,6 +204,26 @@ const BUILTIN_SWARM_AGENTS = Object.freeze([
|
|
|
204
204
|
evidenceRequirements: ["dependency_refs", "version_risks"],
|
|
205
205
|
escalationTargets: ["security", "release"],
|
|
206
206
|
},
|
|
207
|
+
{
|
|
208
|
+
id: "devtestbot",
|
|
209
|
+
persona: "AIdenID devTestBot",
|
|
210
|
+
role: "specialist",
|
|
211
|
+
domain: "Browser/System E2E",
|
|
212
|
+
tools: ["devtestbot.run_session"],
|
|
213
|
+
permissionMode: "runtime-readonly",
|
|
214
|
+
maxTurns: 8,
|
|
215
|
+
confidenceFloor: 0.8,
|
|
216
|
+
allowedPaths: ["."],
|
|
217
|
+
networkMode: "enabled",
|
|
218
|
+
defaultBudget: {
|
|
219
|
+
maxCostUsd: 1.5,
|
|
220
|
+
maxOutputTokens: 6000,
|
|
221
|
+
maxRuntimeMs: 600000,
|
|
222
|
+
maxToolCalls: 40,
|
|
223
|
+
},
|
|
224
|
+
evidenceRequirements: ["artifact_path", "runtime_evidence", "reproduction", "confidence"],
|
|
225
|
+
escalationTargets: ["testing", "frontend", "reliability"],
|
|
226
|
+
},
|
|
207
227
|
{
|
|
208
228
|
id: "frontend",
|
|
209
229
|
persona: "Jules Tanaka",
|
package/src/swarm/runtime.js
CHANGED
|
@@ -10,6 +10,13 @@ function normalizeString(value) {
|
|
|
10
10
|
return String(value || "").trim();
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function sanitizeRuntimeError(error) {
|
|
14
|
+
return String(error?.message || error || "Runtime failed.")
|
|
15
|
+
.replace(/\b(?:authorization|cookie|token|secret|password|otp|reset)\s*[:=]\s*["']?[^"'\s&]+/gi, (match) =>
|
|
16
|
+
match.replace(/[:=]\s*["']?.*$/u, "=[REDACTED]")
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
function formatTimestampToken() {
|
|
14
21
|
const now = new Date();
|
|
15
22
|
const pad = (value) => String(value).padStart(2, "0");
|
|
@@ -298,6 +305,9 @@ export async function runSwarmRuntime({
|
|
|
298
305
|
execute = false,
|
|
299
306
|
maxSteps = 20,
|
|
300
307
|
startUrl = "about:blank",
|
|
308
|
+
identityId = "",
|
|
309
|
+
devTestBotScope = "",
|
|
310
|
+
devTestBotRunSession = null,
|
|
301
311
|
playbookActions = [],
|
|
302
312
|
outputDir = "",
|
|
303
313
|
env,
|
|
@@ -321,6 +331,9 @@ export async function runSwarmRuntime({
|
|
|
321
331
|
const runtimeRunDirectory = path.join(resolvedOutputRoot, "swarms", runId);
|
|
322
332
|
const runStartedAt = Date.now();
|
|
323
333
|
const events = [];
|
|
334
|
+
const findings = [];
|
|
335
|
+
const artifactBundles = [];
|
|
336
|
+
const devTestBotRuns = [];
|
|
324
337
|
let step = 0;
|
|
325
338
|
|
|
326
339
|
const usage = {
|
|
@@ -409,7 +422,128 @@ export async function runSwarmRuntime({
|
|
|
409
422
|
})
|
|
410
423
|
);
|
|
411
424
|
|
|
412
|
-
if (
|
|
425
|
+
if (assignment.agentId === "devtestbot") {
|
|
426
|
+
const scope = normalizeString(devTestBotScope || plan.scenario || "smoke") || "smoke";
|
|
427
|
+
const toolInput = {
|
|
428
|
+
scope,
|
|
429
|
+
identityId: normalizeString(identityId),
|
|
430
|
+
baseUrl: normalizeString(startUrl),
|
|
431
|
+
recordVideo: Boolean(execute),
|
|
432
|
+
execute: Boolean(execute),
|
|
433
|
+
targetPath: normalizedTargetPath,
|
|
434
|
+
outputRoot: resolvedOutputRoot,
|
|
435
|
+
outputDir: path.join(runtimeRunDirectory, "devtestbot", assignment.assignmentId),
|
|
436
|
+
runId: `${runId}-${assignment.assignmentId}`,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
usage.toolCalls += 1;
|
|
440
|
+
usage.outputTokens += estimateTokens(`devtestbot.run_session:${scope}:${Boolean(execute)}`);
|
|
441
|
+
step += 1;
|
|
442
|
+
events.push(
|
|
443
|
+
createEvent({
|
|
444
|
+
runId,
|
|
445
|
+
step,
|
|
446
|
+
eventType: "tool_call",
|
|
447
|
+
agentId: assignment.agentId,
|
|
448
|
+
message: "devtestbot.run_session started",
|
|
449
|
+
metadata: {
|
|
450
|
+
tool: "devtestbot.run_session",
|
|
451
|
+
scope,
|
|
452
|
+
identityId: toolInput.identityId || null,
|
|
453
|
+
baseUrl: toolInput.baseUrl,
|
|
454
|
+
execute: toolInput.execute,
|
|
455
|
+
recordVideo: toolInput.recordVideo,
|
|
456
|
+
},
|
|
457
|
+
usage,
|
|
458
|
+
})
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const runner = devTestBotRunSession || (await import("../agents/devtestbot/tool.js")).runDevTestBotSession;
|
|
463
|
+
const result = await runner(toolInput, {
|
|
464
|
+
targetPath: normalizedTargetPath,
|
|
465
|
+
outputRoot: resolvedOutputRoot,
|
|
466
|
+
runId: toolInput.runId,
|
|
467
|
+
execute: Boolean(execute),
|
|
468
|
+
env,
|
|
469
|
+
});
|
|
470
|
+
const resultFindings = Array.isArray(result.findings) ? result.findings : [];
|
|
471
|
+
findings.push(...resultFindings);
|
|
472
|
+
if (result.artifactBundle) {
|
|
473
|
+
artifactBundles.push(result.artifactBundle);
|
|
474
|
+
}
|
|
475
|
+
devTestBotRuns.push({
|
|
476
|
+
assignmentId: assignment.assignmentId,
|
|
477
|
+
runId: result.runId || toolInput.runId,
|
|
478
|
+
completed: Boolean(result.completed),
|
|
479
|
+
dryRun: Boolean(result.dryRun),
|
|
480
|
+
findingCount: resultFindings.length,
|
|
481
|
+
artifactBundle: result.artifactBundle || null,
|
|
482
|
+
});
|
|
483
|
+
usage.outputTokens += estimateTokens(
|
|
484
|
+
JSON.stringify({
|
|
485
|
+
findingCount: resultFindings.length,
|
|
486
|
+
artifactBundle: result.artifactBundle ? "present" : "missing",
|
|
487
|
+
})
|
|
488
|
+
);
|
|
489
|
+
step += 1;
|
|
490
|
+
events.push(
|
|
491
|
+
createEvent({
|
|
492
|
+
runId,
|
|
493
|
+
step,
|
|
494
|
+
eventType: "tool_result",
|
|
495
|
+
agentId: assignment.agentId,
|
|
496
|
+
message: "devtestbot.run_session completed",
|
|
497
|
+
metadata: {
|
|
498
|
+
tool: "devtestbot.run_session",
|
|
499
|
+
success: true,
|
|
500
|
+
dryRun: Boolean(result.dryRun),
|
|
501
|
+
findingCount: resultFindings.length,
|
|
502
|
+
artifactBundle: result.artifactBundle || null,
|
|
503
|
+
},
|
|
504
|
+
usage,
|
|
505
|
+
})
|
|
506
|
+
);
|
|
507
|
+
for (const finding of resultFindings) {
|
|
508
|
+
step += 1;
|
|
509
|
+
events.push(
|
|
510
|
+
createEvent({
|
|
511
|
+
runId,
|
|
512
|
+
step,
|
|
513
|
+
eventType: "finding",
|
|
514
|
+
agentId: assignment.agentId,
|
|
515
|
+
message: normalizeString(finding.title || "devTestBot finding"),
|
|
516
|
+
metadata: {
|
|
517
|
+
finding,
|
|
518
|
+
},
|
|
519
|
+
usage,
|
|
520
|
+
})
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
} catch (error) {
|
|
524
|
+
stop = {
|
|
525
|
+
stopClass: error?.code || "DEVTESTBOT_RUN_FAILED",
|
|
526
|
+
reason: sanitizeRuntimeError(error),
|
|
527
|
+
blocking: true,
|
|
528
|
+
};
|
|
529
|
+
step += 1;
|
|
530
|
+
events.push(
|
|
531
|
+
createEvent({
|
|
532
|
+
runId,
|
|
533
|
+
step,
|
|
534
|
+
eventType: "agent_error",
|
|
535
|
+
agentId: assignment.agentId,
|
|
536
|
+
message: stop.reason,
|
|
537
|
+
metadata: {
|
|
538
|
+
tool: "devtestbot.run_session",
|
|
539
|
+
stopClass: stop.stopClass,
|
|
540
|
+
},
|
|
541
|
+
usage,
|
|
542
|
+
})
|
|
543
|
+
);
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
} else if (normalizedEngine === "mock" || !execute) {
|
|
413
547
|
usage.toolCalls += 1;
|
|
414
548
|
usage.outputTokens += estimateTokens(`mock:${assignment.agentId}`);
|
|
415
549
|
step += 1;
|
|
@@ -558,6 +692,10 @@ export async function runSwarmRuntime({
|
|
|
558
692
|
usage,
|
|
559
693
|
eventCount: events.length,
|
|
560
694
|
selectedAgents: Array.isArray(plan.selectedAgents) ? [...plan.selectedAgents] : [],
|
|
695
|
+
findingCount: findings.length,
|
|
696
|
+
findings,
|
|
697
|
+
artifactBundles,
|
|
698
|
+
devTestBotRuns,
|
|
561
699
|
};
|
|
562
700
|
|
|
563
701
|
return writeRuntimeArtifacts({
|