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,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Investor-DD devTestBot phase (PR-E3).
|
|
3
|
+
*
|
|
4
|
+
* Adds a bounded, artifact-producing browser evidence phase to the DD
|
|
5
|
+
* package without making local runs depend on a live browser target. When
|
|
6
|
+
* no approved baseUrl is supplied, devTestBot runs in dry-run mode and
|
|
7
|
+
* writes an explicit evidence-gap artifact bundle.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import fsp from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import crypto from "node:crypto";
|
|
13
|
+
|
|
14
|
+
import { recordProvisionedIdentity } from "../ai/identity-store.js";
|
|
15
|
+
import { runDevTestBotSession } from "../agents/devtestbot/tool.js";
|
|
16
|
+
import { checkBudget } from "./investor-dd-file-loop.js";
|
|
17
|
+
|
|
18
|
+
export const DEVTESTBOT_PHASE_MAX_CONCURRENT = 4;
|
|
19
|
+
export const DEVTESTBOT_PHASE_DEFAULT_SCOPE = "smoke";
|
|
20
|
+
export const DEVTESTBOT_PHASE_DEFAULT_SWARMS = 1;
|
|
21
|
+
export const DEVTESTBOT_PHASE_DEFAULT_PER_SWARM_BUDGET_USD = 0.25;
|
|
22
|
+
|
|
23
|
+
async function writeJson(filePath, obj) {
|
|
24
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
25
|
+
await fsp.writeFile(filePath, JSON.stringify(obj, null, 2), "utf-8");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeString(value) {
|
|
29
|
+
return String(value || "").trim();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeScope(value) {
|
|
33
|
+
const normalized = normalizeString(value).toLowerCase().replace(/\s+/g, "-");
|
|
34
|
+
return normalized || DEVTESTBOT_PHASE_DEFAULT_SCOPE;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function clampInt(value, { min = 0, max = DEVTESTBOT_PHASE_MAX_CONCURRENT, fallback = 0 } = {}) {
|
|
38
|
+
const parsed = Number(value);
|
|
39
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
40
|
+
return Math.max(min, Math.min(max, Math.floor(parsed)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizePositiveNumber(value, fallback) {
|
|
44
|
+
const parsed = Number(value);
|
|
45
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parsePlannerJson(text) {
|
|
49
|
+
const raw = normalizeString(text);
|
|
50
|
+
if (!raw) return {};
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(raw);
|
|
53
|
+
} catch {
|
|
54
|
+
const match = raw.match(/\{[\s\S]*\}/);
|
|
55
|
+
if (!match) return {};
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(match[0]);
|
|
58
|
+
} catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildPlannerPrompt({ rootPath, files = [], findings = [], budget = {} }) {
|
|
65
|
+
const severityCounts = {};
|
|
66
|
+
for (const finding of findings || []) {
|
|
67
|
+
const severity = normalizeString(finding?.severity) || "UNKNOWN";
|
|
68
|
+
severityCounts[severity] = (severityCounts[severity] || 0) + 1;
|
|
69
|
+
}
|
|
70
|
+
return [
|
|
71
|
+
"You are the investor-DD orchestrator deciding the devTestBot runtime evidence plan.",
|
|
72
|
+
"Return only compact JSON with keys: identityCount, swarmCount, perSwarmBudget, scope.",
|
|
73
|
+
"Constraints: identityCount 0-4, swarmCount 0-4, perSwarmBudget USD, scope smoke|auth|password-reset|full.",
|
|
74
|
+
"Use smoke unless code/findings justify auth or password-reset runtime evidence.",
|
|
75
|
+
`Target path: ${rootPath}`,
|
|
76
|
+
`Files discovered: ${files.length}`,
|
|
77
|
+
`Findings so far: ${findings.length}`,
|
|
78
|
+
`Severity counts: ${JSON.stringify(severityCounts)}`,
|
|
79
|
+
`Remaining DD budget: ${Number(budget?.maxUsd || 0) - Number(budget?.spentUsd || 0)}`,
|
|
80
|
+
].join("\n");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function callPlannerClient({ plannerClient, rootPath, files, findings, budget }) {
|
|
84
|
+
if (!plannerClient) return {};
|
|
85
|
+
const prompt = buildPlannerPrompt({ rootPath, files, findings, budget });
|
|
86
|
+
if (typeof plannerClient.decideDevTestBotPhase === "function") {
|
|
87
|
+
return plannerClient.decideDevTestBotPhase({ rootPath, files, findings, budget, prompt });
|
|
88
|
+
}
|
|
89
|
+
if (typeof plannerClient.invoke === "function") {
|
|
90
|
+
const response = await plannerClient.invoke({ prompt, stream: false });
|
|
91
|
+
return parsePlannerJson(response?.text || response);
|
|
92
|
+
}
|
|
93
|
+
if (typeof plannerClient.generatePlan === "function") {
|
|
94
|
+
const response = await plannerClient.generatePlan([{ role: "user", content: prompt }], {
|
|
95
|
+
phase: "devtestbot",
|
|
96
|
+
});
|
|
97
|
+
return parsePlannerJson(response?.text || response?.content || response);
|
|
98
|
+
}
|
|
99
|
+
return {};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function chooseScope({ requestedScope, files = [], findings = [], plannedScope }) {
|
|
103
|
+
if (requestedScope) return normalizeScope(requestedScope);
|
|
104
|
+
if (plannedScope) return normalizeScope(plannedScope);
|
|
105
|
+
const combined = [
|
|
106
|
+
...files,
|
|
107
|
+
...findings.map((finding) => `${finding?.kind || ""} ${finding?.title || ""} ${finding?.evidence || ""}`),
|
|
108
|
+
]
|
|
109
|
+
.join("\n")
|
|
110
|
+
.toLowerCase();
|
|
111
|
+
if (/password|reset|otp|magic-link/.test(combined)) return "password-reset";
|
|
112
|
+
if (/auth|login|signup|session/.test(combined)) return "auth";
|
|
113
|
+
return DEVTESTBOT_PHASE_DEFAULT_SCOPE;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function remainingBudgetUsd(budget) {
|
|
117
|
+
const maxUsd = Number(budget?.maxUsd);
|
|
118
|
+
const spentUsd = Number(budget?.spentUsd || 0);
|
|
119
|
+
if (!Number.isFinite(maxUsd)) return Infinity;
|
|
120
|
+
return Math.max(0, maxUsd - spentUsd);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function normalizePhaseOptions(options = {}) {
|
|
124
|
+
const source = options && typeof options === "object" ? options : {};
|
|
125
|
+
return {
|
|
126
|
+
enabled: source.enabled !== false,
|
|
127
|
+
baseUrl: normalizeString(source.baseUrl),
|
|
128
|
+
scope: normalizeString(source.scope),
|
|
129
|
+
identityCount: source.identityCount,
|
|
130
|
+
swarmCount: source.swarmCount,
|
|
131
|
+
perSwarmBudget: source.perSwarmBudget,
|
|
132
|
+
execute: source.execute,
|
|
133
|
+
recordVideo: source.recordVideo,
|
|
134
|
+
plannerClient: source.plannerClient || null,
|
|
135
|
+
runner: source.runner || null,
|
|
136
|
+
provisionIdentity: source.provisionIdentity || null,
|
|
137
|
+
maxConcurrentAgents: source.maxConcurrentAgents,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function planDevTestBotPhase({
|
|
142
|
+
rootPath,
|
|
143
|
+
files = [],
|
|
144
|
+
findings = [],
|
|
145
|
+
budget = null,
|
|
146
|
+
options = {},
|
|
147
|
+
} = {}) {
|
|
148
|
+
const normalized = normalizePhaseOptions(options);
|
|
149
|
+
if (!normalized.enabled) {
|
|
150
|
+
return {
|
|
151
|
+
enabled: false,
|
|
152
|
+
reason: "disabled",
|
|
153
|
+
identityCount: 0,
|
|
154
|
+
swarmCount: 0,
|
|
155
|
+
perSwarmBudget: 0,
|
|
156
|
+
scope: DEVTESTBOT_PHASE_DEFAULT_SCOPE,
|
|
157
|
+
scopes: [],
|
|
158
|
+
execute: false,
|
|
159
|
+
baseUrl: "",
|
|
160
|
+
maxConcurrentAgents: 0,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const budgetCheck = checkBudget(budget);
|
|
165
|
+
if (!budgetCheck.ok) {
|
|
166
|
+
return {
|
|
167
|
+
enabled: false,
|
|
168
|
+
reason: budgetCheck.reason,
|
|
169
|
+
identityCount: 0,
|
|
170
|
+
swarmCount: 0,
|
|
171
|
+
perSwarmBudget: 0,
|
|
172
|
+
scope: DEVTESTBOT_PHASE_DEFAULT_SCOPE,
|
|
173
|
+
scopes: [],
|
|
174
|
+
execute: false,
|
|
175
|
+
baseUrl: normalized.baseUrl,
|
|
176
|
+
maxConcurrentAgents: 0,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let planned = {};
|
|
181
|
+
try {
|
|
182
|
+
planned = await callPlannerClient({
|
|
183
|
+
plannerClient: normalized.plannerClient,
|
|
184
|
+
rootPath,
|
|
185
|
+
files,
|
|
186
|
+
findings,
|
|
187
|
+
budget,
|
|
188
|
+
});
|
|
189
|
+
} catch {
|
|
190
|
+
planned = {};
|
|
191
|
+
}
|
|
192
|
+
const swarmCount = clampInt(normalized.swarmCount ?? planned.swarmCount, {
|
|
193
|
+
min: 1,
|
|
194
|
+
max: DEVTESTBOT_PHASE_MAX_CONCURRENT,
|
|
195
|
+
fallback: DEVTESTBOT_PHASE_DEFAULT_SWARMS,
|
|
196
|
+
});
|
|
197
|
+
const identityCount = clampInt(normalized.identityCount ?? planned.identityCount, {
|
|
198
|
+
min: swarmCount > 0 ? 1 : 0,
|
|
199
|
+
max: DEVTESTBOT_PHASE_MAX_CONCURRENT,
|
|
200
|
+
fallback: swarmCount,
|
|
201
|
+
});
|
|
202
|
+
const scope = chooseScope({
|
|
203
|
+
requestedScope: normalized.scope,
|
|
204
|
+
plannedScope: planned.scope,
|
|
205
|
+
files,
|
|
206
|
+
findings,
|
|
207
|
+
});
|
|
208
|
+
const remaining = remainingBudgetUsd(budget);
|
|
209
|
+
const plannedPerSwarmBudget = normalizePositiveNumber(
|
|
210
|
+
normalized.perSwarmBudget ?? planned.perSwarmBudget,
|
|
211
|
+
DEVTESTBOT_PHASE_DEFAULT_PER_SWARM_BUDGET_USD,
|
|
212
|
+
);
|
|
213
|
+
const perSwarmBudget = Number.isFinite(remaining)
|
|
214
|
+
? Math.min(plannedPerSwarmBudget, remaining / Math.max(1, swarmCount))
|
|
215
|
+
: plannedPerSwarmBudget;
|
|
216
|
+
const scopes = Array.from({ length: swarmCount }, (_, index) => {
|
|
217
|
+
const plannedScopes = Array.isArray(planned.scopes) ? planned.scopes : [];
|
|
218
|
+
return normalizeScope(plannedScopes[index] || scope);
|
|
219
|
+
});
|
|
220
|
+
const execute = Boolean(normalized.baseUrl) && normalized.execute !== false;
|
|
221
|
+
const maxConcurrentAgents = clampInt(normalized.maxConcurrentAgents, {
|
|
222
|
+
min: 1,
|
|
223
|
+
max: DEVTESTBOT_PHASE_MAX_CONCURRENT,
|
|
224
|
+
fallback: Math.min(swarmCount, DEVTESTBOT_PHASE_MAX_CONCURRENT),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
enabled: swarmCount > 0 && identityCount > 0 && perSwarmBudget >= 0,
|
|
229
|
+
reason: "",
|
|
230
|
+
identityCount,
|
|
231
|
+
swarmCount,
|
|
232
|
+
perSwarmBudget,
|
|
233
|
+
scope,
|
|
234
|
+
scopes,
|
|
235
|
+
execute,
|
|
236
|
+
baseUrl: normalized.baseUrl,
|
|
237
|
+
recordVideo: normalized.recordVideo !== false,
|
|
238
|
+
maxConcurrentAgents,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function makeSyntheticIdentityResponse({ runId, index }) {
|
|
243
|
+
const suffix = crypto.randomUUID().slice(0, 8);
|
|
244
|
+
return {
|
|
245
|
+
id: `aidenid-devtestbot-${runId}-${index + 1}-${suffix}`,
|
|
246
|
+
emailAddress: `devtestbot+${suffix}@aidenid.local`,
|
|
247
|
+
status: "ACTIVE",
|
|
248
|
+
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
249
|
+
tags: ["investor-dd", "devtestbot", runId],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export async function provisionDevTestBotIdentities({
|
|
254
|
+
outputRoot,
|
|
255
|
+
runId,
|
|
256
|
+
count,
|
|
257
|
+
provisionIdentity = null,
|
|
258
|
+
onEvent = () => {},
|
|
259
|
+
} = {}) {
|
|
260
|
+
if (!outputRoot) throw new TypeError("provisionDevTestBotIdentities requires outputRoot");
|
|
261
|
+
if (!runId) throw new TypeError("provisionDevTestBotIdentities requires runId");
|
|
262
|
+
const total = clampInt(count, { min: 0, max: DEVTESTBOT_PHASE_MAX_CONCURRENT, fallback: 0 });
|
|
263
|
+
const identities = [];
|
|
264
|
+
|
|
265
|
+
for (let index = 0; index < total; index += 1) {
|
|
266
|
+
const subagentId = `devtestbot-${index + 1}`;
|
|
267
|
+
const idempotencyKey = `${runId}:${subagentId}`;
|
|
268
|
+
let response = null;
|
|
269
|
+
try {
|
|
270
|
+
response = typeof provisionIdentity === "function"
|
|
271
|
+
? await provisionIdentity({ runId, index, subagentId, idempotencyKey })
|
|
272
|
+
: makeSyntheticIdentityResponse({ runId, index });
|
|
273
|
+
} catch (error) {
|
|
274
|
+
onEvent({
|
|
275
|
+
type: "devtestbot_identity_error",
|
|
276
|
+
phase: "devtestbot",
|
|
277
|
+
agentId: subagentId,
|
|
278
|
+
error: safeErrorMessage(error),
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (!normalizeString(response?.id)) {
|
|
282
|
+
response = makeSyntheticIdentityResponse({ runId, index });
|
|
283
|
+
}
|
|
284
|
+
const recorded = await recordProvisionedIdentity({
|
|
285
|
+
outputRoot,
|
|
286
|
+
response,
|
|
287
|
+
context: {
|
|
288
|
+
source: "investor-dd-devtestbot",
|
|
289
|
+
idempotencyKey,
|
|
290
|
+
tags: ["investor-dd", "devtestbot", subagentId, runId],
|
|
291
|
+
eventBudget: 1,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
const identity = recorded.identity || {};
|
|
295
|
+
identities.push({
|
|
296
|
+
subagentId,
|
|
297
|
+
identityId: identity.identityId,
|
|
298
|
+
status: identity.status,
|
|
299
|
+
registryPath: recorded.registryPath,
|
|
300
|
+
});
|
|
301
|
+
onEvent({
|
|
302
|
+
type: "devtestbot_identity_ready",
|
|
303
|
+
phase: "devtestbot",
|
|
304
|
+
agentId: subagentId,
|
|
305
|
+
identityId: identity.identityId,
|
|
306
|
+
status: identity.status,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return identities;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function runWithConcurrency(items, maxConcurrent, worker) {
|
|
313
|
+
const results = new Array(items.length);
|
|
314
|
+
let cursor = 0;
|
|
315
|
+
async function runWorker() {
|
|
316
|
+
while (cursor < items.length) {
|
|
317
|
+
const index = cursor;
|
|
318
|
+
cursor += 1;
|
|
319
|
+
results[index] = await worker(items[index], index);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const workers = [];
|
|
323
|
+
const concurrency = Math.max(1, Math.min(Number(maxConcurrent || 1), items.length || 1));
|
|
324
|
+
for (let index = 0; index < concurrency; index += 1) {
|
|
325
|
+
workers.push(runWorker());
|
|
326
|
+
}
|
|
327
|
+
await Promise.all(workers);
|
|
328
|
+
return results;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function summarizeResultForPackage({ subagentId, identity, result }) {
|
|
332
|
+
return {
|
|
333
|
+
subagentId,
|
|
334
|
+
identityId: identity?.identityId || "",
|
|
335
|
+
scope: result?.scope || "",
|
|
336
|
+
completed: Boolean(result?.completed),
|
|
337
|
+
dryRun: Boolean(result?.dryRun),
|
|
338
|
+
findingCount: Number(result?.findingCount || 0),
|
|
339
|
+
artifactBundle: result?.artifactBundle || null,
|
|
340
|
+
laneSummaries: result?.laneSummaries || {},
|
|
341
|
+
resultPath: result?.artifactBundle?.resultPath || "",
|
|
342
|
+
findingsPath: result?.artifactBundle?.findingsPath || "",
|
|
343
|
+
eventsPath: result?.artifactBundle?.eventsPath || "",
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function decorateFinding(finding, { subagentId, identityId, artifactBundle }) {
|
|
348
|
+
return {
|
|
349
|
+
...finding,
|
|
350
|
+
personaId: finding.personaId || "devtestbot",
|
|
351
|
+
tool: finding.tool || "devtestbot.run_session",
|
|
352
|
+
source: finding.source || "devtestbot",
|
|
353
|
+
agentId: subagentId,
|
|
354
|
+
identityId,
|
|
355
|
+
artifactBundlePath: artifactBundle?.root || "",
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function safeErrorMessage(error) {
|
|
360
|
+
return String(error?.message || error || "devTestBot subagent failed")
|
|
361
|
+
.replace(/(authorization|cookie|token|secret|password|otp|reset[-_ ]?link)\s*[:=]\s*[^"'\s]+/gi, "$1=[REDACTED]")
|
|
362
|
+
.slice(0, 500);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function runDevTestBotPhase({
|
|
366
|
+
runId,
|
|
367
|
+
rootPath,
|
|
368
|
+
outputRoot,
|
|
369
|
+
runRoot,
|
|
370
|
+
artifactDir,
|
|
371
|
+
files = [],
|
|
372
|
+
findings = [],
|
|
373
|
+
budget = null,
|
|
374
|
+
options = {},
|
|
375
|
+
onEvent = () => {},
|
|
376
|
+
} = {}) {
|
|
377
|
+
if (!runId) throw new TypeError("runDevTestBotPhase requires runId");
|
|
378
|
+
if (!rootPath) throw new TypeError("runDevTestBotPhase requires rootPath");
|
|
379
|
+
if (!outputRoot) throw new TypeError("runDevTestBotPhase requires outputRoot");
|
|
380
|
+
if (!runRoot) throw new TypeError("runDevTestBotPhase requires runRoot");
|
|
381
|
+
if (!artifactDir) throw new TypeError("runDevTestBotPhase requires artifactDir");
|
|
382
|
+
|
|
383
|
+
const normalized = normalizePhaseOptions(options);
|
|
384
|
+
const phaseStartedAt = Date.now();
|
|
385
|
+
const plan = await planDevTestBotPhase({
|
|
386
|
+
rootPath,
|
|
387
|
+
files,
|
|
388
|
+
findings,
|
|
389
|
+
budget,
|
|
390
|
+
options: normalized,
|
|
391
|
+
});
|
|
392
|
+
const phaseRoot = path.join(runRoot, "devtestbot");
|
|
393
|
+
await fsp.mkdir(phaseRoot, { recursive: true });
|
|
394
|
+
|
|
395
|
+
onEvent({
|
|
396
|
+
type: "phase_start",
|
|
397
|
+
phase: "devtestbot",
|
|
398
|
+
identityCount: plan.identityCount,
|
|
399
|
+
swarmCount: plan.swarmCount,
|
|
400
|
+
execute: plan.execute,
|
|
401
|
+
});
|
|
402
|
+
onEvent({
|
|
403
|
+
type: "devtestbot_start",
|
|
404
|
+
phase: "devtestbot",
|
|
405
|
+
plan: {
|
|
406
|
+
enabled: plan.enabled,
|
|
407
|
+
reason: plan.reason,
|
|
408
|
+
identityCount: plan.identityCount,
|
|
409
|
+
swarmCount: plan.swarmCount,
|
|
410
|
+
perSwarmBudget: plan.perSwarmBudget,
|
|
411
|
+
scope: plan.scope,
|
|
412
|
+
scopes: plan.scopes,
|
|
413
|
+
execute: plan.execute,
|
|
414
|
+
baseUrlProvided: Boolean(plan.baseUrl),
|
|
415
|
+
maxConcurrentAgents: plan.maxConcurrentAgents,
|
|
416
|
+
},
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
if (!plan.enabled) {
|
|
420
|
+
const skipped = {
|
|
421
|
+
runId,
|
|
422
|
+
phase: "devtestbot",
|
|
423
|
+
skipped: true,
|
|
424
|
+
reason: plan.reason || "disabled",
|
|
425
|
+
plan,
|
|
426
|
+
identities: [],
|
|
427
|
+
subagents: [],
|
|
428
|
+
findings: [],
|
|
429
|
+
artifactRoot: phaseRoot,
|
|
430
|
+
};
|
|
431
|
+
await writeJson(path.join(artifactDir, "devtestbot-summary.json"), skipped);
|
|
432
|
+
onEvent({
|
|
433
|
+
type: "devtestbot_complete",
|
|
434
|
+
phase: "devtestbot",
|
|
435
|
+
skipped: true,
|
|
436
|
+
reason: skipped.reason,
|
|
437
|
+
findingCount: 0,
|
|
438
|
+
});
|
|
439
|
+
onEvent({ type: "phase_complete", phase: "devtestbot", skipped: true, findingCount: 0 });
|
|
440
|
+
return skipped;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const identities = await provisionDevTestBotIdentities({
|
|
444
|
+
outputRoot,
|
|
445
|
+
runId,
|
|
446
|
+
count: plan.identityCount,
|
|
447
|
+
provisionIdentity: normalized.provisionIdentity,
|
|
448
|
+
onEvent,
|
|
449
|
+
});
|
|
450
|
+
const runner = normalized.runner || runDevTestBotSession;
|
|
451
|
+
const subagents = Array.from({ length: plan.swarmCount }, (_, index) => {
|
|
452
|
+
const subagentId = `devtestbot-${index + 1}`;
|
|
453
|
+
return {
|
|
454
|
+
subagentId,
|
|
455
|
+
scope: plan.scopes[index] || plan.scope,
|
|
456
|
+
identity: identities[index % identities.length],
|
|
457
|
+
outputDir: path.join(phaseRoot, subagentId),
|
|
458
|
+
budgetUsd: plan.perSwarmBudget,
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const packageFindings = [];
|
|
463
|
+
const subagentResults = await runWithConcurrency(
|
|
464
|
+
subagents,
|
|
465
|
+
plan.maxConcurrentAgents,
|
|
466
|
+
async (assignment) => {
|
|
467
|
+
const budgetCheck = checkBudget(budget);
|
|
468
|
+
if (!budgetCheck.ok) {
|
|
469
|
+
onEvent({
|
|
470
|
+
type: "devtestbot_agent_error",
|
|
471
|
+
phase: "devtestbot",
|
|
472
|
+
agentId: assignment.subagentId,
|
|
473
|
+
reason: budgetCheck.reason,
|
|
474
|
+
});
|
|
475
|
+
return {
|
|
476
|
+
subagentId: assignment.subagentId,
|
|
477
|
+
identityId: assignment.identity?.identityId || "",
|
|
478
|
+
scope: assignment.scope,
|
|
479
|
+
completed: false,
|
|
480
|
+
error: budgetCheck.reason,
|
|
481
|
+
findings: [],
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
onEvent({
|
|
486
|
+
type: "devtestbot_agent_start",
|
|
487
|
+
phase: "devtestbot",
|
|
488
|
+
agentId: assignment.subagentId,
|
|
489
|
+
identityId: assignment.identity?.identityId || "",
|
|
490
|
+
scope: assignment.scope,
|
|
491
|
+
execute: plan.execute,
|
|
492
|
+
});
|
|
493
|
+
try {
|
|
494
|
+
const result = await runner({
|
|
495
|
+
runId,
|
|
496
|
+
targetPath: rootPath,
|
|
497
|
+
outputRoot,
|
|
498
|
+
outputDir: assignment.outputDir,
|
|
499
|
+
scope: assignment.scope,
|
|
500
|
+
identityId: assignment.identity?.identityId || "",
|
|
501
|
+
baseUrl: plan.baseUrl,
|
|
502
|
+
execute: plan.execute,
|
|
503
|
+
recordVideo: plan.recordVideo,
|
|
504
|
+
}, {
|
|
505
|
+
targetPath: rootPath,
|
|
506
|
+
outputRoot,
|
|
507
|
+
runId,
|
|
508
|
+
outputDir: assignment.outputDir,
|
|
509
|
+
scope: assignment.scope,
|
|
510
|
+
identityId: assignment.identity?.identityId || "",
|
|
511
|
+
baseUrl: plan.baseUrl,
|
|
512
|
+
execute: plan.execute,
|
|
513
|
+
onEvent: (event) => {
|
|
514
|
+
onEvent({
|
|
515
|
+
type: "devtestbot_agent_event",
|
|
516
|
+
phase: "devtestbot",
|
|
517
|
+
agentId: assignment.subagentId,
|
|
518
|
+
event: event?.event || event?.type || "",
|
|
519
|
+
});
|
|
520
|
+
},
|
|
521
|
+
});
|
|
522
|
+
if (budget && Number.isFinite(budget.maxUsd)) {
|
|
523
|
+
budget.spentUsd += assignment.budgetUsd;
|
|
524
|
+
}
|
|
525
|
+
const decorated = (result.findings || []).map((finding) =>
|
|
526
|
+
decorateFinding(finding, {
|
|
527
|
+
subagentId: assignment.subagentId,
|
|
528
|
+
identityId: assignment.identity?.identityId || "",
|
|
529
|
+
artifactBundle: result.artifactBundle,
|
|
530
|
+
})
|
|
531
|
+
);
|
|
532
|
+
packageFindings.push(...decorated);
|
|
533
|
+
onEvent({
|
|
534
|
+
type: "devtestbot_agent_complete",
|
|
535
|
+
phase: "devtestbot",
|
|
536
|
+
agentId: assignment.subagentId,
|
|
537
|
+
findingCount: decorated.length,
|
|
538
|
+
artifactRoot: result.artifactBundle?.root || assignment.outputDir,
|
|
539
|
+
});
|
|
540
|
+
return {
|
|
541
|
+
...summarizeResultForPackage({
|
|
542
|
+
subagentId: assignment.subagentId,
|
|
543
|
+
identity: assignment.identity,
|
|
544
|
+
result,
|
|
545
|
+
}),
|
|
546
|
+
findings: decorated,
|
|
547
|
+
};
|
|
548
|
+
} catch (error) {
|
|
549
|
+
const safeMessage = safeErrorMessage(error);
|
|
550
|
+
onEvent({
|
|
551
|
+
type: "devtestbot_agent_error",
|
|
552
|
+
phase: "devtestbot",
|
|
553
|
+
agentId: assignment.subagentId,
|
|
554
|
+
error: safeMessage,
|
|
555
|
+
});
|
|
556
|
+
return {
|
|
557
|
+
subagentId: assignment.subagentId,
|
|
558
|
+
identityId: assignment.identity?.identityId || "",
|
|
559
|
+
scope: assignment.scope,
|
|
560
|
+
completed: false,
|
|
561
|
+
error: safeMessage,
|
|
562
|
+
findings: [],
|
|
563
|
+
artifactBundle: { root: assignment.outputDir },
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
const summary = {
|
|
570
|
+
runId,
|
|
571
|
+
phase: "devtestbot",
|
|
572
|
+
skipped: false,
|
|
573
|
+
artifactRoot: phaseRoot,
|
|
574
|
+
generatedAt: new Date().toISOString(),
|
|
575
|
+
durationSeconds: (Date.now() - phaseStartedAt) / 1000,
|
|
576
|
+
plan: {
|
|
577
|
+
...plan,
|
|
578
|
+
baseUrl: plan.baseUrl ? "[configured]" : "",
|
|
579
|
+
},
|
|
580
|
+
identities,
|
|
581
|
+
subagents: subagentResults,
|
|
582
|
+
findingCount: packageFindings.length,
|
|
583
|
+
findings: packageFindings,
|
|
584
|
+
};
|
|
585
|
+
await writeJson(path.join(artifactDir, "devtestbot-summary.json"), summary);
|
|
586
|
+
onEvent({
|
|
587
|
+
type: "devtestbot_complete",
|
|
588
|
+
phase: "devtestbot",
|
|
589
|
+
findingCount: packageFindings.length,
|
|
590
|
+
artifactRoot: phaseRoot,
|
|
591
|
+
});
|
|
592
|
+
onEvent({
|
|
593
|
+
type: "phase_complete",
|
|
594
|
+
phase: "devtestbot",
|
|
595
|
+
findingCount: packageFindings.length,
|
|
596
|
+
artifactRoot: phaseRoot,
|
|
597
|
+
});
|
|
598
|
+
return summary;
|
|
599
|
+
}
|