@whisperr/wizard 0.6.0 → 0.7.1
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/dist/index.js +2198 -166
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import { realpathSync } from "fs";
|
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
6
|
|
|
7
7
|
// src/cli.ts
|
|
8
|
-
import { resolve as
|
|
9
|
-
import * as
|
|
8
|
+
import { resolve as resolve3 } from "path";
|
|
9
|
+
import * as p2 from "@clack/prompts";
|
|
10
10
|
import open2 from "open";
|
|
11
11
|
|
|
12
12
|
// src/core/config.ts
|
|
@@ -940,7 +940,7 @@ var ALL_PLAYBOOKS = [
|
|
|
940
940
|
swiftPlaybook
|
|
941
941
|
];
|
|
942
942
|
function playbookByTargetId(id) {
|
|
943
|
-
return ALL_PLAYBOOKS.find((
|
|
943
|
+
return ALL_PLAYBOOKS.find((p3) => p3.target.id === id);
|
|
944
944
|
}
|
|
945
945
|
|
|
946
946
|
// src/core/detect.ts
|
|
@@ -982,7 +982,7 @@ function makeDetectContext(repoPath) {
|
|
|
982
982
|
async function detectStack(repoPath) {
|
|
983
983
|
const ctx = makeDetectContext(repoPath);
|
|
984
984
|
const results = await Promise.all(
|
|
985
|
-
ALL_PLAYBOOKS.map((
|
|
985
|
+
ALL_PLAYBOOKS.map((p3) => p3.detect(ctx).catch(() => null))
|
|
986
986
|
);
|
|
987
987
|
return results.filter((d) => d !== null).sort((a, b) => b.confidence - a.confidence);
|
|
988
988
|
}
|
|
@@ -1181,10 +1181,1660 @@ function mockManifest(appId, config) {
|
|
|
1181
1181
|
properties: [{ name: "reason", description: "stated cancel reason" }],
|
|
1182
1182
|
interventions: [{ code: "win_back", label: "Win-back", weight: 1 }]
|
|
1183
1183
|
}
|
|
1184
|
-
],
|
|
1185
|
-
supportedTargets: ["flutter", "web-js", "nextjs", "react-native", "swift"]
|
|
1184
|
+
],
|
|
1185
|
+
supportedTargets: ["flutter", "web-js", "nextjs", "react-native", "swift"]
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// src/engine/cli.ts
|
|
1190
|
+
import * as p from "@clack/prompts";
|
|
1191
|
+
|
|
1192
|
+
// src/core/report.ts
|
|
1193
|
+
function scrubSummary(text) {
|
|
1194
|
+
return scrubTerminalControls(text).replace(/sk-ant-[A-Za-z0-9_-]+/g, "[redacted]").replace(/sk-[A-Za-z0-9]{16,}/g, "[redacted]").replace(/AKIA[0-9A-Z]{16}/g, "[redacted]").replace(/Bearer\s+[A-Za-z0-9._-]+/g, "[redacted]").replace(
|
|
1195
|
+
/\b(ANTHROPIC_(?:AUTH_TOKEN|API_KEY))\s*=\s*["']?[^"'\s]+["']?/gi,
|
|
1196
|
+
"$1=[redacted]"
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
async function postRunReport(config, session, report) {
|
|
1200
|
+
if (config.offline) return { ok: true };
|
|
1201
|
+
try {
|
|
1202
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/report`, {
|
|
1203
|
+
method: "POST",
|
|
1204
|
+
headers: {
|
|
1205
|
+
"Content-Type": "application/json",
|
|
1206
|
+
Authorization: `Bearer ${session.token}`
|
|
1207
|
+
},
|
|
1208
|
+
body: JSON.stringify(report)
|
|
1209
|
+
});
|
|
1210
|
+
if (res.ok) return { ok: true };
|
|
1211
|
+
return {
|
|
1212
|
+
ok: false,
|
|
1213
|
+
status: res.status,
|
|
1214
|
+
detail: await responseDetail(res)
|
|
1215
|
+
};
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
return { ok: false, detail: detailSlice(errorMessage(err)) };
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
async function responseDetail(res) {
|
|
1221
|
+
try {
|
|
1222
|
+
return detailSlice(await res.text());
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
return detailSlice(errorMessage(err));
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
function errorMessage(err) {
|
|
1228
|
+
return err instanceof Error ? err.message : String(err);
|
|
1229
|
+
}
|
|
1230
|
+
function detailSlice(detail) {
|
|
1231
|
+
const sliced = detail.slice(0, 200).trim();
|
|
1232
|
+
return sliced || void 0;
|
|
1233
|
+
}
|
|
1234
|
+
function scrubTerminalControls(value) {
|
|
1235
|
+
return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/core/verify.ts
|
|
1239
|
+
async function pollFirstEvent(config, session, opts = {}) {
|
|
1240
|
+
if (config.offline) return { received: false };
|
|
1241
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
1242
|
+
const intervalMs = opts.intervalMs ?? 3e3;
|
|
1243
|
+
const deadline = Date.now() + timeoutMs;
|
|
1244
|
+
while (Date.now() < deadline) {
|
|
1245
|
+
if (opts.signal?.aborted) return { received: false };
|
|
1246
|
+
try {
|
|
1247
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/first-event`, {
|
|
1248
|
+
headers: { authorization: `Bearer ${session.token}` }
|
|
1249
|
+
});
|
|
1250
|
+
if (res.ok) {
|
|
1251
|
+
const body = await res.json();
|
|
1252
|
+
if (body.received) {
|
|
1253
|
+
return { received: true, eventType: body.event_type };
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
} catch {
|
|
1257
|
+
}
|
|
1258
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1259
|
+
}
|
|
1260
|
+
return { received: false };
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// src/engine/output.ts
|
|
1264
|
+
function formatProgressLine(event) {
|
|
1265
|
+
const parts = [event.phase];
|
|
1266
|
+
if (event.cluster) {
|
|
1267
|
+
parts.push(`cluster ${event.cluster.index}/${event.cluster.total}`);
|
|
1268
|
+
}
|
|
1269
|
+
if (event.file) {
|
|
1270
|
+
parts.push(event.file);
|
|
1271
|
+
}
|
|
1272
|
+
parts.push(event.action);
|
|
1273
|
+
return parts.join(" \xB7 ");
|
|
1274
|
+
}
|
|
1275
|
+
function createProgressSink(options) {
|
|
1276
|
+
const interval = options.heartbeatIntervalMs ?? 15e3;
|
|
1277
|
+
const now = options.now ?? (() => Date.now());
|
|
1278
|
+
let lastLineAt = 0;
|
|
1279
|
+
let lastLine = "";
|
|
1280
|
+
const writeLine = (line) => {
|
|
1281
|
+
lastLineAt = now();
|
|
1282
|
+
lastLine = line;
|
|
1283
|
+
options.write(line);
|
|
1284
|
+
};
|
|
1285
|
+
return {
|
|
1286
|
+
emit(event) {
|
|
1287
|
+
if (options.json) {
|
|
1288
|
+
writeLine(JSON.stringify({ kind: "progress", ...event }));
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const line = formatProgressLine(event);
|
|
1292
|
+
if (!options.isTTY && line === lastLine) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
writeLine(line);
|
|
1296
|
+
},
|
|
1297
|
+
heartbeat(phase, action) {
|
|
1298
|
+
if (now() - lastLineAt < interval) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (options.json) {
|
|
1302
|
+
writeLine(JSON.stringify({ kind: "heartbeat", phase, action }));
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
writeLine(`${phase} \xB7 still working \xB7 ${action}`);
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// src/engine/orchestrator.ts
|
|
1311
|
+
import { basename, join as join6 } from "path";
|
|
1312
|
+
|
|
1313
|
+
// src/engine/clusters.ts
|
|
1314
|
+
import { createHash } from "crypto";
|
|
1315
|
+
import { dirname } from "path";
|
|
1316
|
+
var MIN_CLUSTER_EVENTS = 3;
|
|
1317
|
+
var MAX_CLUSTER_EVENTS = 8;
|
|
1318
|
+
function clusterIdFor(files) {
|
|
1319
|
+
const digest = createHash("sha256").update([...files].sort().join("\n")).digest("hex");
|
|
1320
|
+
return `cluster_${digest.slice(0, 12)}`;
|
|
1321
|
+
}
|
|
1322
|
+
function deriveClusters(placements) {
|
|
1323
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1324
|
+
for (const placement of placements) {
|
|
1325
|
+
const existing = byFile.get(placement.file) ?? [];
|
|
1326
|
+
existing.push(placement);
|
|
1327
|
+
byFile.set(placement.file, existing);
|
|
1328
|
+
}
|
|
1329
|
+
const byDirectory = /* @__PURE__ */ new Map();
|
|
1330
|
+
for (const [file, events] of byFile) {
|
|
1331
|
+
const directory = dirname(file);
|
|
1332
|
+
const bucket = byDirectory.get(directory) ?? { files: /* @__PURE__ */ new Set(), events: [] };
|
|
1333
|
+
bucket.files.add(file);
|
|
1334
|
+
bucket.events.push(...events);
|
|
1335
|
+
byDirectory.set(directory, bucket);
|
|
1336
|
+
}
|
|
1337
|
+
const clusters = [];
|
|
1338
|
+
const pending = [];
|
|
1339
|
+
for (const directory of [...byDirectory.keys()].sort()) {
|
|
1340
|
+
const bucket = byDirectory.get(directory);
|
|
1341
|
+
if (bucket.events.length >= MIN_CLUSTER_EVENTS) {
|
|
1342
|
+
pending.push(bucket);
|
|
1343
|
+
continue;
|
|
1344
|
+
}
|
|
1345
|
+
const last = pending[pending.length - 1];
|
|
1346
|
+
if (last && last.events.length + bucket.events.length <= MAX_CLUSTER_EVENTS) {
|
|
1347
|
+
for (const file of bucket.files) last.files.add(file);
|
|
1348
|
+
last.events.push(...bucket.events);
|
|
1349
|
+
} else {
|
|
1350
|
+
pending.push(bucket);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
for (const bucket of pending) {
|
|
1354
|
+
const events = [...bucket.events].sort((a, b) => a.eventCode.localeCompare(b.eventCode));
|
|
1355
|
+
for (let start = 0; start < events.length; start += MAX_CLUSTER_EVENTS) {
|
|
1356
|
+
const slice = events.slice(start, start + MAX_CLUSTER_EVENTS);
|
|
1357
|
+
const files = [...new Set(slice.map((event) => event.file))].sort();
|
|
1358
|
+
clusters.push({
|
|
1359
|
+
id: clusterIdFor(files.length > 0 ? files : [`slice_${start}`]),
|
|
1360
|
+
files,
|
|
1361
|
+
events: slice,
|
|
1362
|
+
status: "pending",
|
|
1363
|
+
attempts: 0
|
|
1364
|
+
});
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return clusters;
|
|
1368
|
+
}
|
|
1369
|
+
var TERMINAL_STATUSES = ["reviewed", "failed_resumable", "blocked"];
|
|
1370
|
+
function nextPendingCluster(clusters) {
|
|
1371
|
+
return clusters.find((cluster) => cluster.status === "pending" || cluster.status === "editing");
|
|
1372
|
+
}
|
|
1373
|
+
function clustersComplete(clusters) {
|
|
1374
|
+
return clusters.every((cluster) => TERMINAL_STATUSES.includes(cluster.status));
|
|
1375
|
+
}
|
|
1376
|
+
function transitionCluster(clusters, clusterId, status, note3) {
|
|
1377
|
+
return clusters.map((cluster) => {
|
|
1378
|
+
if (cluster.id !== clusterId) {
|
|
1379
|
+
return cluster;
|
|
1380
|
+
}
|
|
1381
|
+
const attempts = status === "editing" ? cluster.attempts + 1 : cluster.attempts;
|
|
1382
|
+
return { ...cluster, status, attempts, note: note3 ?? cluster.note };
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// src/engine/integrate.ts
|
|
1387
|
+
import { writeFile } from "fs/promises";
|
|
1388
|
+
import { join as join3 } from "path";
|
|
1389
|
+
|
|
1390
|
+
// src/engine/bindings.ts
|
|
1391
|
+
function bindingsTargetFor(target9) {
|
|
1392
|
+
switch (target9) {
|
|
1393
|
+
case "swift":
|
|
1394
|
+
return "swift";
|
|
1395
|
+
case "flutter":
|
|
1396
|
+
return "dart";
|
|
1397
|
+
default:
|
|
1398
|
+
return "typescript";
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function camelCase(code) {
|
|
1402
|
+
return code.split("_").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1403
|
+
}
|
|
1404
|
+
function generateBindings(target9, events) {
|
|
1405
|
+
switch (bindingsTargetFor(target9)) {
|
|
1406
|
+
case "swift":
|
|
1407
|
+
return generateSwift(events);
|
|
1408
|
+
case "dart":
|
|
1409
|
+
return generateDart(events);
|
|
1410
|
+
default:
|
|
1411
|
+
return generateTypeScript(events);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function generateTypeScript(events) {
|
|
1415
|
+
const lines = [
|
|
1416
|
+
"// Generated by @whisperr/wizard \u2014 do not edit by hand.",
|
|
1417
|
+
"// Call bindWhisperr(client) once right after the Whisperr SDK is initialized;",
|
|
1418
|
+
"// events tracked before binding are queued, never dropped.",
|
|
1419
|
+
"",
|
|
1420
|
+
"type WhisperrPayloadValue = string | number | boolean;",
|
|
1421
|
+
"type WhisperrClientLike = {",
|
|
1422
|
+
" track: (event: string, payload?: Record<string, WhisperrPayloadValue>) => unknown;",
|
|
1423
|
+
"};",
|
|
1424
|
+
"",
|
|
1425
|
+
"let boundClient: WhisperrClientLike | null = null;",
|
|
1426
|
+
"const queued: Array<[string, Record<string, WhisperrPayloadValue> | undefined]> = [];",
|
|
1427
|
+
"",
|
|
1428
|
+
"export function bindWhisperr(client: WhisperrClientLike): void {",
|
|
1429
|
+
" boundClient = client;",
|
|
1430
|
+
" while (queued.length > 0) {",
|
|
1431
|
+
" const [event, payload] = queued.shift()!;",
|
|
1432
|
+
" try { client.track(event, payload); } catch { /* analytics must never break the app */ }",
|
|
1433
|
+
" }",
|
|
1434
|
+
"}",
|
|
1435
|
+
"",
|
|
1436
|
+
"function emit(event: string, payload?: Record<string, WhisperrPayloadValue>): void {",
|
|
1437
|
+
" if (boundClient) {",
|
|
1438
|
+
" try { boundClient.track(event, payload); } catch { /* analytics must never break the app */ }",
|
|
1439
|
+
" } else {",
|
|
1440
|
+
" queued.push([event, payload]);",
|
|
1441
|
+
" }",
|
|
1442
|
+
"}",
|
|
1443
|
+
"",
|
|
1444
|
+
"export const WhisperrEvents = {"
|
|
1445
|
+
];
|
|
1446
|
+
for (const event of events) {
|
|
1447
|
+
const fields = Object.keys(event.payloadSchema);
|
|
1448
|
+
for (const field of fields) {
|
|
1449
|
+
lines.push(` /** ${field}: ${event.payloadSchema[field]} */`);
|
|
1450
|
+
}
|
|
1451
|
+
if (fields.length === 0) {
|
|
1452
|
+
lines.push(` ${camelCase(event.code)}(): void {`, ` emit("${event.code}");`, " },");
|
|
1453
|
+
} else {
|
|
1454
|
+
const params = fields.map((field) => `${camelCase(field)}: WhisperrPayloadValue`).join("; ");
|
|
1455
|
+
const entries = fields.map((field) => `"${field}": ${camelCase(field)}`).join(", ");
|
|
1456
|
+
lines.push(
|
|
1457
|
+
` ${camelCase(event.code)}(payload: { ${params} }): void {`,
|
|
1458
|
+
` const { ${fields.map((field) => camelCase(field)).join(", ")} } = payload;`,
|
|
1459
|
+
` emit("${event.code}", { ${entries} });`,
|
|
1460
|
+
" },"
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
lines.push("};", "");
|
|
1465
|
+
return {
|
|
1466
|
+
relativePath: "whisperr-events.ts",
|
|
1467
|
+
content: lines.join("\n"),
|
|
1468
|
+
bindInstruction: "Import { bindWhisperr } from the generated whisperr-events module and call bindWhisperr(client) once, immediately after the Whisperr SDK client is created at app startup."
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
function generateSwift(events) {
|
|
1472
|
+
const lines = [
|
|
1473
|
+
"// Generated by @whisperr/wizard \u2014 do not edit by hand.",
|
|
1474
|
+
"// Call WhisperrEvents.bind(track:) once right after the Whisperr SDK is ready;",
|
|
1475
|
+
"// events tracked before binding are queued, never dropped.",
|
|
1476
|
+
"",
|
|
1477
|
+
"import Foundation",
|
|
1478
|
+
"",
|
|
1479
|
+
"enum WhisperrEvents {",
|
|
1480
|
+
" typealias TrackFunction = (String, [String: String]) -> Void",
|
|
1481
|
+
" private static var boundTrack: TrackFunction?",
|
|
1482
|
+
" private static var queued: [(String, [String: String])] = []",
|
|
1483
|
+
" private static let lock = NSLock()",
|
|
1484
|
+
"",
|
|
1485
|
+
" static func bind(track: @escaping TrackFunction) {",
|
|
1486
|
+
" lock.lock()",
|
|
1487
|
+
" boundTrack = track",
|
|
1488
|
+
" let pending = queued",
|
|
1489
|
+
" queued.removeAll()",
|
|
1490
|
+
" lock.unlock()",
|
|
1491
|
+
" for (event, payload) in pending { track(event, payload) }",
|
|
1492
|
+
" }",
|
|
1493
|
+
"",
|
|
1494
|
+
" private static func emit(_ event: String, _ payload: [String: String] = [:]) {",
|
|
1495
|
+
" lock.lock()",
|
|
1496
|
+
" if let track = boundTrack {",
|
|
1497
|
+
" lock.unlock()",
|
|
1498
|
+
" track(event, payload)",
|
|
1499
|
+
" } else {",
|
|
1500
|
+
" queued.append((event, payload))",
|
|
1501
|
+
" lock.unlock()",
|
|
1502
|
+
" }",
|
|
1503
|
+
" }",
|
|
1504
|
+
""
|
|
1505
|
+
];
|
|
1506
|
+
for (const event of events) {
|
|
1507
|
+
const fields = Object.keys(event.payloadSchema);
|
|
1508
|
+
for (const field of fields) {
|
|
1509
|
+
lines.push(` /// ${field}: ${event.payloadSchema[field]}`);
|
|
1510
|
+
}
|
|
1511
|
+
if (fields.length === 0) {
|
|
1512
|
+
lines.push(` static func ${camelCase(event.code)}() {`, ` emit("${event.code}")`, " }", "");
|
|
1513
|
+
} else {
|
|
1514
|
+
const params = fields.map((field) => `${camelCase(field)}: String`).join(", ");
|
|
1515
|
+
const entries = fields.map((field) => `"${field}": ${camelCase(field)}`).join(", ");
|
|
1516
|
+
lines.push(
|
|
1517
|
+
` static func ${camelCase(event.code)}(${params}) {`,
|
|
1518
|
+
` emit("${event.code}", [${entries}])`,
|
|
1519
|
+
" }",
|
|
1520
|
+
""
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
lines.push("}", "");
|
|
1525
|
+
return {
|
|
1526
|
+
relativePath: "WhisperrEvents.swift",
|
|
1527
|
+
content: lines.join("\n"),
|
|
1528
|
+
bindInstruction: "Call WhisperrEvents.bind(track:) once, immediately after Whisperr SDK initialization completes, forwarding to the SDK's track call. String payload values only."
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function generateDart(events) {
|
|
1532
|
+
const lines = [
|
|
1533
|
+
"// Generated by @whisperr/wizard \u2014 do not edit by hand.",
|
|
1534
|
+
"// Call WhisperrEvents.bind(track) once right after the Whisperr SDK is ready;",
|
|
1535
|
+
"// events tracked before binding are queued, never dropped.",
|
|
1536
|
+
"",
|
|
1537
|
+
"typedef WhisperrTrack = void Function(String event, Map<String, Object> payload);",
|
|
1538
|
+
"",
|
|
1539
|
+
"class WhisperrEvents {",
|
|
1540
|
+
" static WhisperrTrack? _track;",
|
|
1541
|
+
" static final List<MapEntry<String, Map<String, Object>>> _queued = [];",
|
|
1542
|
+
"",
|
|
1543
|
+
" static void bind(WhisperrTrack track) {",
|
|
1544
|
+
" _track = track;",
|
|
1545
|
+
" for (final entry in _queued) {",
|
|
1546
|
+
" track(entry.key, entry.value);",
|
|
1547
|
+
" }",
|
|
1548
|
+
" _queued.clear();",
|
|
1549
|
+
" }",
|
|
1550
|
+
"",
|
|
1551
|
+
" static void _emit(String event, [Map<String, Object> payload = const {}]) {",
|
|
1552
|
+
" final track = _track;",
|
|
1553
|
+
" if (track != null) {",
|
|
1554
|
+
" track(event, payload);",
|
|
1555
|
+
" } else {",
|
|
1556
|
+
" _queued.add(MapEntry(event, payload));",
|
|
1557
|
+
" }",
|
|
1558
|
+
" }",
|
|
1559
|
+
""
|
|
1560
|
+
];
|
|
1561
|
+
for (const event of events) {
|
|
1562
|
+
const fields = Object.keys(event.payloadSchema);
|
|
1563
|
+
for (const field of fields) {
|
|
1564
|
+
lines.push(` /// ${field}: ${event.payloadSchema[field]}`);
|
|
1565
|
+
}
|
|
1566
|
+
if (fields.length === 0) {
|
|
1567
|
+
lines.push(` static void ${camelCase(event.code)}() => _emit('${event.code}');`, "");
|
|
1568
|
+
} else {
|
|
1569
|
+
const params = fields.map((field) => `required Object ${camelCase(field)}`).join(", ");
|
|
1570
|
+
const entries = fields.map((field) => `'${field}': ${camelCase(field)}`).join(", ");
|
|
1571
|
+
lines.push(
|
|
1572
|
+
` static void ${camelCase(event.code)}({${params}}) =>`,
|
|
1573
|
+
` _emit('${event.code}', {${entries}});`,
|
|
1574
|
+
""
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
lines.push("}", "");
|
|
1579
|
+
return {
|
|
1580
|
+
relativePath: "whisperr_events.dart",
|
|
1581
|
+
content: lines.join("\n"),
|
|
1582
|
+
bindInstruction: "Call WhisperrEvents.bind(track) once, immediately after the Whisperr SDK is initialized, forwarding to the SDK's track call."
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/engine/budgets.ts
|
|
1587
|
+
var OVERFLOW_PATTERNS = [
|
|
1588
|
+
/prompt is too long/i,
|
|
1589
|
+
/context.{0,20}(length|window|limit).{0,20}exceed/i,
|
|
1590
|
+
/exceeds? .{0,20}context/i,
|
|
1591
|
+
/maximum context/i,
|
|
1592
|
+
/input is too long/i
|
|
1593
|
+
];
|
|
1594
|
+
var TRANSIENT_PATTERNS = [
|
|
1595
|
+
/rate.?limit/i,
|
|
1596
|
+
/overloaded/i,
|
|
1597
|
+
/timeout/i,
|
|
1598
|
+
/timed out/i,
|
|
1599
|
+
/econnreset/i,
|
|
1600
|
+
/socket hang up/i,
|
|
1601
|
+
/5\d\d/,
|
|
1602
|
+
/temporarily unavailable/i
|
|
1603
|
+
];
|
|
1604
|
+
function classifyFailure(message) {
|
|
1605
|
+
const text = message ?? "";
|
|
1606
|
+
if (OVERFLOW_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
1607
|
+
return "context_overflow";
|
|
1608
|
+
}
|
|
1609
|
+
if (TRANSIENT_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
1610
|
+
return "transient";
|
|
1611
|
+
}
|
|
1612
|
+
return "fatal";
|
|
1613
|
+
}
|
|
1614
|
+
var MAX_CLUSTER_ATTEMPTS = 2;
|
|
1615
|
+
function nextClusterStatusAfterFailure(failure, attempts) {
|
|
1616
|
+
if (failure === "transient" && attempts < MAX_CLUSTER_ATTEMPTS) {
|
|
1617
|
+
return "pending";
|
|
1618
|
+
}
|
|
1619
|
+
if (failure === "fatal") {
|
|
1620
|
+
return "blocked";
|
|
1621
|
+
}
|
|
1622
|
+
return "failed_resumable";
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
// src/engine/verify.ts
|
|
1626
|
+
import { spawn } from "child_process";
|
|
1627
|
+
import { readFileSync } from "fs";
|
|
1628
|
+
import { join as join2 } from "path";
|
|
1629
|
+
var JS_TARGETS = /* @__PURE__ */ new Set(["web-js", "nextjs", "node", "react-native"]);
|
|
1630
|
+
function resolveVerifySteps(repoPath, target9) {
|
|
1631
|
+
if (target9 === "flutter") {
|
|
1632
|
+
return [{ kind: "analyze", command: "flutter", args: ["analyze", "--no-pub"] }];
|
|
1633
|
+
}
|
|
1634
|
+
if (JS_TARGETS.has(target9)) {
|
|
1635
|
+
const scripts = packageScripts(repoPath);
|
|
1636
|
+
const steps = [];
|
|
1637
|
+
if (scripts.typecheck) {
|
|
1638
|
+
steps.push({ kind: "typecheck", command: npmCommand(), args: ["run", "typecheck"] });
|
|
1639
|
+
} else if (hasTsconfig(repoPath)) {
|
|
1640
|
+
steps.push({ kind: "typecheck", command: "npx", args: ["--no-install", "tsc", "--noEmit"] });
|
|
1641
|
+
}
|
|
1642
|
+
if (steps.length === 0 && scripts.build) {
|
|
1643
|
+
steps.push({ kind: "build", command: npmCommand(), args: ["run", "build"] });
|
|
1644
|
+
}
|
|
1645
|
+
return steps;
|
|
1646
|
+
}
|
|
1647
|
+
return [];
|
|
1648
|
+
}
|
|
1649
|
+
function packageScripts(repoPath) {
|
|
1650
|
+
try {
|
|
1651
|
+
const parsed = JSON.parse(readFileSync(join2(repoPath, "package.json"), "utf8"));
|
|
1652
|
+
return parsed.scripts ?? {};
|
|
1653
|
+
} catch {
|
|
1654
|
+
return {};
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function hasTsconfig(repoPath) {
|
|
1658
|
+
try {
|
|
1659
|
+
readFileSync(join2(repoPath, "tsconfig.json"), "utf8");
|
|
1660
|
+
return true;
|
|
1661
|
+
} catch {
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
function npmCommand() {
|
|
1666
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
1667
|
+
}
|
|
1668
|
+
function runVerifyStep(cwd, step, timeoutMs = 3e5) {
|
|
1669
|
+
return new Promise((resolve4) => {
|
|
1670
|
+
const child = spawn(step.command, step.args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
1671
|
+
let output = "";
|
|
1672
|
+
const capture = (chunk) => {
|
|
1673
|
+
if (output.length < 2e4) {
|
|
1674
|
+
output += String(chunk);
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
child.stdout.on("data", capture);
|
|
1678
|
+
child.stderr.on("data", capture);
|
|
1679
|
+
const timer = setTimeout(() => {
|
|
1680
|
+
child.kill("SIGKILL");
|
|
1681
|
+
}, timeoutMs);
|
|
1682
|
+
child.on("error", (error) => {
|
|
1683
|
+
clearTimeout(timer);
|
|
1684
|
+
resolve4({ ...step, ok: false, output: String(error) });
|
|
1685
|
+
});
|
|
1686
|
+
child.on("close", (exitCode) => {
|
|
1687
|
+
clearTimeout(timer);
|
|
1688
|
+
resolve4({ ...step, ok: exitCode === 0, output });
|
|
1689
|
+
});
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
async function runVerifySteps(cwd, steps) {
|
|
1693
|
+
const results = [];
|
|
1694
|
+
for (const step of steps) {
|
|
1695
|
+
results.push(await runVerifyStep(cwd, step));
|
|
1696
|
+
}
|
|
1697
|
+
return results;
|
|
1698
|
+
}
|
|
1699
|
+
function verifiedStatus(results) {
|
|
1700
|
+
if (results.length === 0) {
|
|
1701
|
+
return "applied";
|
|
1702
|
+
}
|
|
1703
|
+
if (results.some((result) => !result.ok)) {
|
|
1704
|
+
return "applied";
|
|
1705
|
+
}
|
|
1706
|
+
if (results.some((result) => result.kind === "build")) {
|
|
1707
|
+
return "build_verified";
|
|
1708
|
+
}
|
|
1709
|
+
if (results.some((result) => result.kind === "typecheck" || result.kind === "analyze")) {
|
|
1710
|
+
return "type_verified";
|
|
1711
|
+
}
|
|
1712
|
+
return "syntax_verified";
|
|
1713
|
+
}
|
|
1714
|
+
function runVerificationStatus(results) {
|
|
1715
|
+
if (results.length === 0) {
|
|
1716
|
+
return "unavailable";
|
|
1717
|
+
}
|
|
1718
|
+
return results.every((result) => result.ok) ? "passed" : "failed";
|
|
1719
|
+
}
|
|
1720
|
+
function verificationCommand(results) {
|
|
1721
|
+
const last = results[results.length - 1];
|
|
1722
|
+
return last ? [last.command, ...last.args].join(" ") : void 0;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/engine/integrate.ts
|
|
1726
|
+
var REVIEW_CHECKLIST = [
|
|
1727
|
+
"- Events must fire after the confirmed outcome (post-await/success), never on intent or button tap.",
|
|
1728
|
+
"- View-appearance events must be deduplicated against re-renders and navigation returns.",
|
|
1729
|
+
"- Debounced flows must emit after the debounce commits, not per keystroke/tap.",
|
|
1730
|
+
"- Denied/failed permission or authorization results must not be recorded as granted.",
|
|
1731
|
+
"- Never persist a sent/once marker before the track call actually executed.",
|
|
1732
|
+
"- Payloads must use only the generated wrapper fields \u2014 no extra fields, no personal identifiers."
|
|
1733
|
+
].join("\n");
|
|
1734
|
+
async function writeBindingsModule(worktree, target9, registered) {
|
|
1735
|
+
const bindings = generateBindings(target9, registered);
|
|
1736
|
+
await writeFile(join3(worktree.path, bindings.relativePath), bindings.content);
|
|
1737
|
+
return bindings;
|
|
1738
|
+
}
|
|
1739
|
+
async function runSdkSetupPass(deps, bindings) {
|
|
1740
|
+
deps.sink.emit({ phase: "binding", action: "installing SDK and wiring identify()" });
|
|
1741
|
+
const outcome = await deps.provider.invoke(
|
|
1742
|
+
{
|
|
1743
|
+
role: "edit",
|
|
1744
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
1745
|
+
userPrompt: [
|
|
1746
|
+
"CORE SETUP for the Whisperr SDK in this repository. Do exactly this, then stop:",
|
|
1747
|
+
"1. Add and install the Whisperr SDK dependency for this stack.",
|
|
1748
|
+
`2. Initialize it once at app startup with API key ${deps.ingestion.apiKey} and base URL ${deps.ingestion.baseUrl}.`,
|
|
1749
|
+
`3. The generated bindings module at ${bindings.relativePath} queues events until bound.`,
|
|
1750
|
+
` ${bindings.bindInstruction}`,
|
|
1751
|
+
" You may move the generated file to the idiomatic source directory (fix imports), but never edit its generated content.",
|
|
1752
|
+
"4. Wire identify() for the END USER (paying customer) at login success and session restore; reset() on logout.",
|
|
1753
|
+
" Never instrument admin/staff/operator paths.",
|
|
1754
|
+
"Do not instrument product events yet. Do not run builds."
|
|
1755
|
+
].join("\n"),
|
|
1756
|
+
tools: [],
|
|
1757
|
+
allowRepoWrite: true,
|
|
1758
|
+
cwd: deps.worktree.path
|
|
1759
|
+
},
|
|
1760
|
+
deps.models.edit,
|
|
1761
|
+
(action) => deps.sink.emit({ phase: "binding", action })
|
|
1762
|
+
);
|
|
1763
|
+
return outcome.kind === "completed";
|
|
1764
|
+
}
|
|
1765
|
+
function clusterPrompt(cluster, registered, bindings) {
|
|
1766
|
+
const byCode = new Map(registered.map((event) => [event.code, event]));
|
|
1767
|
+
const briefs = cluster.events.map((placement) => {
|
|
1768
|
+
const event = byCode.get(placement.eventCode);
|
|
1769
|
+
const fields = Object.keys(event?.payloadSchema ?? {});
|
|
1770
|
+
return [
|
|
1771
|
+
`- ${placement.eventCode}: call the generated wrapper from ${bindings.relativePath}`,
|
|
1772
|
+
` anchor: ${placement.file}${placement.symbol ? ` (${placement.symbol})` : ""}`,
|
|
1773
|
+
` why: ${event?.reasoning ?? ""}`,
|
|
1774
|
+
fields.length > 0 ? ` payload fields: ${fields.join(", ")}` : " payload: none"
|
|
1775
|
+
].join("\n");
|
|
1776
|
+
});
|
|
1777
|
+
return [
|
|
1778
|
+
"Instrument EXACTLY these events by calling their generated wrapper functions at the anchors.",
|
|
1779
|
+
"Use only the generated wrappers \u2014 never call the SDK's raw track() and never invent event names.",
|
|
1780
|
+
"",
|
|
1781
|
+
"Placement rules:",
|
|
1782
|
+
REVIEW_CHECKLIST,
|
|
1783
|
+
"",
|
|
1784
|
+
"Events for this cluster:",
|
|
1785
|
+
...briefs,
|
|
1786
|
+
"",
|
|
1787
|
+
"Only edit the listed files (plus imports of the bindings module). Then stop."
|
|
1788
|
+
].join("\n");
|
|
1789
|
+
}
|
|
1790
|
+
async function reviewCluster(deps, cluster, diff) {
|
|
1791
|
+
const outcome = await deps.provider.invoke(
|
|
1792
|
+
{
|
|
1793
|
+
role: "review",
|
|
1794
|
+
systemPrompt: [
|
|
1795
|
+
"You review an instrumentation diff against a strict checklist. Answer with a verdict line",
|
|
1796
|
+
"`VERDICT: pass` or `VERDICT: fail`, then bullet notes for anything wrong."
|
|
1797
|
+
].join("\n"),
|
|
1798
|
+
userPrompt: [
|
|
1799
|
+
"Checklist:",
|
|
1800
|
+
REVIEW_CHECKLIST,
|
|
1801
|
+
"",
|
|
1802
|
+
`Events expected: ${cluster.events.map((event) => event.eventCode).join(", ")}`,
|
|
1803
|
+
"",
|
|
1804
|
+
"Diff:",
|
|
1805
|
+
diff.length > 6e4 ? diff.slice(0, 6e4) + "\n\u2026(truncated)" : diff
|
|
1806
|
+
].join("\n"),
|
|
1807
|
+
tools: [],
|
|
1808
|
+
allowRepoWrite: false,
|
|
1809
|
+
cwd: deps.worktree.path
|
|
1810
|
+
},
|
|
1811
|
+
deps.models.review,
|
|
1812
|
+
(action) => deps.sink.emit({ phase: "reviewing", action })
|
|
1813
|
+
);
|
|
1814
|
+
if (outcome.kind !== "completed") {
|
|
1815
|
+
return { ok: false, notes: `review pass did not complete (${outcome.kind})` };
|
|
1816
|
+
}
|
|
1817
|
+
const ok = /verdict:\s*pass/i.test(outcome.text);
|
|
1818
|
+
return { ok, notes: outcome.text.trim() };
|
|
1819
|
+
}
|
|
1820
|
+
async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
1821
|
+
let clusters = initialClusters;
|
|
1822
|
+
const eventStatuses = /* @__PURE__ */ new Map();
|
|
1823
|
+
const bindings = generateBindings(deps.target, deps.registered);
|
|
1824
|
+
const steps = resolveVerifySteps(deps.worktree.path, deps.target);
|
|
1825
|
+
const total = clusters.length;
|
|
1826
|
+
for (; ; ) {
|
|
1827
|
+
const cluster = nextPendingCluster(clusters);
|
|
1828
|
+
if (!cluster) {
|
|
1829
|
+
break;
|
|
1830
|
+
}
|
|
1831
|
+
const index = clusters.findIndex((candidate) => candidate.id === cluster.id) + 1;
|
|
1832
|
+
clusters = transitionCluster(clusters, cluster.id, "editing");
|
|
1833
|
+
deps.saveClusters(clusters);
|
|
1834
|
+
deps.sink.emit({
|
|
1835
|
+
phase: "integrating",
|
|
1836
|
+
cluster: { index, total },
|
|
1837
|
+
file: cluster.files[0],
|
|
1838
|
+
action: "placing events"
|
|
1839
|
+
});
|
|
1840
|
+
const edit = await deps.provider.invoke(
|
|
1841
|
+
{
|
|
1842
|
+
role: "edit",
|
|
1843
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
1844
|
+
userPrompt: clusterPrompt(cluster, deps.registered, bindings),
|
|
1845
|
+
tools: [],
|
|
1846
|
+
allowRepoWrite: true,
|
|
1847
|
+
cwd: deps.worktree.path
|
|
1848
|
+
},
|
|
1849
|
+
deps.models.edit,
|
|
1850
|
+
(action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
|
|
1851
|
+
);
|
|
1852
|
+
if (edit.kind !== "completed") {
|
|
1853
|
+
const failure = edit.kind === "context_overflow" ? "context_overflow" : edit.kind === "budget_exhausted" ? "context_overflow" : classifyFailure(edit.kind === "failed" ? edit.error : "");
|
|
1854
|
+
const nextStatus = nextClusterStatusAfterFailure(failure, cluster.attempts);
|
|
1855
|
+
clusters = transitionCluster(clusters, cluster.id, nextStatus, `edit ${edit.kind}`);
|
|
1856
|
+
deps.saveClusters(clusters);
|
|
1857
|
+
for (const placement of cluster.events) {
|
|
1858
|
+
eventStatuses.set(placement.eventCode, "failed");
|
|
1859
|
+
}
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
let statuses = "applied";
|
|
1863
|
+
if (steps.length > 0) {
|
|
1864
|
+
deps.sink.emit({ phase: "verifying", cluster: { index, total }, action: "running verifier" });
|
|
1865
|
+
const results = await runVerifySteps(deps.worktree.path, steps);
|
|
1866
|
+
statuses = verifiedStatus(results);
|
|
1867
|
+
if (results.some((result) => !result.ok) && cluster.attempts < MAX_CLUSTER_ATTEMPTS) {
|
|
1868
|
+
deps.sink.emit({ phase: "verifying", cluster: { index, total }, action: "repairing verification failure" });
|
|
1869
|
+
await deps.provider.invoke(
|
|
1870
|
+
{
|
|
1871
|
+
role: "edit",
|
|
1872
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
1873
|
+
userPrompt: [
|
|
1874
|
+
"The verification command failed after your edits. Fix ONLY the failure below, then stop.",
|
|
1875
|
+
"",
|
|
1876
|
+
results.map((result) => result.output).join("\n").slice(0, 2e4)
|
|
1877
|
+
].join("\n"),
|
|
1878
|
+
tools: [],
|
|
1879
|
+
allowRepoWrite: true,
|
|
1880
|
+
cwd: deps.worktree.path
|
|
1881
|
+
},
|
|
1882
|
+
deps.models.edit,
|
|
1883
|
+
(action) => deps.sink.emit({ phase: "verifying", cluster: { index, total }, action })
|
|
1884
|
+
);
|
|
1885
|
+
const retried = await runVerifySteps(deps.worktree.path, steps);
|
|
1886
|
+
statuses = verifiedStatus(retried);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
clusters = transitionCluster(clusters, cluster.id, "verified");
|
|
1890
|
+
deps.saveClusters(clusters);
|
|
1891
|
+
const diff = await clusterDiff(cluster.files);
|
|
1892
|
+
const review = await reviewCluster(deps, cluster, diff);
|
|
1893
|
+
const finalStatus = review.ok ? statuses === "applied" ? "applied" : "reviewed" : statuses;
|
|
1894
|
+
clusters = transitionCluster(clusters, cluster.id, "reviewed", review.ok ? void 0 : review.notes.slice(0, 500));
|
|
1895
|
+
deps.saveClusters(clusters);
|
|
1896
|
+
for (const placement of cluster.events) {
|
|
1897
|
+
eventStatuses.set(placement.eventCode, finalStatus);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (!clustersComplete(clusters)) {
|
|
1901
|
+
throw new Error("cluster queue did not reach a terminal state");
|
|
1902
|
+
}
|
|
1903
|
+
return { clusters, eventStatuses };
|
|
1904
|
+
}
|
|
1905
|
+
async function finalizeIntegration(deps) {
|
|
1906
|
+
const steps = resolveVerifySteps(deps.worktree.path, deps.target);
|
|
1907
|
+
deps.sink.emit({ phase: "verifying", action: "final verification pass" });
|
|
1908
|
+
const results = await runVerifySteps(deps.worktree.path, steps);
|
|
1909
|
+
return {
|
|
1910
|
+
results,
|
|
1911
|
+
status: runVerificationStatus(results),
|
|
1912
|
+
command: verificationCommand(results)
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// src/engine/runs.ts
|
|
1917
|
+
import { createHash as createHash2 } from "crypto";
|
|
1918
|
+
var RunsApiError = class extends Error {
|
|
1919
|
+
constructor(message, status, code) {
|
|
1920
|
+
super(message);
|
|
1921
|
+
this.status = status;
|
|
1922
|
+
this.code = code;
|
|
1923
|
+
this.name = "RunsApiError";
|
|
1924
|
+
}
|
|
1925
|
+
status;
|
|
1926
|
+
code;
|
|
1927
|
+
};
|
|
1928
|
+
function itemIdempotencyKey(kind, code, extra = "") {
|
|
1929
|
+
return createHash2("sha256").update(`${kind}
|
|
1930
|
+
${code}
|
|
1931
|
+
${extra}`).digest("hex").slice(0, 32);
|
|
1932
|
+
}
|
|
1933
|
+
var RunsClient = class {
|
|
1934
|
+
constructor(config, session) {
|
|
1935
|
+
this.config = config;
|
|
1936
|
+
this.session = session;
|
|
1937
|
+
}
|
|
1938
|
+
config;
|
|
1939
|
+
session;
|
|
1940
|
+
async request(method, path, body) {
|
|
1941
|
+
let response;
|
|
1942
|
+
try {
|
|
1943
|
+
response = await fetch(`${this.config.apiBaseUrl}${path}`, {
|
|
1944
|
+
method,
|
|
1945
|
+
headers: {
|
|
1946
|
+
"Content-Type": "application/json",
|
|
1947
|
+
Authorization: `Bearer ${this.session.token}`
|
|
1948
|
+
},
|
|
1949
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1950
|
+
});
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
throw new RunsApiError(error instanceof Error ? error.message : "request failed");
|
|
1953
|
+
}
|
|
1954
|
+
let payload;
|
|
1955
|
+
const text = await response.text();
|
|
1956
|
+
try {
|
|
1957
|
+
payload = text.length > 0 ? JSON.parse(text) : {};
|
|
1958
|
+
} catch {
|
|
1959
|
+
payload = {};
|
|
1960
|
+
}
|
|
1961
|
+
if (!response.ok) {
|
|
1962
|
+
const detail = payload;
|
|
1963
|
+
throw new RunsApiError(
|
|
1964
|
+
detail.message ?? `runs API ${method} ${path} failed`,
|
|
1965
|
+
response.status,
|
|
1966
|
+
detail.code
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
return payload;
|
|
1970
|
+
}
|
|
1971
|
+
createOrResumeRun(input) {
|
|
1972
|
+
return this.request("POST", "/wizard/runs", { ...input }).then(normalizeRunBootstrap);
|
|
1973
|
+
}
|
|
1974
|
+
getRun(runId) {
|
|
1975
|
+
return this.request("GET", `/wizard/runs/${runId}`).then(normalizeRunBootstrap);
|
|
1976
|
+
}
|
|
1977
|
+
patchRun(runId, patch) {
|
|
1978
|
+
return this.request("PATCH", `/wizard/runs/${runId}`, { ...patch });
|
|
1979
|
+
}
|
|
1980
|
+
writeItem(runId, kind, payload, idempotencyExtra = "") {
|
|
1981
|
+
const code = typeof payload.code === "string" ? payload.code : JSON.stringify(payload);
|
|
1982
|
+
return this.request("POST", `/wizard/runs/${runId}/items`, {
|
|
1983
|
+
idempotencyKey: itemIdempotencyKey(kind, code, idempotencyExtra),
|
|
1984
|
+
kind,
|
|
1985
|
+
payload
|
|
1986
|
+
}).then(normalizeItemWriteResult);
|
|
1987
|
+
}
|
|
1988
|
+
completeRun(runId, completion) {
|
|
1989
|
+
return this.request(
|
|
1990
|
+
"POST",
|
|
1991
|
+
`/wizard/runs/${runId}/complete`,
|
|
1992
|
+
{ ...completion }
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
function normalizeItemWriteResult(payload) {
|
|
1997
|
+
const value = record(payload);
|
|
1998
|
+
const item = record(value?.item);
|
|
1999
|
+
const id = stringValue(value?.id) || stringValue(item?.id);
|
|
2000
|
+
if (!value || value.kind !== "event" || typeof value.created !== "boolean" || !id) {
|
|
2001
|
+
throw new RunsApiError("runs API returned an invalid item write response");
|
|
2002
|
+
}
|
|
2003
|
+
return {
|
|
2004
|
+
kind: "event",
|
|
2005
|
+
created: value.created,
|
|
2006
|
+
id,
|
|
2007
|
+
...typeof value.code === "string" ? { code: value.code } : {},
|
|
2008
|
+
...item && typeof item.id === "string" ? { item: { id: item.id, ...typeof item.code === "string" ? { code: item.code } : {} } } : {}
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
function normalizeRunBootstrap(payload) {
|
|
2012
|
+
const value = record(payload);
|
|
2013
|
+
if (!value) {
|
|
2014
|
+
throw new RunsApiError("runs API returned an invalid run bootstrap response");
|
|
2015
|
+
}
|
|
2016
|
+
const snapshotSource = record(value.snapshot) ?? value;
|
|
2017
|
+
const events = Array.isArray(snapshotSource.events) ? snapshotSource.events : void 0;
|
|
2018
|
+
const run3 = record(value.run);
|
|
2019
|
+
const project = record(value.project);
|
|
2020
|
+
const ingestion = record(value.ingestion);
|
|
2021
|
+
if (!run3 || typeof run3.id !== "string" || !project || typeof project.id !== "string" || !events || events.some((event) => !isSnapshotEvent(event)) || !ingestion || typeof ingestion.apiKey !== "string" || typeof ingestion.baseUrl !== "string") {
|
|
2022
|
+
throw new RunsApiError("runs API returned an invalid run bootstrap response");
|
|
2023
|
+
}
|
|
2024
|
+
return {
|
|
2025
|
+
project: {
|
|
2026
|
+
id: project.id,
|
|
2027
|
+
repoFingerprint: stringValue(project.repoFingerprint),
|
|
2028
|
+
displayName: stringValue(project.displayName),
|
|
2029
|
+
target: stringValue(project.target),
|
|
2030
|
+
kind: stringValue(project.kind)
|
|
2031
|
+
},
|
|
2032
|
+
run: { id: run3.id, status: stringValue(run3.status), phase: stringValue(run3.phase) },
|
|
2033
|
+
resumed: value.resumed === true,
|
|
2034
|
+
universeMode: stringValue(value.universeMode),
|
|
2035
|
+
...record(value.onboardingContext) ? { onboardingContext: record(value.onboardingContext) } : {},
|
|
2036
|
+
app: record(value.app) ?? {},
|
|
2037
|
+
snapshot: {
|
|
2038
|
+
setupStatus: stringValue(snapshotSource.setupStatus),
|
|
2039
|
+
events
|
|
2040
|
+
},
|
|
2041
|
+
ingestion: { apiKey: ingestion.apiKey, baseUrl: ingestion.baseUrl }
|
|
2042
|
+
};
|
|
2043
|
+
}
|
|
2044
|
+
function record(value) {
|
|
2045
|
+
return typeof value === "object" && value !== null ? value : void 0;
|
|
2046
|
+
}
|
|
2047
|
+
function stringValue(value) {
|
|
2048
|
+
return typeof value === "string" ? value : "";
|
|
2049
|
+
}
|
|
2050
|
+
function isSnapshotEvent(value) {
|
|
2051
|
+
const event = record(value);
|
|
2052
|
+
return !!event && typeof event.id === "string" && typeof event.code === "string" && typeof event.name === "string" && typeof event.reasoning === "string" && typeof event.projectId === "string" && !!record(event.payloadSchema);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
// src/engine/selection.ts
|
|
2056
|
+
import { z } from "zod";
|
|
2057
|
+
|
|
2058
|
+
// src/engine/prompts.ts
|
|
2059
|
+
var SURVEY_SYSTEM_PROMPT = [
|
|
2060
|
+
"You are the read-only survey pass of the Whisperr integration wizard.",
|
|
2061
|
+
"Map the repository so a later pass can pick the product's important lifecycle events.",
|
|
2062
|
+
"You cannot edit files. Be fast and decisive; read entry points and feature roots, not every file.",
|
|
2063
|
+
"",
|
|
2064
|
+
"Report, in plain markdown:",
|
|
2065
|
+
"1. Stack: frameworks, package manager, app entry point file.",
|
|
2066
|
+
"2. Feature areas: name, root directory, and the 1-3 files where committed user actions happen.",
|
|
2067
|
+
"3. The end-user identify() anchor: the login/signup success and session-restore call sites for the",
|
|
2068
|
+
" PAYING end user \u2014 never admin, staff, operator, or seller paths.",
|
|
2069
|
+
"4. Existing analytics calls, if any.",
|
|
2070
|
+
"5. Committed-action call sites worth instrumenting: file, function/symbol, and what the user just did.",
|
|
2071
|
+
"Cite concrete file paths for every claim. Do not propose event names yet."
|
|
2072
|
+
].join("\n");
|
|
2073
|
+
function surveyUserPrompt(repoPath, onboardingContext) {
|
|
2074
|
+
return [
|
|
2075
|
+
`Survey the repository at ${repoPath}.`,
|
|
2076
|
+
"",
|
|
2077
|
+
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
2078
|
+
"",
|
|
2079
|
+
"Produce the survey report now."
|
|
2080
|
+
].join("\n");
|
|
2081
|
+
}
|
|
2082
|
+
var SELECTION_SYSTEM_PROMPT = [
|
|
2083
|
+
"You are the event-selection pass of the Whisperr integration wizard.",
|
|
2084
|
+
"From the survey report and business context, choose the IMPORTANT product events this codebase",
|
|
2085
|
+
"can actually emit, and propose each one with the propose_event tool.",
|
|
2086
|
+
"",
|
|
2087
|
+
"Rules \u2014 these are hard constraints:",
|
|
2088
|
+
"- Only propose an event with a concrete call site: a real file and function where the committed",
|
|
2089
|
+
" user action happens. Never propose absence/timeout/derived concepts (daily_log_missed,",
|
|
2090
|
+
" streak_broken) \u2014 the server derives those later.",
|
|
2091
|
+
"- Emit after confirmed outcomes, not intent: a purchase event fires on confirmed success, not on",
|
|
2092
|
+
" button tap; denied permissions are not grants.",
|
|
2093
|
+
"- Importance is anchored in the business context: the activation moment, churn-risk and healthy",
|
|
2094
|
+
" signals, and how the product charges. A handful of load-bearing events beats wide coverage.",
|
|
2095
|
+
"- An empty or small set is a valid outcome. There is no quota. Never invent events to look thorough.",
|
|
2096
|
+
"- Codes are lowercase snake_case, named for what happened (checkout_completed, not do_checkout).",
|
|
2097
|
+
"- payloadSchema lists the fields worth capturing with a short description of each \u2014 the important",
|
|
2098
|
+
" info only. NEVER include personal identifiers: no email, phone, names, addresses, birth dates,",
|
|
2099
|
+
" government ids, usernames, passwords, device ids, or IP addresses. Domain metrics are welcome.",
|
|
2100
|
+
"- The end user is the paying customer/consumer. Never instrument admin, staff, or operator actions.",
|
|
2101
|
+
"",
|
|
2102
|
+
"Call propose_event once per event. When done, call finish_selection with a one-line summary."
|
|
2103
|
+
].join("\n");
|
|
2104
|
+
function selectionUserPrompt(surveyReport, onboardingContext, existingEventCodes, rejectedCodes) {
|
|
2105
|
+
return [
|
|
2106
|
+
"Survey report:",
|
|
2107
|
+
surveyReport,
|
|
2108
|
+
"",
|
|
2109
|
+
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
2110
|
+
existingEventCodes.length > 0 ? "Already registered for this app (do not re-propose; other projects may own them):\n- " + existingEventCodes.join("\n- ") : "",
|
|
2111
|
+
rejectedCodes.length > 0 ? "Previously rejected by the user (do not re-propose):\n- " + rejectedCodes.join("\n- ") : "",
|
|
2112
|
+
"",
|
|
2113
|
+
"Propose the important events now."
|
|
2114
|
+
].filter((section) => section !== "").join("\n");
|
|
2115
|
+
}
|
|
2116
|
+
function renderOnboardingContext(context) {
|
|
2117
|
+
if (!context) {
|
|
2118
|
+
return "";
|
|
2119
|
+
}
|
|
2120
|
+
const rendered = JSON.stringify(context, null, 1);
|
|
2121
|
+
return rendered.length > 6e3 ? rendered.slice(0, 6e3) + "\n\u2026(truncated)" : rendered;
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/engine/selection.ts
|
|
2125
|
+
var PII_SEGMENTS = /* @__PURE__ */ new Set([
|
|
2126
|
+
"email",
|
|
2127
|
+
"phone",
|
|
2128
|
+
"address",
|
|
2129
|
+
"surname",
|
|
2130
|
+
"dob",
|
|
2131
|
+
"birthday",
|
|
2132
|
+
"birthdate",
|
|
2133
|
+
"ssn",
|
|
2134
|
+
"passport",
|
|
2135
|
+
"password",
|
|
2136
|
+
"username"
|
|
2137
|
+
]);
|
|
2138
|
+
var PII_PAIRS = [
|
|
2139
|
+
["first", "name"],
|
|
2140
|
+
["last", "name"],
|
|
2141
|
+
["full", "name"],
|
|
2142
|
+
["user", "name"],
|
|
2143
|
+
["middle", "name"],
|
|
2144
|
+
["maiden", "name"],
|
|
2145
|
+
["birth", "date"],
|
|
2146
|
+
["date", "birth"],
|
|
2147
|
+
["national", "id"],
|
|
2148
|
+
["government", "id"],
|
|
2149
|
+
["device", "id"],
|
|
2150
|
+
["personal", "number"],
|
|
2151
|
+
["social", "security"],
|
|
2152
|
+
["tax", "id"],
|
|
2153
|
+
["drivers", "license"]
|
|
2154
|
+
];
|
|
2155
|
+
function isPIIField(name) {
|
|
2156
|
+
const segments = name.split("_");
|
|
2157
|
+
if (segments.some((segment) => PII_SEGMENTS.has(segment))) {
|
|
2158
|
+
return true;
|
|
2159
|
+
}
|
|
2160
|
+
for (let index = 0; index + 1 < segments.length; index += 1) {
|
|
2161
|
+
if (PII_PAIRS.some(([a, b]) => segments[index] === a && segments[index + 1] === b)) {
|
|
2162
|
+
return true;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
return false;
|
|
2166
|
+
}
|
|
2167
|
+
function normalizeEventCode(value) {
|
|
2168
|
+
const collapsed = value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_{2,}/g, "_");
|
|
2169
|
+
return /^[a-z]/.test(collapsed) ? collapsed : "";
|
|
2170
|
+
}
|
|
2171
|
+
function validateProposedEvent(input, seen, rejected) {
|
|
2172
|
+
const code = normalizeEventCode(input.code);
|
|
2173
|
+
if (code === "") {
|
|
2174
|
+
return { ok: false, reason: `event code ${JSON.stringify(input.code)} is not snake_case` };
|
|
2175
|
+
}
|
|
2176
|
+
if (seen.has(code)) {
|
|
2177
|
+
return { ok: false, reason: `event ${code} was already proposed` };
|
|
2178
|
+
}
|
|
2179
|
+
if (rejected.has(code)) {
|
|
2180
|
+
return { ok: false, reason: `event ${code} was rejected by the user \u2014 do not re-propose it` };
|
|
2181
|
+
}
|
|
2182
|
+
if (!input.name.trim() || !input.reasoning.trim() || !input.anchorFile.trim()) {
|
|
2183
|
+
return { ok: false, reason: "name, reasoning, and anchorFile are required" };
|
|
2184
|
+
}
|
|
2185
|
+
const schema = {};
|
|
2186
|
+
for (const [field, description] of Object.entries(input.payloadSchema ?? {})) {
|
|
2187
|
+
const normalizedField = normalizeEventCode(field);
|
|
2188
|
+
if (normalizedField === "" || !description.trim()) {
|
|
2189
|
+
return { ok: false, reason: `payload field ${JSON.stringify(field)} needs a snake_case name and a description` };
|
|
2190
|
+
}
|
|
2191
|
+
if (isPIIField(normalizedField)) {
|
|
2192
|
+
return { ok: false, reason: `payload field ${normalizedField} is a personal identifier and is not allowed` };
|
|
2193
|
+
}
|
|
2194
|
+
schema[normalizedField] = description.trim();
|
|
2195
|
+
}
|
|
2196
|
+
if (Object.keys(schema).length > 20) {
|
|
2197
|
+
return { ok: false, reason: "payload schema exceeds 20 fields" };
|
|
2198
|
+
}
|
|
2199
|
+
return {
|
|
2200
|
+
ok: true,
|
|
2201
|
+
event: {
|
|
2202
|
+
code,
|
|
2203
|
+
name: input.name.trim(),
|
|
2204
|
+
reasoning: input.reasoning.trim(),
|
|
2205
|
+
anchorFile: input.anchorFile.trim(),
|
|
2206
|
+
anchorSymbol: input.anchorSymbol?.trim() || void 0,
|
|
2207
|
+
payloadSchema: schema
|
|
2208
|
+
}
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSessionId) {
|
|
2212
|
+
sink.emit({ phase: "surveying", action: "mapping the repository" });
|
|
2213
|
+
const outcome = await provider.invoke(
|
|
2214
|
+
{
|
|
2215
|
+
role: "survey",
|
|
2216
|
+
systemPrompt: SURVEY_SYSTEM_PROMPT,
|
|
2217
|
+
userPrompt: surveyUserPrompt(repoPath, renderOnboardingContext(bootstrap.onboardingContext)),
|
|
2218
|
+
tools: [],
|
|
2219
|
+
allowRepoWrite: false,
|
|
2220
|
+
cwd: repoPath,
|
|
2221
|
+
resumeSessionId
|
|
2222
|
+
},
|
|
2223
|
+
models.survey,
|
|
2224
|
+
(action) => sink.emit({ phase: "surveying", action })
|
|
2225
|
+
);
|
|
2226
|
+
return { report: outcome.kind === "completed" ? outcome.text : "", outcome };
|
|
2227
|
+
}
|
|
2228
|
+
async function runSelection(provider, models, repoPath, bootstrap, surveyReport, rejectedCodes, sink) {
|
|
2229
|
+
const proposed = [];
|
|
2230
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2231
|
+
const rejected = new Set(rejectedCodes);
|
|
2232
|
+
let summary = "";
|
|
2233
|
+
const proposeTool = {
|
|
2234
|
+
name: "propose_event",
|
|
2235
|
+
description: "Propose one important, concretely-anchored product event. Returns ok:true when accepted.",
|
|
2236
|
+
inputSchema: {
|
|
2237
|
+
code: z.string().describe("lowercase snake_case event code"),
|
|
2238
|
+
name: z.string().describe("short human-readable name"),
|
|
2239
|
+
reasoning: z.string().describe("why this event matters for this business"),
|
|
2240
|
+
anchorFile: z.string().describe("repository file where the committed action happens"),
|
|
2241
|
+
anchorSymbol: z.string().optional().describe("function/symbol at the anchor"),
|
|
2242
|
+
payloadSchema: z.record(z.string(), z.string()).optional().describe("important payload fields: name -> short description; no personal identifiers")
|
|
2243
|
+
},
|
|
2244
|
+
jsonSchema: {
|
|
2245
|
+
type: "object",
|
|
2246
|
+
properties: {
|
|
2247
|
+
code: { type: "string", description: "lowercase snake_case event code" },
|
|
2248
|
+
name: { type: "string", description: "short human-readable name" },
|
|
2249
|
+
reasoning: { type: "string", description: "why this event matters for this business" },
|
|
2250
|
+
anchorFile: { type: "string", description: "repository file where the committed action happens" },
|
|
2251
|
+
anchorSymbol: { type: "string", description: "function/symbol at the anchor" },
|
|
2252
|
+
payloadSchema: {
|
|
2253
|
+
type: "object",
|
|
2254
|
+
additionalProperties: { type: "string" },
|
|
2255
|
+
description: "important payload fields: name -> short description; no personal identifiers"
|
|
2256
|
+
}
|
|
2257
|
+
},
|
|
2258
|
+
required: ["code", "name", "reasoning", "anchorFile"]
|
|
2259
|
+
},
|
|
2260
|
+
handler: async (input) => {
|
|
2261
|
+
const candidate = {
|
|
2262
|
+
code: String(input.code ?? ""),
|
|
2263
|
+
name: String(input.name ?? ""),
|
|
2264
|
+
reasoning: String(input.reasoning ?? ""),
|
|
2265
|
+
anchorFile: String(input.anchorFile ?? ""),
|
|
2266
|
+
anchorSymbol: input.anchorSymbol === void 0 ? void 0 : String(input.anchorSymbol),
|
|
2267
|
+
payloadSchema: input.payloadSchema ?? {}
|
|
2268
|
+
};
|
|
2269
|
+
const validated = validateProposedEvent(candidate, seen, rejected);
|
|
2270
|
+
if (!validated.ok) {
|
|
2271
|
+
return { ok: false, reason: validated.reason };
|
|
2272
|
+
}
|
|
2273
|
+
seen.add(validated.event.code);
|
|
2274
|
+
proposed.push(validated.event);
|
|
2275
|
+
sink.emit({
|
|
2276
|
+
phase: "designing",
|
|
2277
|
+
file: validated.event.anchorFile,
|
|
2278
|
+
action: `proposed ${validated.event.code}`
|
|
2279
|
+
});
|
|
2280
|
+
return { ok: true, code: validated.event.code };
|
|
2281
|
+
}
|
|
2282
|
+
};
|
|
2283
|
+
const finishTool = {
|
|
2284
|
+
name: "finish_selection",
|
|
2285
|
+
description: "Signal that every important event has been proposed.",
|
|
2286
|
+
inputSchema: { summary: z.string().describe("one line on what was selected and why") },
|
|
2287
|
+
jsonSchema: {
|
|
2288
|
+
type: "object",
|
|
2289
|
+
properties: { summary: { type: "string", description: "one line on what was selected and why" } },
|
|
2290
|
+
required: ["summary"]
|
|
2291
|
+
},
|
|
2292
|
+
handler: async (input) => {
|
|
2293
|
+
summary = String(input.summary ?? "");
|
|
2294
|
+
return { ok: true };
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2297
|
+
sink.emit({ phase: "designing", action: "selecting important events" });
|
|
2298
|
+
const outcome = await provider.invoke(
|
|
2299
|
+
{
|
|
2300
|
+
role: "design",
|
|
2301
|
+
systemPrompt: SELECTION_SYSTEM_PROMPT,
|
|
2302
|
+
userPrompt: selectionUserPrompt(
|
|
2303
|
+
surveyReport,
|
|
2304
|
+
renderOnboardingContext(bootstrap.onboardingContext),
|
|
2305
|
+
bootstrap.snapshot.events.map((event) => event.code),
|
|
2306
|
+
rejectedCodes
|
|
2307
|
+
),
|
|
2308
|
+
tools: [proposeTool, finishTool],
|
|
2309
|
+
allowRepoWrite: false,
|
|
2310
|
+
cwd: repoPath
|
|
2311
|
+
},
|
|
2312
|
+
models.design,
|
|
2313
|
+
(action) => sink.emit({ phase: "designing", action })
|
|
2314
|
+
);
|
|
2315
|
+
return {
|
|
2316
|
+
outcome: outcome.kind,
|
|
2317
|
+
proposed,
|
|
2318
|
+
summary,
|
|
2319
|
+
surveyReport,
|
|
2320
|
+
sessionId: outcome.sessionId
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
async function registerApprovedEvents(runs, runId, approved, sink) {
|
|
2324
|
+
const registered = [];
|
|
2325
|
+
for (const event of approved) {
|
|
2326
|
+
const result = await runs.writeItem(runId, "event", {
|
|
2327
|
+
code: event.code,
|
|
2328
|
+
name: event.name,
|
|
2329
|
+
reasoning: event.reasoning,
|
|
2330
|
+
...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
|
|
2331
|
+
});
|
|
2332
|
+
registered.push({ ...event, eventId: result.id || result.item?.id || "" });
|
|
2333
|
+
sink.emit({ phase: "persisting", file: event.anchorFile, action: `registered ${event.code}` });
|
|
2334
|
+
}
|
|
2335
|
+
return registered;
|
|
2336
|
+
}
|
|
2337
|
+
function placementsFor(registered) {
|
|
2338
|
+
return registered.map((event) => ({
|
|
2339
|
+
eventId: event.eventId,
|
|
2340
|
+
eventCode: event.code,
|
|
2341
|
+
file: event.anchorFile,
|
|
2342
|
+
symbol: event.anchorSymbol,
|
|
2343
|
+
status: "planned"
|
|
2344
|
+
}));
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// src/engine/state.ts
|
|
2348
|
+
import { createHash as createHash3 } from "crypto";
|
|
2349
|
+
import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
|
|
2350
|
+
import { homedir, tmpdir } from "os";
|
|
2351
|
+
import { join as join4 } from "path";
|
|
2352
|
+
|
|
2353
|
+
// src/engine/types.ts
|
|
2354
|
+
var ENGINE_PHASES = [
|
|
2355
|
+
"authorizing",
|
|
2356
|
+
"surveying",
|
|
2357
|
+
"designing",
|
|
2358
|
+
"persisting",
|
|
2359
|
+
"binding",
|
|
2360
|
+
"integrating",
|
|
2361
|
+
"reviewing",
|
|
2362
|
+
"verifying",
|
|
2363
|
+
"reporting",
|
|
2364
|
+
"completed"
|
|
2365
|
+
];
|
|
2366
|
+
|
|
2367
|
+
// src/engine/state.ts
|
|
2368
|
+
function stateDirectory() {
|
|
2369
|
+
const override = process.env.WHISPERR_WIZARD_STATE_DIR?.trim();
|
|
2370
|
+
const preferred = override || join4(homedir(), ".whisperr", "wizard-runs");
|
|
2371
|
+
if (canCreateStateDirectory(preferred)) {
|
|
2372
|
+
return preferred;
|
|
2373
|
+
}
|
|
2374
|
+
const user = typeof process.getuid === "function" ? process.getuid() : "user";
|
|
2375
|
+
const fallback = join4(tmpdir(), `whisperr-${user}`, "wizard-runs");
|
|
2376
|
+
if (canCreateStateDirectory(fallback)) {
|
|
2377
|
+
return fallback;
|
|
2378
|
+
}
|
|
2379
|
+
throw new Error("Whisperr Wizard could not create a directory for run state");
|
|
2380
|
+
}
|
|
2381
|
+
function stateKey(apiBaseUrl, appId, repoFingerprint2) {
|
|
2382
|
+
return createHash3("sha256").update(`${apiBaseUrl}
|
|
2383
|
+
${appId}
|
|
2384
|
+
${repoFingerprint2}`).digest("hex").slice(0, 24);
|
|
2385
|
+
}
|
|
2386
|
+
function statePath(apiBaseUrl, appId, repoFingerprint2) {
|
|
2387
|
+
return join4(stateDirectory(), `${stateKey(apiBaseUrl, appId, repoFingerprint2)}.json`);
|
|
2388
|
+
}
|
|
2389
|
+
function saveRunState(state) {
|
|
2390
|
+
const directory = stateDirectory();
|
|
2391
|
+
const path = join4(directory, `${stateKey(state.apiBaseUrl, state.appId, state.repoFingerprint)}.json`);
|
|
2392
|
+
const payload = JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2);
|
|
2393
|
+
const temporary = `${path}.tmp`;
|
|
2394
|
+
writeFileSync(temporary, payload, { mode: 384 });
|
|
2395
|
+
renameSync(temporary, path);
|
|
2396
|
+
}
|
|
2397
|
+
function canCreateStateDirectory(directory) {
|
|
2398
|
+
try {
|
|
2399
|
+
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
2400
|
+
return true;
|
|
2401
|
+
} catch {
|
|
2402
|
+
return false;
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
function loadRunState(apiBaseUrl, appId, repoFingerprint2) {
|
|
2406
|
+
const path = statePath(apiBaseUrl, appId, repoFingerprint2);
|
|
2407
|
+
let raw;
|
|
2408
|
+
try {
|
|
2409
|
+
raw = readFileSync2(path, "utf8");
|
|
2410
|
+
} catch {
|
|
2411
|
+
return void 0;
|
|
2412
|
+
}
|
|
2413
|
+
let parsed;
|
|
2414
|
+
try {
|
|
2415
|
+
parsed = JSON.parse(raw);
|
|
2416
|
+
} catch {
|
|
2417
|
+
return void 0;
|
|
2418
|
+
}
|
|
2419
|
+
if (!isEngineRunState(parsed)) {
|
|
2420
|
+
return void 0;
|
|
2421
|
+
}
|
|
2422
|
+
if (parsed.apiBaseUrl !== apiBaseUrl || parsed.appId !== appId || parsed.repoFingerprint !== repoFingerprint2) {
|
|
2423
|
+
return void 0;
|
|
2424
|
+
}
|
|
2425
|
+
return parsed;
|
|
2426
|
+
}
|
|
2427
|
+
function isEngineRunState(value) {
|
|
2428
|
+
if (typeof value !== "object" || value === null) {
|
|
2429
|
+
return false;
|
|
2430
|
+
}
|
|
2431
|
+
const candidate = value;
|
|
2432
|
+
return candidate.version === 1 && typeof candidate.apiBaseUrl === "string" && typeof candidate.appId === "string" && typeof candidate.repoFingerprint === "string" && typeof candidate.runId === "string" && typeof candidate.phase === "string" && ENGINE_PHASES.includes(candidate.phase) && typeof candidate.designComplete === "boolean" && Array.isArray(candidate.tombstonedCodes) && Array.isArray(candidate.clusters) && typeof candidate.providerSessions === "object" && candidate.providerSessions !== null;
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
// src/engine/worktree.ts
|
|
2436
|
+
import { spawn as spawn2 } from "child_process";
|
|
2437
|
+
import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
|
|
2438
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2439
|
+
import { join as join5 } from "path";
|
|
2440
|
+
var WorktreeError = class extends Error {
|
|
2441
|
+
constructor(code, message) {
|
|
2442
|
+
super(message);
|
|
2443
|
+
this.code = code;
|
|
2444
|
+
this.name = "WorktreeError";
|
|
2445
|
+
}
|
|
2446
|
+
code;
|
|
2447
|
+
};
|
|
2448
|
+
function git(cwd, args) {
|
|
2449
|
+
return new Promise((resolve4) => {
|
|
2450
|
+
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2451
|
+
let stdout = "";
|
|
2452
|
+
let stderr = "";
|
|
2453
|
+
child.stdout.on("data", (chunk) => {
|
|
2454
|
+
stdout += String(chunk);
|
|
2455
|
+
});
|
|
2456
|
+
child.stderr.on("data", (chunk) => {
|
|
2457
|
+
stderr += String(chunk);
|
|
2458
|
+
});
|
|
2459
|
+
child.on("error", () => resolve4({ ok: false, stdout, stderr: "git is not available" }));
|
|
2460
|
+
child.on("close", (exitCode) => resolve4({ ok: exitCode === 0, stdout, stderr }));
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
async function must(cwd, args) {
|
|
2464
|
+
const result = await git(cwd, args);
|
|
2465
|
+
if (!result.ok) {
|
|
2466
|
+
throw new WorktreeError("git_failed", `git ${args.join(" ")}: ${result.stderr.trim()}`);
|
|
2467
|
+
}
|
|
2468
|
+
return result.stdout;
|
|
2469
|
+
}
|
|
2470
|
+
async function createWorktree(repoPath, force = false) {
|
|
2471
|
+
const inside = await git(repoPath, ["rev-parse", "--is-inside-work-tree"]);
|
|
2472
|
+
if (!inside.ok) {
|
|
2473
|
+
throw new WorktreeError("not_a_repo", "the target directory is not a git repository");
|
|
2474
|
+
}
|
|
2475
|
+
const status = await must(repoPath, ["status", "--porcelain"]);
|
|
2476
|
+
if (status.trim() !== "" && !force) {
|
|
2477
|
+
throw new WorktreeError(
|
|
2478
|
+
"dirty_repo",
|
|
2479
|
+
"the repository has uncommitted changes \u2014 commit or stash them, or rerun with --force"
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
const baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
|
|
2483
|
+
const path = await mkdtemp(join5(tmpdir2(), "whisperr-worktree-"));
|
|
2484
|
+
await must(repoPath, ["worktree", "add", "--detach", "--force", path, "HEAD"]);
|
|
2485
|
+
return { path, baseRef, repoPath };
|
|
2486
|
+
}
|
|
2487
|
+
async function worktreePatch(handle) {
|
|
2488
|
+
await must(handle.path, ["add", "-A"]);
|
|
2489
|
+
return must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
|
|
2490
|
+
}
|
|
2491
|
+
async function applyPatchToRepo(handle, patch) {
|
|
2492
|
+
if (patch.trim() === "") {
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
await new Promise((resolve4, reject) => {
|
|
2496
|
+
const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
|
|
2497
|
+
cwd: handle.repoPath,
|
|
2498
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
2499
|
+
});
|
|
2500
|
+
let stderr = "";
|
|
2501
|
+
child.stderr.on("data", (chunk) => {
|
|
2502
|
+
stderr += String(chunk);
|
|
2503
|
+
});
|
|
2504
|
+
child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
|
|
2505
|
+
child.on("close", (exitCode) => {
|
|
2506
|
+
if (exitCode === 0) {
|
|
2507
|
+
resolve4();
|
|
2508
|
+
} else {
|
|
2509
|
+
reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
|
|
2510
|
+
}
|
|
2511
|
+
});
|
|
2512
|
+
child.stdin.end(patch);
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
async function savePartialPatch(handle, destination) {
|
|
2516
|
+
const patch = await worktreePatch(handle);
|
|
2517
|
+
if (patch.trim() === "") {
|
|
2518
|
+
return void 0;
|
|
2519
|
+
}
|
|
2520
|
+
await writeFile2(destination, patch, { mode: 384 });
|
|
2521
|
+
return destination;
|
|
2522
|
+
}
|
|
2523
|
+
async function removeWorktree(handle) {
|
|
2524
|
+
await git(handle.repoPath, ["worktree", "remove", "--force", handle.path]);
|
|
2525
|
+
await git(handle.repoPath, ["worktree", "prune"]);
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
// src/engine/orchestrator.ts
|
|
2529
|
+
async function runEngine(input) {
|
|
2530
|
+
const runs = new RunsClient(input.config, input.session);
|
|
2531
|
+
let bootstrap;
|
|
2532
|
+
try {
|
|
2533
|
+
bootstrap = await runs.createOrResumeRun({
|
|
2534
|
+
repoFingerprint: input.repoFingerprint,
|
|
2535
|
+
displayName: basename(input.repoPath),
|
|
2536
|
+
target: input.target,
|
|
2537
|
+
kind: input.projectKind
|
|
2538
|
+
});
|
|
2539
|
+
} catch (error) {
|
|
2540
|
+
if (error instanceof RunsApiError && error.code === "wrong_universe_mode") {
|
|
2541
|
+
return { kind: "wrong_mode" };
|
|
2542
|
+
}
|
|
2543
|
+
throw error;
|
|
2544
|
+
}
|
|
2545
|
+
const runId = bootstrap.run.id;
|
|
2546
|
+
input.provider.onRunReady?.(runId);
|
|
2547
|
+
const patchPhase = async (phase, message) => {
|
|
2548
|
+
try {
|
|
2549
|
+
await runs.patchRun(runId, { phase, ...message ? { message } : {} });
|
|
2550
|
+
} catch {
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
const failRun = async (error) => {
|
|
2554
|
+
try {
|
|
2555
|
+
await runs.patchRun(runId, { status: "failed", error: error.slice(0, 500) });
|
|
2556
|
+
} catch {
|
|
2557
|
+
}
|
|
2558
|
+
};
|
|
2559
|
+
let state;
|
|
2560
|
+
try {
|
|
2561
|
+
state = loadRunState(input.config.apiBaseUrl, input.session.appId, input.repoFingerprint) ?? freshState(input, runId);
|
|
2562
|
+
if (state.runId !== runId) {
|
|
2563
|
+
state = freshState(input, runId);
|
|
2564
|
+
}
|
|
2565
|
+
} catch (error) {
|
|
2566
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2567
|
+
await failRun(`state initialization failed: ${reason}`);
|
|
2568
|
+
return { kind: "aborted", reason: `state initialization failed: ${reason}` };
|
|
2569
|
+
}
|
|
2570
|
+
const persist = () => saveRunState(state);
|
|
2571
|
+
let registered;
|
|
2572
|
+
if (!state.designComplete) {
|
|
2573
|
+
try {
|
|
2574
|
+
await patchPhase("surveying");
|
|
2575
|
+
const survey = await runSurvey(
|
|
2576
|
+
input.provider,
|
|
2577
|
+
input.models,
|
|
2578
|
+
input.repoPath,
|
|
2579
|
+
bootstrap,
|
|
2580
|
+
input.sink,
|
|
2581
|
+
state.providerSessions.survey
|
|
2582
|
+
);
|
|
2583
|
+
if (survey.outcome.kind !== "completed") {
|
|
2584
|
+
await failRun(`survey ${survey.outcome.kind}`);
|
|
2585
|
+
return { kind: "aborted", reason: `survey ${survey.outcome.kind}` };
|
|
2586
|
+
}
|
|
2587
|
+
state.providerSessions.survey = survey.outcome.sessionId;
|
|
2588
|
+
persist();
|
|
2589
|
+
await patchPhase("designing");
|
|
2590
|
+
const selection = await runSelection(
|
|
2591
|
+
input.provider,
|
|
2592
|
+
input.models,
|
|
2593
|
+
input.repoPath,
|
|
2594
|
+
bootstrap,
|
|
2595
|
+
survey.report,
|
|
2596
|
+
state.tombstonedCodes,
|
|
2597
|
+
input.sink
|
|
2598
|
+
);
|
|
2599
|
+
if (selection.outcome !== "completed" && selection.proposed.length === 0) {
|
|
2600
|
+
await failRun(`selection ${selection.outcome}`);
|
|
2601
|
+
return { kind: "aborted", reason: `selection ${selection.outcome}` };
|
|
2602
|
+
}
|
|
2603
|
+
const approved = await input.callbacks.reviewEvents(selection.proposed);
|
|
2604
|
+
if (approved === null) {
|
|
2605
|
+
await failRun("selection rejected by user");
|
|
2606
|
+
return { kind: "aborted", reason: "selection rejected by user" };
|
|
2607
|
+
}
|
|
2608
|
+
const approvedCodes = new Set(approved.map((event) => event.code));
|
|
2609
|
+
for (const event of selection.proposed) {
|
|
2610
|
+
if (!approvedCodes.has(event.code) && !state.tombstonedCodes.includes(event.code)) {
|
|
2611
|
+
state.tombstonedCodes.push(event.code);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
persist();
|
|
2615
|
+
await patchPhase("persisting", `registering ${approved.length} events`);
|
|
2616
|
+
registered = await registerApprovedEvents(runs, runId, approved, input.sink);
|
|
2617
|
+
state.designComplete = true;
|
|
2618
|
+
state.clusters = deriveClusters(placementsFor(registered));
|
|
2619
|
+
persist();
|
|
2620
|
+
} catch (error) {
|
|
2621
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2622
|
+
await failRun(`design failed: ${reason}`);
|
|
2623
|
+
return { kind: "aborted", reason: `design failed: ${reason}` };
|
|
2624
|
+
}
|
|
2625
|
+
} else {
|
|
2626
|
+
registered = rebuildRegistered(state, bootstrap);
|
|
2627
|
+
input.sink.emit({ phase: "designing", action: `resumed with ${registered.length} registered events` });
|
|
2628
|
+
}
|
|
2629
|
+
if (registered.length === 0) {
|
|
2630
|
+
await patchPhase("reporting", "no events registered for this project");
|
|
2631
|
+
try {
|
|
2632
|
+
await completeRun(runs, runId, [], /* @__PURE__ */ new Map(), [], true, "unavailable", void 0);
|
|
2633
|
+
} catch (error) {
|
|
2634
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2635
|
+
await failRun(`completion failed: ${reason}`);
|
|
2636
|
+
return { kind: "aborted", reason: `completion failed: ${reason}` };
|
|
2637
|
+
}
|
|
2638
|
+
return {
|
|
2639
|
+
kind: "completed",
|
|
2640
|
+
runId,
|
|
2641
|
+
registered,
|
|
2642
|
+
eventStatuses: /* @__PURE__ */ new Map(),
|
|
2643
|
+
reportEvents: [],
|
|
2644
|
+
changedFiles: [],
|
|
2645
|
+
identifyWired: false,
|
|
2646
|
+
applied: false,
|
|
2647
|
+
summary: "No important events found for this project; nothing was instrumented."
|
|
2648
|
+
};
|
|
2649
|
+
}
|
|
2650
|
+
await patchPhase("binding");
|
|
2651
|
+
let worktree;
|
|
2652
|
+
try {
|
|
2653
|
+
worktree = await createWorktree(input.repoPath, input.force ?? false);
|
|
2654
|
+
} catch (error) {
|
|
2655
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2656
|
+
await failRun(reason);
|
|
2657
|
+
return { kind: "aborted", reason };
|
|
2658
|
+
}
|
|
2659
|
+
state.worktreePath = worktree.path;
|
|
2660
|
+
persist();
|
|
2661
|
+
try {
|
|
2662
|
+
const bindings = await writeBindingsModule(worktree, input.target, registered);
|
|
2663
|
+
const deps = {
|
|
2664
|
+
provider: input.provider,
|
|
2665
|
+
models: input.models,
|
|
2666
|
+
worktree,
|
|
2667
|
+
target: input.target,
|
|
2668
|
+
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
2669
|
+
registered,
|
|
2670
|
+
ingestion: bootstrap.ingestion,
|
|
2671
|
+
sink: input.sink,
|
|
2672
|
+
saveClusters: (clusters) => {
|
|
2673
|
+
state.clusters = clusters;
|
|
2674
|
+
persist();
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
const identifyWired = await runSdkSetupPass(deps, bindings);
|
|
2678
|
+
await patchPhase("integrating");
|
|
2679
|
+
const integration = await runClusterIntegration(
|
|
2680
|
+
deps,
|
|
2681
|
+
state.clusters,
|
|
2682
|
+
async () => worktreePatch(worktree)
|
|
2683
|
+
);
|
|
2684
|
+
state.clusters = integration.clusters;
|
|
2685
|
+
persist();
|
|
2686
|
+
const finalVerify = await finalizeIntegration(deps);
|
|
2687
|
+
const patch = await worktreePatch(worktree);
|
|
2688
|
+
const changedFiles2 = patchFiles(patch);
|
|
2689
|
+
await patchPhase("reporting");
|
|
2690
|
+
const evidence = buildEvidence(registered, integration.eventStatuses, changedFiles2);
|
|
2691
|
+
await completeRun(
|
|
2692
|
+
runs,
|
|
2693
|
+
runId,
|
|
2694
|
+
evidence,
|
|
2695
|
+
integration.eventStatuses,
|
|
2696
|
+
changedFiles2,
|
|
2697
|
+
identifyWired,
|
|
2698
|
+
finalVerify.status,
|
|
2699
|
+
finalVerify.command
|
|
2700
|
+
);
|
|
2701
|
+
const summaryLines = summarize(registered, integration.eventStatuses);
|
|
2702
|
+
const applied = patch.trim() !== "" && await input.callbacks.confirmApply(patch, summaryLines);
|
|
2703
|
+
if (applied) {
|
|
2704
|
+
await applyPatchToRepo(worktree, patch);
|
|
2705
|
+
}
|
|
2706
|
+
const partialPatchPath = applied ? void 0 : await savePartialPatch(worktree, join6(input.repoPath, ".whisperr-wizard.patch"));
|
|
2707
|
+
await removeWorktree(worktree);
|
|
2708
|
+
state.worktreePath = void 0;
|
|
2709
|
+
persist();
|
|
2710
|
+
const hasFailures = [...integration.eventStatuses.values()].some(
|
|
2711
|
+
(status) => status === "failed"
|
|
2712
|
+
);
|
|
2713
|
+
return {
|
|
2714
|
+
kind: hasFailures ? "partial" : "completed",
|
|
2715
|
+
runId,
|
|
2716
|
+
registered,
|
|
2717
|
+
eventStatuses: integration.eventStatuses,
|
|
2718
|
+
reportEvents: buildReportEvents(registered, integration.eventStatuses),
|
|
2719
|
+
changedFiles: changedFiles2,
|
|
2720
|
+
identifyWired,
|
|
2721
|
+
applied,
|
|
2722
|
+
partialPatchPath: partialPatchPath ?? void 0,
|
|
2723
|
+
summary: summaryLines.join("\n")
|
|
2724
|
+
};
|
|
2725
|
+
} catch (error) {
|
|
2726
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2727
|
+
await failRun(reason);
|
|
2728
|
+
const partialPatchPath = await savePartialPatch(
|
|
2729
|
+
worktree,
|
|
2730
|
+
join6(input.repoPath, ".whisperr-wizard.patch")
|
|
2731
|
+
).catch(() => void 0);
|
|
2732
|
+
persist();
|
|
2733
|
+
return {
|
|
2734
|
+
kind: "partial",
|
|
2735
|
+
runId,
|
|
2736
|
+
registered,
|
|
2737
|
+
eventStatuses: /* @__PURE__ */ new Map(),
|
|
2738
|
+
reportEvents: buildReportEvents(registered, /* @__PURE__ */ new Map()),
|
|
2739
|
+
changedFiles: [],
|
|
2740
|
+
identifyWired: false,
|
|
2741
|
+
applied: false,
|
|
2742
|
+
partialPatchPath: partialPatchPath ?? void 0,
|
|
2743
|
+
summary: `Integration stopped early: ${reason}. Registered events are saved; rerun to resume.`
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
function freshState(input, runId) {
|
|
2748
|
+
return {
|
|
2749
|
+
version: 1,
|
|
2750
|
+
apiBaseUrl: input.config.apiBaseUrl,
|
|
2751
|
+
appId: input.session.appId,
|
|
2752
|
+
repoFingerprint: input.repoFingerprint,
|
|
2753
|
+
runId,
|
|
2754
|
+
phase: "authorizing",
|
|
2755
|
+
providerSessions: {},
|
|
2756
|
+
designComplete: false,
|
|
2757
|
+
tombstonedCodes: [],
|
|
2758
|
+
clusters: [],
|
|
2759
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1186
2760
|
};
|
|
1187
2761
|
}
|
|
2762
|
+
function rebuildRegistered(state, bootstrap) {
|
|
2763
|
+
const byCode = new Map(bootstrap.snapshot.events.map((event) => [event.code, event]));
|
|
2764
|
+
const registered = [];
|
|
2765
|
+
for (const cluster of state.clusters) {
|
|
2766
|
+
for (const placement of cluster.events) {
|
|
2767
|
+
const event = byCode.get(placement.eventCode);
|
|
2768
|
+
if (!event) {
|
|
2769
|
+
continue;
|
|
2770
|
+
}
|
|
2771
|
+
registered.push({
|
|
2772
|
+
code: event.code,
|
|
2773
|
+
name: event.name,
|
|
2774
|
+
reasoning: event.reasoning,
|
|
2775
|
+
eventId: event.id,
|
|
2776
|
+
anchorFile: placement.file,
|
|
2777
|
+
anchorSymbol: placement.symbol,
|
|
2778
|
+
payloadSchema: event.payloadSchema ?? {}
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
return registered;
|
|
2783
|
+
}
|
|
2784
|
+
function buildEvidence(registered, statuses, changedFiles2) {
|
|
2785
|
+
const changed = new Set(changedFiles2);
|
|
2786
|
+
return registered.map((event) => {
|
|
2787
|
+
const status = statuses.get(event.code) ?? "planned";
|
|
2788
|
+
const needsFile = status !== "planned" && status !== "unsupported" && status !== "failed";
|
|
2789
|
+
const file = changed.has(event.anchorFile) ? event.anchorFile : changedFiles2[0];
|
|
2790
|
+
return {
|
|
2791
|
+
eventId: event.eventId,
|
|
2792
|
+
eventCode: event.code,
|
|
2793
|
+
...needsFile && file ? { file } : {},
|
|
2794
|
+
status
|
|
2795
|
+
};
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
async function completeRun(runs, runId, evidence, _statuses, changedFiles2, identifyWired, verificationStatus, verificationCommand2) {
|
|
2799
|
+
await runs.completeRun(runId, {
|
|
2800
|
+
changedFiles: changedFiles2,
|
|
2801
|
+
identifyWired,
|
|
2802
|
+
verificationStatus,
|
|
2803
|
+
...verificationCommand2 ? { verificationCommand: verificationCommand2 } : {},
|
|
2804
|
+
events: evidence
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
function buildReportEvents(registered, statuses) {
|
|
2808
|
+
return registered.map((event) => {
|
|
2809
|
+
const status = statuses.get(event.code);
|
|
2810
|
+
const wired = status !== void 0 && status !== "failed" && status !== "unsupported" && status !== "planned";
|
|
2811
|
+
return {
|
|
2812
|
+
event_type: event.code,
|
|
2813
|
+
status: wired ? "wired" : "skipped",
|
|
2814
|
+
...wired ? { file: event.anchorFile } : {},
|
|
2815
|
+
...wired ? {} : { reason: status ?? "not integrated" }
|
|
2816
|
+
};
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
function summarize(registered, statuses) {
|
|
2820
|
+
return registered.map((event) => {
|
|
2821
|
+
const status = statuses.get(event.code) ?? "planned";
|
|
2822
|
+
return `${event.code}: ${status} (${event.anchorFile})`;
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
function patchFiles(patch) {
|
|
2826
|
+
const files = /* @__PURE__ */ new Set();
|
|
2827
|
+
for (const line of patch.split("\n")) {
|
|
2828
|
+
const match = /^diff --git a\/(.+?) b\//.exec(line);
|
|
2829
|
+
if (match?.[1]) {
|
|
2830
|
+
files.add(match[1]);
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
return [...files];
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
// src/engine/providers/claude.ts
|
|
2837
|
+
import { createSdkMcpServer, query as query2, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
1188
2838
|
|
|
1189
2839
|
// src/core/agent.ts
|
|
1190
2840
|
import {
|
|
@@ -1192,10 +2842,10 @@ import {
|
|
|
1192
2842
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
1193
2843
|
|
|
1194
2844
|
// src/core/git.ts
|
|
1195
|
-
import { spawn } from "child_process";
|
|
1196
|
-
import { createHash } from "crypto";
|
|
2845
|
+
import { spawn as spawn3 } from "child_process";
|
|
2846
|
+
import { createHash as createHash4 } from "crypto";
|
|
1197
2847
|
import { readFile as readFile2 } from "fs/promises";
|
|
1198
|
-
import { basename, join as
|
|
2848
|
+
import { basename as basename2, join as join7 } from "path";
|
|
1199
2849
|
async function takeCheckpoint(repoPath) {
|
|
1200
2850
|
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
1201
2851
|
if (!isRepo) return { isRepo: false };
|
|
@@ -1243,8 +2893,8 @@ async function revertToCheckpoint(repoPath, checkpoint) {
|
|
|
1243
2893
|
}
|
|
1244
2894
|
async function repoFingerprint(repoPath) {
|
|
1245
2895
|
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
1246
|
-
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${
|
|
1247
|
-
return
|
|
2896
|
+
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename2(repoPath)}`;
|
|
2897
|
+
return createHash4("sha256").update(seed).digest("hex").slice(0, 16);
|
|
1248
2898
|
}
|
|
1249
2899
|
function normalizeRemote(url) {
|
|
1250
2900
|
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
@@ -1261,7 +2911,7 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
|
1261
2911
|
const contents = [];
|
|
1262
2912
|
for (const file of files) {
|
|
1263
2913
|
try {
|
|
1264
|
-
contents.push({ file, content: await readFile2(
|
|
2914
|
+
contents.push({ file, content: await readFile2(join7(repoPath, file), "utf8") });
|
|
1265
2915
|
} catch {
|
|
1266
2916
|
continue;
|
|
1267
2917
|
}
|
|
@@ -1291,14 +2941,14 @@ function escapeRegExp(s) {
|
|
|
1291
2941
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1292
2942
|
}
|
|
1293
2943
|
function run(cwd, args) {
|
|
1294
|
-
return new Promise((
|
|
1295
|
-
const child =
|
|
2944
|
+
return new Promise((resolve4) => {
|
|
2945
|
+
const child = spawn3("git", args, { cwd });
|
|
1296
2946
|
let stdout = "";
|
|
1297
2947
|
let stderr = "";
|
|
1298
2948
|
child.stdout.on("data", (d) => stdout += d.toString());
|
|
1299
2949
|
child.stderr.on("data", (d) => stderr += d.toString());
|
|
1300
|
-
child.on("close", (code) =>
|
|
1301
|
-
child.on("error", () =>
|
|
2950
|
+
child.on("close", (code) => resolve4({ ok: code === 0, stdout, stderr }));
|
|
2951
|
+
child.on("error", () => resolve4({ ok: false, stdout, stderr }));
|
|
1302
2952
|
});
|
|
1303
2953
|
}
|
|
1304
2954
|
|
|
@@ -1469,10 +3119,10 @@ function renderEventsBrief(m) {
|
|
|
1469
3119
|
if (e.description) lines.push(` ${e.description}`);
|
|
1470
3120
|
if (e.properties?.length) {
|
|
1471
3121
|
lines.push(" properties to capture if available:");
|
|
1472
|
-
for (const
|
|
1473
|
-
const req =
|
|
1474
|
-
const desc =
|
|
1475
|
-
lines.push(` \xB7 ${
|
|
3122
|
+
for (const p3 of e.properties) {
|
|
3123
|
+
const req = p3.required ? " (required)" : "";
|
|
3124
|
+
const desc = p3.description ? ` \u2014 ${p3.description}` : "";
|
|
3125
|
+
lines.push(` \xB7 ${p3.name}${req}${desc}`);
|
|
1476
3126
|
}
|
|
1477
3127
|
}
|
|
1478
3128
|
}
|
|
@@ -2691,6 +4341,446 @@ function short(s, max = 60) {
|
|
|
2691
4341
|
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
2692
4342
|
}
|
|
2693
4343
|
|
|
4344
|
+
// src/engine/providers/claude.ts
|
|
4345
|
+
var READ_ONLY_TOOLS2 = ["Read", "Glob", "Grep"];
|
|
4346
|
+
var EDIT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
4347
|
+
function createClaudeProvider(config, session) {
|
|
4348
|
+
return {
|
|
4349
|
+
name: "claude",
|
|
4350
|
+
async invoke(invocation, roleConfig, onProgress) {
|
|
4351
|
+
applyModelAuthEnv(config, session);
|
|
4352
|
+
const hostTools = invocation.tools.map(
|
|
4353
|
+
(engineTool) => tool(engineTool.name, engineTool.description, engineTool.inputSchema, async (args) => {
|
|
4354
|
+
const result = await engineTool.handler(args);
|
|
4355
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4356
|
+
})
|
|
4357
|
+
);
|
|
4358
|
+
const allowedTools = [
|
|
4359
|
+
...invocation.allowRepoWrite ? EDIT_TOOLS : READ_ONLY_TOOLS2,
|
|
4360
|
+
...invocation.tools.map((engineTool) => `mcp__engine__${engineTool.name}`)
|
|
4361
|
+
];
|
|
4362
|
+
let sessionId = invocation.resumeSessionId;
|
|
4363
|
+
let costUsd = 0;
|
|
4364
|
+
let turns = 0;
|
|
4365
|
+
const deadline = Date.now() + roleConfig.maxMs;
|
|
4366
|
+
const outcome = (kind, text = "", error = "") => kind === "completed" ? { kind, text, sessionId, costUsd, turns } : kind === "failed" ? { kind, error, sessionId, costUsd, turns } : { kind, sessionId, costUsd, turns };
|
|
4367
|
+
try {
|
|
4368
|
+
const response = query2({
|
|
4369
|
+
prompt: invocation.userPrompt,
|
|
4370
|
+
options: {
|
|
4371
|
+
model: roleConfig.model,
|
|
4372
|
+
cwd: invocation.cwd,
|
|
4373
|
+
systemPrompt: invocation.systemPrompt,
|
|
4374
|
+
thinking: { type: "adaptive" },
|
|
4375
|
+
...roleConfig.effort ? { effort: roleConfig.effort } : {},
|
|
4376
|
+
tools: [...allowedTools],
|
|
4377
|
+
...hostTools.length > 0 ? { mcpServers: { engine: createSdkMcpServer({ name: "engine", tools: hostTools }) } } : {},
|
|
4378
|
+
hooks: buildToolPolicyHooks(invocation.cwd),
|
|
4379
|
+
canUseTool: createToolPermissionCallback(invocation.cwd),
|
|
4380
|
+
maxTurns: roleConfig.maxTurns,
|
|
4381
|
+
...invocation.resumeSessionId ? { resume: invocation.resumeSessionId } : {},
|
|
4382
|
+
settingSources: []
|
|
4383
|
+
}
|
|
4384
|
+
});
|
|
4385
|
+
for await (const raw of response) {
|
|
4386
|
+
if (raw.session_id) {
|
|
4387
|
+
sessionId = raw.session_id;
|
|
4388
|
+
}
|
|
4389
|
+
if (raw.type === "assistant") {
|
|
4390
|
+
turns += 1;
|
|
4391
|
+
const block = raw.message?.content?.find(
|
|
4392
|
+
(candidate) => candidate.type === "text" || candidate.type === "tool_use"
|
|
4393
|
+
);
|
|
4394
|
+
if (block?.type === "text" && block.text?.trim()) {
|
|
4395
|
+
onProgress((block.text.trim().split("\n")[0] ?? "").slice(0, 120));
|
|
4396
|
+
} else if (block?.type === "tool_use" && block.name) {
|
|
4397
|
+
onProgress(`using ${block.name}`);
|
|
4398
|
+
}
|
|
4399
|
+
if (Date.now() > deadline) {
|
|
4400
|
+
await response.interrupt?.();
|
|
4401
|
+
return outcome("budget_exhausted");
|
|
4402
|
+
}
|
|
4403
|
+
} else if (raw.type === "result") {
|
|
4404
|
+
costUsd = raw.total_cost_usd ?? 0;
|
|
4405
|
+
const subtype = raw.subtype ?? "";
|
|
4406
|
+
if (subtype === "success") {
|
|
4407
|
+
return outcome("completed", raw.result ?? "");
|
|
4408
|
+
}
|
|
4409
|
+
if (subtype.includes("max_turns") || subtype.includes("max_budget")) {
|
|
4410
|
+
return outcome("budget_exhausted");
|
|
4411
|
+
}
|
|
4412
|
+
const detail = raw.result ?? subtype;
|
|
4413
|
+
if (classifyFailure(detail) === "context_overflow") {
|
|
4414
|
+
return outcome("context_overflow");
|
|
4415
|
+
}
|
|
4416
|
+
return outcome("failed", "", detail || "model run failed");
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
return outcome("failed", "", "model stream ended without a result");
|
|
4420
|
+
} catch (error) {
|
|
4421
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4422
|
+
if (classifyFailure(detail) === "context_overflow") {
|
|
4423
|
+
return outcome("context_overflow");
|
|
4424
|
+
}
|
|
4425
|
+
return outcome("failed", "", detail);
|
|
4426
|
+
}
|
|
4427
|
+
}
|
|
4428
|
+
};
|
|
4429
|
+
}
|
|
4430
|
+
|
|
4431
|
+
// src/engine/providers/openai.ts
|
|
4432
|
+
import { readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4433
|
+
import { join as join8, resolve as resolve2, sep } from "path";
|
|
4434
|
+
function jailedPath(cwd, candidate) {
|
|
4435
|
+
const resolved = resolve2(cwd, candidate);
|
|
4436
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + sep)) {
|
|
4437
|
+
throw new Error(`path ${candidate} escapes the repository`);
|
|
4438
|
+
}
|
|
4439
|
+
return resolved;
|
|
4440
|
+
}
|
|
4441
|
+
function fileTools(cwd, allowWrite) {
|
|
4442
|
+
const tools = [
|
|
4443
|
+
{
|
|
4444
|
+
name: "read_file",
|
|
4445
|
+
description: "Read a repository file (UTF-8, truncated at 40000 chars).",
|
|
4446
|
+
inputSchema: {},
|
|
4447
|
+
jsonSchema: {
|
|
4448
|
+
type: "object",
|
|
4449
|
+
properties: { path: { type: "string" } },
|
|
4450
|
+
required: ["path"]
|
|
4451
|
+
},
|
|
4452
|
+
handler: async (input) => {
|
|
4453
|
+
const content = readFileSync3(jailedPath(cwd, String(input.path ?? "")), "utf8");
|
|
4454
|
+
return { content: content.slice(0, 4e4) };
|
|
4455
|
+
}
|
|
4456
|
+
},
|
|
4457
|
+
{
|
|
4458
|
+
name: "list_dir",
|
|
4459
|
+
description: "List entries of a repository directory.",
|
|
4460
|
+
inputSchema: {},
|
|
4461
|
+
jsonSchema: {
|
|
4462
|
+
type: "object",
|
|
4463
|
+
properties: { path: { type: "string" } },
|
|
4464
|
+
required: ["path"]
|
|
4465
|
+
},
|
|
4466
|
+
handler: async (input) => {
|
|
4467
|
+
const target9 = jailedPath(cwd, String(input.path ?? "."));
|
|
4468
|
+
const entries = readdirSync(target9).map((entry) => {
|
|
4469
|
+
const kind = statSync(join8(target9, entry)).isDirectory() ? "dir" : "file";
|
|
4470
|
+
return `${kind}:${entry}`;
|
|
4471
|
+
});
|
|
4472
|
+
return { entries: entries.slice(0, 500) };
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
];
|
|
4476
|
+
if (allowWrite) {
|
|
4477
|
+
tools.push(
|
|
4478
|
+
{
|
|
4479
|
+
name: "write_file",
|
|
4480
|
+
description: "Create or overwrite a repository file with the given content.",
|
|
4481
|
+
inputSchema: {},
|
|
4482
|
+
jsonSchema: {
|
|
4483
|
+
type: "object",
|
|
4484
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
4485
|
+
required: ["path", "content"]
|
|
4486
|
+
},
|
|
4487
|
+
handler: async (input) => {
|
|
4488
|
+
writeFileSync2(jailedPath(cwd, String(input.path ?? "")), String(input.content ?? ""));
|
|
4489
|
+
return { ok: true };
|
|
4490
|
+
}
|
|
4491
|
+
},
|
|
4492
|
+
{
|
|
4493
|
+
name: "replace_in_file",
|
|
4494
|
+
description: "Replace one exact occurrence of a string in a repository file.",
|
|
4495
|
+
inputSchema: {},
|
|
4496
|
+
jsonSchema: {
|
|
4497
|
+
type: "object",
|
|
4498
|
+
properties: {
|
|
4499
|
+
path: { type: "string" },
|
|
4500
|
+
oldText: { type: "string" },
|
|
4501
|
+
newText: { type: "string" }
|
|
4502
|
+
},
|
|
4503
|
+
required: ["path", "oldText", "newText"]
|
|
4504
|
+
},
|
|
4505
|
+
handler: async (input) => {
|
|
4506
|
+
const path = jailedPath(cwd, String(input.path ?? ""));
|
|
4507
|
+
const content = readFileSync3(path, "utf8");
|
|
4508
|
+
const oldText = String(input.oldText ?? "");
|
|
4509
|
+
if (!content.includes(oldText)) {
|
|
4510
|
+
return { ok: false, reason: "oldText not found" };
|
|
4511
|
+
}
|
|
4512
|
+
writeFileSync2(path, content.replace(oldText, String(input.newText ?? "")));
|
|
4513
|
+
return { ok: true };
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
);
|
|
4517
|
+
}
|
|
4518
|
+
return tools;
|
|
4519
|
+
}
|
|
4520
|
+
function createOpenAIProvider(config, session) {
|
|
4521
|
+
let runId;
|
|
4522
|
+
let conversationId;
|
|
4523
|
+
const gatewayFetch = async (path, body) => {
|
|
4524
|
+
if (!runId) {
|
|
4525
|
+
throw new Error("OpenAI provider used before the run was created");
|
|
4526
|
+
}
|
|
4527
|
+
const response = await fetch(
|
|
4528
|
+
`${config.apiBaseUrl}/wizard/openai/runs/${encodeURIComponent(runId)}${path}`,
|
|
4529
|
+
{
|
|
4530
|
+
method: "POST",
|
|
4531
|
+
headers: {
|
|
4532
|
+
"Content-Type": "application/json",
|
|
4533
|
+
Authorization: `Bearer ${session.token}`
|
|
4534
|
+
},
|
|
4535
|
+
body: JSON.stringify(body)
|
|
4536
|
+
}
|
|
4537
|
+
);
|
|
4538
|
+
const text = await response.text();
|
|
4539
|
+
let payload;
|
|
4540
|
+
try {
|
|
4541
|
+
payload = text ? JSON.parse(text) : {};
|
|
4542
|
+
} catch {
|
|
4543
|
+
payload = {};
|
|
4544
|
+
}
|
|
4545
|
+
if (!response.ok) {
|
|
4546
|
+
const detail = payload.message ?? text;
|
|
4547
|
+
const error = new Error(detail || `gateway request failed (${response.status})`);
|
|
4548
|
+
error.code = payload.code;
|
|
4549
|
+
throw error;
|
|
4550
|
+
}
|
|
4551
|
+
return payload;
|
|
4552
|
+
};
|
|
4553
|
+
const ensureConversation = async (resumeId) => {
|
|
4554
|
+
if (conversationId) {
|
|
4555
|
+
return conversationId;
|
|
4556
|
+
}
|
|
4557
|
+
if (resumeId) {
|
|
4558
|
+
conversationId = resumeId;
|
|
4559
|
+
return resumeId;
|
|
4560
|
+
}
|
|
4561
|
+
try {
|
|
4562
|
+
const created = await gatewayFetch("/v1/conversations", {});
|
|
4563
|
+
conversationId = created.id;
|
|
4564
|
+
} catch (error) {
|
|
4565
|
+
if (error.code !== "conversation_already_bound") {
|
|
4566
|
+
throw error;
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
if (!conversationId) {
|
|
4570
|
+
throw new Error("wizard run already has a bound conversation; rerun with its resume state");
|
|
4571
|
+
}
|
|
4572
|
+
return conversationId;
|
|
4573
|
+
};
|
|
4574
|
+
return {
|
|
4575
|
+
name: "openai-gateway",
|
|
4576
|
+
onRunReady(readyRunId) {
|
|
4577
|
+
runId = readyRunId;
|
|
4578
|
+
},
|
|
4579
|
+
async invoke(invocation, roleConfig, onProgress) {
|
|
4580
|
+
let costUsd = 0;
|
|
4581
|
+
let turns = 0;
|
|
4582
|
+
const outcome = (kind, text = "", error = "") => kind === "completed" ? { kind, text, sessionId: conversationId, costUsd, turns } : kind === "failed" ? { kind, error, sessionId: conversationId, costUsd, turns } : { kind, sessionId: conversationId, costUsd, turns };
|
|
4583
|
+
const allTools = [...invocation.tools, ...fileTools(invocation.cwd, invocation.allowRepoWrite)];
|
|
4584
|
+
const toolsByName = new Map(allTools.map((tool2) => [tool2.name, tool2]));
|
|
4585
|
+
const toolDefs = allTools.map((tool2) => ({
|
|
4586
|
+
type: "function",
|
|
4587
|
+
name: tool2.name,
|
|
4588
|
+
description: tool2.description,
|
|
4589
|
+
parameters: tool2.jsonSchema
|
|
4590
|
+
}));
|
|
4591
|
+
try {
|
|
4592
|
+
const conversation = await ensureConversation(invocation.resumeSessionId);
|
|
4593
|
+
const deadline = Date.now() + roleConfig.maxMs;
|
|
4594
|
+
let input = [
|
|
4595
|
+
{ role: "user", content: invocation.userPrompt }
|
|
4596
|
+
];
|
|
4597
|
+
for (let turn = 0; turn < roleConfig.maxTurns; turn += 1) {
|
|
4598
|
+
turns = turn + 1;
|
|
4599
|
+
if (Date.now() > deadline) {
|
|
4600
|
+
return outcome("budget_exhausted");
|
|
4601
|
+
}
|
|
4602
|
+
const response = await gatewayFetch("/v1/responses", {
|
|
4603
|
+
model: roleConfig.model,
|
|
4604
|
+
instructions: invocation.systemPrompt,
|
|
4605
|
+
conversation,
|
|
4606
|
+
input,
|
|
4607
|
+
tools: toolDefs,
|
|
4608
|
+
...roleConfig.effort ? { reasoning: { effort: roleConfig.effort } } : {}
|
|
4609
|
+
});
|
|
4610
|
+
const items = response.output ?? [];
|
|
4611
|
+
const calls = items.filter(
|
|
4612
|
+
(item) => item.type === "function_call"
|
|
4613
|
+
);
|
|
4614
|
+
if (calls.length === 0) {
|
|
4615
|
+
const text = response.output_text ?? items.filter((item) => item.type === "message").map(
|
|
4616
|
+
(item) => Array.isArray(item.content) ? item.content.map((block) => String(block.text ?? "")).join("") : ""
|
|
4617
|
+
).join("\n");
|
|
4618
|
+
return outcome("completed", String(text ?? ""));
|
|
4619
|
+
}
|
|
4620
|
+
const results = [];
|
|
4621
|
+
for (const call of calls) {
|
|
4622
|
+
onProgress(`using ${call.name}`);
|
|
4623
|
+
const tool2 = toolsByName.get(call.name);
|
|
4624
|
+
let output;
|
|
4625
|
+
if (!tool2) {
|
|
4626
|
+
output = { ok: false, reason: `unknown tool ${call.name}` };
|
|
4627
|
+
} else {
|
|
4628
|
+
try {
|
|
4629
|
+
output = await tool2.handler(JSON.parse(call.arguments || "{}"));
|
|
4630
|
+
} catch (error) {
|
|
4631
|
+
output = { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
4632
|
+
}
|
|
4633
|
+
}
|
|
4634
|
+
results.push({
|
|
4635
|
+
type: "function_call_output",
|
|
4636
|
+
call_id: call.call_id,
|
|
4637
|
+
output: JSON.stringify(output)
|
|
4638
|
+
});
|
|
4639
|
+
}
|
|
4640
|
+
input = results;
|
|
4641
|
+
}
|
|
4642
|
+
return outcome("budget_exhausted");
|
|
4643
|
+
} catch (error) {
|
|
4644
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4645
|
+
if (classifyFailure(detail) === "context_overflow") {
|
|
4646
|
+
return outcome("context_overflow");
|
|
4647
|
+
}
|
|
4648
|
+
return outcome("failed", "", detail);
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
};
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
// src/engine/providers/router.ts
|
|
4655
|
+
function createRoutedProvider(routes, fallback) {
|
|
4656
|
+
const providers = /* @__PURE__ */ new Set([fallback, ...Object.values(routes)]);
|
|
4657
|
+
return {
|
|
4658
|
+
name: "routed",
|
|
4659
|
+
onRunReady(runId) {
|
|
4660
|
+
for (const provider of providers) {
|
|
4661
|
+
provider.onRunReady?.(runId);
|
|
4662
|
+
}
|
|
4663
|
+
},
|
|
4664
|
+
invoke(invocation, config, onProgress) {
|
|
4665
|
+
const provider = routes[invocation.role] ?? fallback;
|
|
4666
|
+
return provider.invoke(invocation, config, onProgress);
|
|
4667
|
+
}
|
|
4668
|
+
};
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4671
|
+
// src/engine/cli.ts
|
|
4672
|
+
var MINUTE = 6e4;
|
|
4673
|
+
function engineModelConfig(config) {
|
|
4674
|
+
const model = (role, fallback) => process.env[`WHISPERR_WIZARD_ENGINE_${role.toUpperCase()}_MODEL`]?.trim() || fallback;
|
|
4675
|
+
return {
|
|
4676
|
+
survey: { model: model("survey", "claude-sonnet-5"), effort: "medium", maxTurns: 40, maxMs: 12 * MINUTE },
|
|
4677
|
+
design: { model: model("design", config.plannerModel), effort: "high", maxTurns: 40, maxMs: 15 * MINUTE },
|
|
4678
|
+
edit: { model: model("edit", config.model), effort: config.effort, maxTurns: 50, maxMs: 15 * MINUTE },
|
|
4679
|
+
review: { model: model("review", config.model), effort: "medium", maxTurns: 15, maxMs: 8 * MINUTE }
|
|
4680
|
+
};
|
|
4681
|
+
}
|
|
4682
|
+
function engineProvider(config, session) {
|
|
4683
|
+
const claude = createClaudeProvider(config, session);
|
|
4684
|
+
const preset = process.env.WHISPERR_WIZARD_PRESET?.trim() ?? "claude-default";
|
|
4685
|
+
if (preset === "sol-design") {
|
|
4686
|
+
const openai = createOpenAIProvider(config, session);
|
|
4687
|
+
return createRoutedProvider({ survey: openai, design: openai, review: openai }, claude);
|
|
4688
|
+
}
|
|
4689
|
+
return claude;
|
|
4690
|
+
}
|
|
4691
|
+
async function tryEngineFlow(options) {
|
|
4692
|
+
const { repoPath, fingerprint, config, session, playbook, theme: theme2 } = options;
|
|
4693
|
+
const startedAt = Date.now();
|
|
4694
|
+
const sink = createProgressSink({
|
|
4695
|
+
json: false,
|
|
4696
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
4697
|
+
write: (line) => p.log.message(theme2.muted(line))
|
|
4698
|
+
});
|
|
4699
|
+
const result = await runEngine({
|
|
4700
|
+
repoPath,
|
|
4701
|
+
repoFingerprint: fingerprint,
|
|
4702
|
+
config,
|
|
4703
|
+
session,
|
|
4704
|
+
provider: engineProvider(config, session),
|
|
4705
|
+
models: engineModelConfig(config),
|
|
4706
|
+
target: playbook.target.id,
|
|
4707
|
+
projectKind: ["node", "python", "php", "go", "express", "django", "fastapi", "laravel", "spring-boot"].includes(
|
|
4708
|
+
playbook.target.id
|
|
4709
|
+
) ? "backend" : "frontend",
|
|
4710
|
+
playbookSystemPrompt: playbook.systemPrompt,
|
|
4711
|
+
sink,
|
|
4712
|
+
callbacks: {
|
|
4713
|
+
reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2),
|
|
4714
|
+
confirmApply: async (patch, summaryLines) => {
|
|
4715
|
+
p.note(summaryLines.join("\n"), "Integration result");
|
|
4716
|
+
const lineCount = patch.split("\n").length;
|
|
4717
|
+
const answer = await p.confirm({
|
|
4718
|
+
message: `Apply these changes to your working tree? (${lineCount} patch lines)`,
|
|
4719
|
+
initialValue: true
|
|
4720
|
+
});
|
|
4721
|
+
return answer === true;
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
});
|
|
4725
|
+
if (result.kind === "wrong_mode") {
|
|
4726
|
+
return { handled: false, exitCode: 0 };
|
|
4727
|
+
}
|
|
4728
|
+
if (result.kind === "aborted") {
|
|
4729
|
+
p.cancel(result.reason);
|
|
4730
|
+
return { handled: true, exitCode: 1 };
|
|
4731
|
+
}
|
|
4732
|
+
p.note(result.summary || "Nothing to integrate.", "Run summary");
|
|
4733
|
+
if (result.partialPatchPath) {
|
|
4734
|
+
p.log.warn(`Unapplied changes saved to ${result.partialPatchPath}`);
|
|
4735
|
+
}
|
|
4736
|
+
await postRunReport(config, session, {
|
|
4737
|
+
target: playbook.target.id,
|
|
4738
|
+
repo_fingerprint: fingerprint,
|
|
4739
|
+
identify_wired: result.identifyWired,
|
|
4740
|
+
verified: null,
|
|
4741
|
+
cost_usd: 0,
|
|
4742
|
+
duration_ms: Date.now() - startedAt,
|
|
4743
|
+
summary: result.summary.slice(0, 2e3),
|
|
4744
|
+
events: result.reportEvents,
|
|
4745
|
+
exit_class: result.kind === "completed" ? "engine_completed" : "engine_partial"
|
|
4746
|
+
});
|
|
4747
|
+
if (result.applied && result.registered.length > 0) {
|
|
4748
|
+
const spin = p.spinner();
|
|
4749
|
+
spin.start("Waiting for the first event (deploy or run your app)");
|
|
4750
|
+
const first = await pollFirstEvent(config, session, { timeoutMs: 9e4 });
|
|
4751
|
+
spin.stop(
|
|
4752
|
+
first.received ? theme2.bright(`Whisperr received ${first.eventType ?? "an event"} \u2713`) : theme2.muted("No event yet \u2014 it will show up once the instrumented build runs.")
|
|
4753
|
+
);
|
|
4754
|
+
}
|
|
4755
|
+
return { handled: true, exitCode: result.kind === "completed" ? 0 : 1 };
|
|
4756
|
+
}
|
|
4757
|
+
async function reviewEventsInTerminal(proposed, theme2) {
|
|
4758
|
+
if (proposed.length === 0) {
|
|
4759
|
+
return [];
|
|
4760
|
+
}
|
|
4761
|
+
const lines = proposed.map((event) => {
|
|
4762
|
+
const fields = Object.keys(event.payloadSchema);
|
|
4763
|
+
return [
|
|
4764
|
+
`${theme2.bright(event.code)} \u2014 ${event.name}`,
|
|
4765
|
+
` ${event.reasoning}`,
|
|
4766
|
+
` anchor: ${event.anchorFile}${event.anchorSymbol ? ` (${event.anchorSymbol})` : ""}`,
|
|
4767
|
+
fields.length > 0 ? ` payload: ${fields.join(", ")}` : " payload: none"
|
|
4768
|
+
].join("\n");
|
|
4769
|
+
});
|
|
4770
|
+
p.note(lines.join("\n\n"), `Proposed events (${proposed.length})`);
|
|
4771
|
+
const selection = await p.multiselect({
|
|
4772
|
+
message: "Register these events? Deselect any you don't want.",
|
|
4773
|
+
options: proposed.map((event) => ({ value: event.code, label: event.code, hint: event.name })),
|
|
4774
|
+
initialValues: proposed.map((event) => event.code),
|
|
4775
|
+
required: false
|
|
4776
|
+
});
|
|
4777
|
+
if (p.isCancel(selection)) {
|
|
4778
|
+
return null;
|
|
4779
|
+
}
|
|
4780
|
+
const chosen = new Set(selection);
|
|
4781
|
+
return proposed.filter((event) => chosen.has(event.code));
|
|
4782
|
+
}
|
|
4783
|
+
|
|
2694
4784
|
// src/core/opportunities.ts
|
|
2695
4785
|
function normalizeCode(value) {
|
|
2696
4786
|
return value.trim().toLowerCase().replace(/[ \-./]/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
@@ -2788,8 +4878,8 @@ async function fetchSuggestions(config, session, statuses) {
|
|
|
2788
4878
|
}
|
|
2789
4879
|
function isSuggestionRecord(value) {
|
|
2790
4880
|
if (typeof value !== "object" || value === null) return false;
|
|
2791
|
-
const
|
|
2792
|
-
return typeof
|
|
4881
|
+
const record2 = value;
|
|
4882
|
+
return typeof record2.id === "string" && (record2.kind === "event" || record2.kind === "intervention") && typeof record2.code === "string" && (record2.status === "proposed" || record2.status === "approved" || record2.status === "rejected" || record2.status === "integrated") && typeof record2.payload === "object" && record2.payload !== null;
|
|
2793
4883
|
}
|
|
2794
4884
|
async function markSuggestionIntegrated(config, session, id, target9, repoFingerprint2) {
|
|
2795
4885
|
if (config.offline) return false;
|
|
@@ -2909,33 +4999,8 @@ function normalizeSide(value) {
|
|
|
2909
4999
|
}
|
|
2910
5000
|
}
|
|
2911
5001
|
|
|
2912
|
-
// src/core/verify.ts
|
|
2913
|
-
async function pollFirstEvent(config, session, opts = {}) {
|
|
2914
|
-
if (config.offline) return { received: false };
|
|
2915
|
-
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
2916
|
-
const intervalMs = opts.intervalMs ?? 3e3;
|
|
2917
|
-
const deadline = Date.now() + timeoutMs;
|
|
2918
|
-
while (Date.now() < deadline) {
|
|
2919
|
-
if (opts.signal?.aborted) return { received: false };
|
|
2920
|
-
try {
|
|
2921
|
-
const res = await fetch(`${config.apiBaseUrl}/wizard/first-event`, {
|
|
2922
|
-
headers: { authorization: `Bearer ${session.token}` }
|
|
2923
|
-
});
|
|
2924
|
-
if (res.ok) {
|
|
2925
|
-
const body = await res.json();
|
|
2926
|
-
if (body.received) {
|
|
2927
|
-
return { received: true, eventType: body.event_type };
|
|
2928
|
-
}
|
|
2929
|
-
}
|
|
2930
|
-
} catch {
|
|
2931
|
-
}
|
|
2932
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
2933
|
-
}
|
|
2934
|
-
return { received: false };
|
|
2935
|
-
}
|
|
2936
|
-
|
|
2937
5002
|
// src/core/postflight.ts
|
|
2938
|
-
import { spawn as
|
|
5003
|
+
import { spawn as spawn4 } from "child_process";
|
|
2939
5004
|
function verdictToVerified(verdict) {
|
|
2940
5005
|
if (verdict.toolMissing || verdict.timedOut) return null;
|
|
2941
5006
|
return verdict.ok;
|
|
@@ -2947,8 +5012,8 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2947
5012
|
return Promise.resolve({ ran: false, ok: true, output: "" });
|
|
2948
5013
|
}
|
|
2949
5014
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2950
|
-
return new Promise((
|
|
2951
|
-
const child =
|
|
5015
|
+
return new Promise((resolve4) => {
|
|
5016
|
+
const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
|
|
2952
5017
|
let out = "";
|
|
2953
5018
|
const append = (d) => {
|
|
2954
5019
|
out += d.toString();
|
|
@@ -2958,11 +5023,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2958
5023
|
child.stderr?.on("data", append);
|
|
2959
5024
|
const timer = setTimeout(() => {
|
|
2960
5025
|
child.kill("SIGKILL");
|
|
2961
|
-
|
|
5026
|
+
resolve4({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
|
|
2962
5027
|
}, timeoutMs);
|
|
2963
5028
|
child.on("close", (code) => {
|
|
2964
5029
|
clearTimeout(timer);
|
|
2965
|
-
|
|
5030
|
+
resolve4({
|
|
2966
5031
|
ran: true,
|
|
2967
5032
|
ok: code === 0,
|
|
2968
5033
|
command,
|
|
@@ -2974,7 +5039,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2974
5039
|
});
|
|
2975
5040
|
child.on("error", () => {
|
|
2976
5041
|
clearTimeout(timer);
|
|
2977
|
-
|
|
5042
|
+
resolve4({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
|
|
2978
5043
|
});
|
|
2979
5044
|
});
|
|
2980
5045
|
}
|
|
@@ -2983,68 +5048,22 @@ function tail2(s) {
|
|
|
2983
5048
|
return t.length > MAX_OUTPUT ? `\u2026${t.slice(-MAX_OUTPUT)}` : t;
|
|
2984
5049
|
}
|
|
2985
5050
|
|
|
2986
|
-
// src/core/report.ts
|
|
2987
|
-
function scrubSummary(text) {
|
|
2988
|
-
return scrubTerminalControls(text).replace(/sk-ant-[A-Za-z0-9_-]+/g, "[redacted]").replace(/sk-[A-Za-z0-9]{16,}/g, "[redacted]").replace(/AKIA[0-9A-Z]{16}/g, "[redacted]").replace(/Bearer\s+[A-Za-z0-9._-]+/g, "[redacted]").replace(
|
|
2989
|
-
/\b(ANTHROPIC_(?:AUTH_TOKEN|API_KEY))\s*=\s*["']?[^"'\s]+["']?/gi,
|
|
2990
|
-
"$1=[redacted]"
|
|
2991
|
-
);
|
|
2992
|
-
}
|
|
2993
|
-
async function postRunReport(config, session, report) {
|
|
2994
|
-
if (config.offline) return { ok: true };
|
|
2995
|
-
try {
|
|
2996
|
-
const res = await fetch(`${config.apiBaseUrl}/wizard/report`, {
|
|
2997
|
-
method: "POST",
|
|
2998
|
-
headers: {
|
|
2999
|
-
"Content-Type": "application/json",
|
|
3000
|
-
Authorization: `Bearer ${session.token}`
|
|
3001
|
-
},
|
|
3002
|
-
body: JSON.stringify(report)
|
|
3003
|
-
});
|
|
3004
|
-
if (res.ok) return { ok: true };
|
|
3005
|
-
return {
|
|
3006
|
-
ok: false,
|
|
3007
|
-
status: res.status,
|
|
3008
|
-
detail: await responseDetail(res)
|
|
3009
|
-
};
|
|
3010
|
-
} catch (err) {
|
|
3011
|
-
return { ok: false, detail: detailSlice(errorMessage(err)) };
|
|
3012
|
-
}
|
|
3013
|
-
}
|
|
3014
|
-
async function responseDetail(res) {
|
|
3015
|
-
try {
|
|
3016
|
-
return detailSlice(await res.text());
|
|
3017
|
-
} catch (err) {
|
|
3018
|
-
return detailSlice(errorMessage(err));
|
|
3019
|
-
}
|
|
3020
|
-
}
|
|
3021
|
-
function errorMessage(err) {
|
|
3022
|
-
return err instanceof Error ? err.message : String(err);
|
|
3023
|
-
}
|
|
3024
|
-
function detailSlice(detail) {
|
|
3025
|
-
const sliced = detail.slice(0, 200).trim();
|
|
3026
|
-
return sliced || void 0;
|
|
3027
|
-
}
|
|
3028
|
-
function scrubTerminalControls(value) {
|
|
3029
|
-
return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
3030
|
-
}
|
|
3031
|
-
|
|
3032
5051
|
// src/core/version.ts
|
|
3033
|
-
import { readFileSync } from "fs";
|
|
3034
|
-
import { dirname as
|
|
5052
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
5053
|
+
import { dirname as dirname3, join as join9, parse } from "path";
|
|
3035
5054
|
import { fileURLToPath } from "url";
|
|
3036
5055
|
var PACKAGE_NAME = "@whisperr/wizard";
|
|
3037
5056
|
function packageVersion() {
|
|
3038
|
-
let dir =
|
|
5057
|
+
let dir = dirname3(fileURLToPath(import.meta.url));
|
|
3039
5058
|
const { root } = parse(dir);
|
|
3040
5059
|
while (true) {
|
|
3041
5060
|
try {
|
|
3042
|
-
const pkg = JSON.parse(
|
|
5061
|
+
const pkg = JSON.parse(readFileSync4(join9(dir, "package.json"), "utf8"));
|
|
3043
5062
|
if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
|
|
3044
5063
|
} catch {
|
|
3045
5064
|
}
|
|
3046
5065
|
if (dir === root) return "0.0.0";
|
|
3047
|
-
dir =
|
|
5066
|
+
dir = dirname3(dir);
|
|
3048
5067
|
}
|
|
3049
5068
|
}
|
|
3050
5069
|
|
|
@@ -3177,12 +5196,12 @@ function banner() {
|
|
|
3177
5196
|
|
|
3178
5197
|
// src/cli.ts
|
|
3179
5198
|
async function run2(options) {
|
|
3180
|
-
const repoPath =
|
|
5199
|
+
const repoPath = resolve3(options.path ?? process.cwd());
|
|
3181
5200
|
const config = resolveConfig(options);
|
|
3182
5201
|
console.log(banner());
|
|
3183
|
-
|
|
5202
|
+
p2.intro(theme.signal("Let's wire Whisperr into your app."));
|
|
3184
5203
|
if (config.offline) {
|
|
3185
|
-
|
|
5204
|
+
p2.log.warn(
|
|
3186
5205
|
theme.warn("offline mode") + theme.muted(" \u2014 using a demo manifest, no account needed.")
|
|
3187
5206
|
);
|
|
3188
5207
|
}
|
|
@@ -3192,27 +5211,27 @@ async function run2(options) {
|
|
|
3192
5211
|
);
|
|
3193
5212
|
const chosen = await chooseTarget(detections);
|
|
3194
5213
|
if (!chosen) {
|
|
3195
|
-
|
|
5214
|
+
p2.cancel("No supported stack selected.");
|
|
3196
5215
|
return 1;
|
|
3197
5216
|
}
|
|
3198
5217
|
if (chosen.playbook.target.availability === "planned") {
|
|
3199
|
-
|
|
5218
|
+
p2.note(
|
|
3200
5219
|
`We can detect ${theme.bright(chosen.playbook.target.displayName)} but the Whisperr SDK for it isn't shipped yet.
|
|
3201
5220
|
Flutter is live today; ${theme.bright(
|
|
3202
5221
|
chosen.playbook.target.displayName
|
|
3203
5222
|
)} is next on the roadmap.`,
|
|
3204
5223
|
theme.warn("SDK coming soon")
|
|
3205
5224
|
);
|
|
3206
|
-
const cont = await
|
|
5225
|
+
const cont = await p2.confirm({
|
|
3207
5226
|
message: "Continue anyway and let the agent scaffold against the planned SDK?",
|
|
3208
5227
|
initialValue: false
|
|
3209
5228
|
});
|
|
3210
|
-
if (
|
|
3211
|
-
|
|
5229
|
+
if (p2.isCancel(cont) || !cont) {
|
|
5230
|
+
p2.outro(theme.muted("No problem \u2014 come back when the SDK lands."));
|
|
3212
5231
|
return 0;
|
|
3213
5232
|
}
|
|
3214
5233
|
} else {
|
|
3215
|
-
|
|
5234
|
+
p2.log.success(
|
|
3216
5235
|
`Detected ${theme.bright(chosen.playbook.target.displayName)} ` + theme.muted(`(${chosen.detection?.evidence.join(", ") ?? "selected"})`)
|
|
3217
5236
|
);
|
|
3218
5237
|
}
|
|
@@ -3220,43 +5239,56 @@ Flutter is live today; ${theme.bright(
|
|
|
3220
5239
|
try {
|
|
3221
5240
|
session = config.offline ? await authenticate(config) : await withBrowserAuth(config);
|
|
3222
5241
|
} catch (err) {
|
|
3223
|
-
|
|
5242
|
+
p2.cancel(theme.alert(err.message));
|
|
3224
5243
|
return 1;
|
|
3225
5244
|
}
|
|
3226
5245
|
startSessionKeepalive(config, session);
|
|
3227
5246
|
const fingerprint = await repoFingerprint(repoPath);
|
|
5247
|
+
if (!config.offline) {
|
|
5248
|
+
const engineOutcome = await tryEngineFlow({
|
|
5249
|
+
repoPath,
|
|
5250
|
+
fingerprint,
|
|
5251
|
+
config,
|
|
5252
|
+
session,
|
|
5253
|
+
playbook: chosen.playbook,
|
|
5254
|
+
theme
|
|
5255
|
+
});
|
|
5256
|
+
if (engineOutcome.handled) {
|
|
5257
|
+
return engineOutcome.exitCode;
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
3228
5260
|
const manifest = await withSpinner(
|
|
3229
5261
|
"Loading your onboarding context",
|
|
3230
5262
|
() => fetchManifest(config, session, chosen.playbook.target.id, fingerprint)
|
|
3231
5263
|
);
|
|
3232
5264
|
const approvedSuggestionsPromise = config.offline ? null : fetchSuggestions(config, session, ["approved"]);
|
|
3233
|
-
|
|
5265
|
+
p2.note(
|
|
3234
5266
|
summarizeManifest(manifest),
|
|
3235
5267
|
theme.signal(`Plan for ${manifest.appName ?? manifest.appId}`)
|
|
3236
5268
|
);
|
|
3237
5269
|
const checkpoint = await takeCheckpoint(repoPath);
|
|
3238
5270
|
if (!checkpoint.isRepo) {
|
|
3239
5271
|
if (!options.force) {
|
|
3240
|
-
|
|
5272
|
+
p2.cancel(
|
|
3241
5273
|
theme.alert("Not a git repository.") + " The wizard edits your code and relies on git to undo safely.\n" + theme.muted("Run `git init` and commit first, or re-run with --force to proceed without a safety net.")
|
|
3242
5274
|
);
|
|
3243
5275
|
return 1;
|
|
3244
5276
|
}
|
|
3245
|
-
|
|
5277
|
+
p2.log.warn(
|
|
3246
5278
|
theme.warn("not a git repo") + theme.muted(" \u2014 proceeding with --force; there is no automatic undo.")
|
|
3247
5279
|
);
|
|
3248
5280
|
} else if (!await isWorkingTreeClean(repoPath)) {
|
|
3249
5281
|
if (!options.force) {
|
|
3250
|
-
|
|
5282
|
+
p2.cancel(
|
|
3251
5283
|
theme.alert("Your working tree has uncommitted changes.") + " Commit or stash them first so the wizard's edits stay isolated and reversible.\n" + theme.muted("Or re-run with --force to proceed anyway.")
|
|
3252
5284
|
);
|
|
3253
5285
|
return 1;
|
|
3254
5286
|
}
|
|
3255
|
-
|
|
5287
|
+
p2.log.warn(
|
|
3256
5288
|
theme.warn("uncommitted changes present") + theme.muted(" \u2014 proceeding with --force; a revert would also drop your own changes.")
|
|
3257
5289
|
);
|
|
3258
5290
|
}
|
|
3259
|
-
const spin =
|
|
5291
|
+
const spin = p2.spinner();
|
|
3260
5292
|
const useIntegrationSpinner = Boolean(process.stdout.isTTY);
|
|
3261
5293
|
let phaseLabel = "Integrating";
|
|
3262
5294
|
let lastLine = "";
|
|
@@ -3272,7 +5304,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3272
5304
|
if (useIntegrationSpinner) {
|
|
3273
5305
|
spin.start(theme.bright(phaseLabel));
|
|
3274
5306
|
} else {
|
|
3275
|
-
|
|
5307
|
+
p2.log.step(theme.bright(phaseLabel));
|
|
3276
5308
|
}
|
|
3277
5309
|
const tick = useIntegrationSpinner ? setInterval(updateIntegrationMessage, 1e3) : void 0;
|
|
3278
5310
|
const additions = { proposed: 0, applied: [] };
|
|
@@ -3290,11 +5322,11 @@ Flutter is live today; ${theme.bright(
|
|
|
3290
5322
|
if (useIntegrationSpinner) {
|
|
3291
5323
|
spin.stop(theme.success("Placement plan ready"));
|
|
3292
5324
|
}
|
|
3293
|
-
|
|
5325
|
+
p2.note(renderPlacementPlan(plan), "Placement plan");
|
|
3294
5326
|
const placeEntries = plan.filter((entry) => entry.decision === "place");
|
|
3295
5327
|
let selectedEvents = placeEntries.map((entry) => entry.event);
|
|
3296
5328
|
if (config.reviewPlan && interactive && placeEntries.length) {
|
|
3297
|
-
const selection = await
|
|
5329
|
+
const selection = await p2.multiselect({
|
|
3298
5330
|
message: "Wire these events?",
|
|
3299
5331
|
options: placeEntries.map((entry) => ({
|
|
3300
5332
|
value: entry.event,
|
|
@@ -3304,7 +5336,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3304
5336
|
initialValues: selectedEvents,
|
|
3305
5337
|
required: false
|
|
3306
5338
|
});
|
|
3307
|
-
selectedEvents =
|
|
5339
|
+
selectedEvents = p2.isCancel(selection) ? [] : selection;
|
|
3308
5340
|
}
|
|
3309
5341
|
if (useIntegrationSpinner) {
|
|
3310
5342
|
spin.start(theme.bright("Continuing integration"));
|
|
@@ -3333,7 +5365,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3333
5365
|
if (useIntegrationSpinner) {
|
|
3334
5366
|
updateIntegrationMessage();
|
|
3335
5367
|
} else {
|
|
3336
|
-
|
|
5368
|
+
p2.log.step(theme.bright(label));
|
|
3337
5369
|
}
|
|
3338
5370
|
},
|
|
3339
5371
|
onActivity(line) {
|
|
@@ -3344,7 +5376,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3344
5376
|
}
|
|
3345
5377
|
}
|
|
3346
5378
|
}).catch((err) => {
|
|
3347
|
-
|
|
5379
|
+
p2.log.error(err.message);
|
|
3348
5380
|
return null;
|
|
3349
5381
|
});
|
|
3350
5382
|
if (tick) clearInterval(tick);
|
|
@@ -3400,7 +5432,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3400
5432
|
opportunities_applied: additions.applied.length
|
|
3401
5433
|
});
|
|
3402
5434
|
if (!result.ok) {
|
|
3403
|
-
|
|
5435
|
+
p2.log.warn(
|
|
3404
5436
|
theme.warn("Couldn't record this run's coverage on the server") + theme.muted(
|
|
3405
5437
|
` (${formatReportFailure(result)}) \u2014 future runs may re-propose events already wired here.`
|
|
3406
5438
|
)
|
|
@@ -3412,7 +5444,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3412
5444
|
if (useIntegrationSpinner) {
|
|
3413
5445
|
spin.stop(theme.alert("Integration stopped"));
|
|
3414
5446
|
} else {
|
|
3415
|
-
|
|
5447
|
+
p2.log.error(theme.alert("Integration stopped"));
|
|
3416
5448
|
}
|
|
3417
5449
|
const reverted = await maybeRevert(
|
|
3418
5450
|
repoPath,
|
|
@@ -3426,13 +5458,13 @@ Flutter is live today; ${theme.bright(
|
|
|
3426
5458
|
if (useIntegrationSpinner) {
|
|
3427
5459
|
spin.stop(stopLabel);
|
|
3428
5460
|
} else if (!outcome.coreOk) {
|
|
3429
|
-
|
|
5461
|
+
p2.log.warn(stopLabel);
|
|
3430
5462
|
} else {
|
|
3431
|
-
|
|
5463
|
+
p2.log.success(stopLabel);
|
|
3432
5464
|
}
|
|
3433
5465
|
let files = await changedFiles(repoPath, checkpoint);
|
|
3434
5466
|
if (files.length) {
|
|
3435
|
-
|
|
5467
|
+
p2.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
3436
5468
|
}
|
|
3437
5469
|
logAgentSummary(outcome.summary);
|
|
3438
5470
|
if (!outcome.coreOk) {
|
|
@@ -3443,14 +5475,14 @@ Flutter is live today; ${theme.bright(
|
|
|
3443
5475
|
);
|
|
3444
5476
|
if (reverted) {
|
|
3445
5477
|
await reportRun("reverted");
|
|
3446
|
-
|
|
5478
|
+
p2.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
3447
5479
|
return 1;
|
|
3448
5480
|
}
|
|
3449
5481
|
await reportRun("core_failed");
|
|
3450
5482
|
}
|
|
3451
5483
|
if (chosen.playbook.verifyCommand && files.length) {
|
|
3452
5484
|
const cmd = chosen.playbook.verifyCommand;
|
|
3453
|
-
const vspin =
|
|
5485
|
+
const vspin = p2.spinner();
|
|
3454
5486
|
vspin.start(`Verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
3455
5487
|
let verdict = await runVerifyCommand(repoPath, cmd);
|
|
3456
5488
|
verified = verdictToVerified(verdict);
|
|
@@ -3470,7 +5502,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3470
5502
|
vspin.stop(
|
|
3471
5503
|
theme.warn(`Verification failed \u2014 ${cmd}`) + theme.muted(" \u2014 attempting one repair pass")
|
|
3472
5504
|
);
|
|
3473
|
-
const repairSpin =
|
|
5505
|
+
const repairSpin = p2.spinner();
|
|
3474
5506
|
repairSpin.start(`Repairing verifier failures \u2014 ${theme.muted(cmd)}`);
|
|
3475
5507
|
try {
|
|
3476
5508
|
const repair = await runRepairPass({
|
|
@@ -3495,7 +5527,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3495
5527
|
);
|
|
3496
5528
|
}
|
|
3497
5529
|
files = await changedFiles(repoPath, checkpoint);
|
|
3498
|
-
const reverifySpin =
|
|
5530
|
+
const reverifySpin = p2.spinner();
|
|
3499
5531
|
reverifySpin.start(`Re-verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
3500
5532
|
verdict = await runVerifyCommand(repoPath, cmd);
|
|
3501
5533
|
verified = verdictToVerified(verdict);
|
|
@@ -3519,7 +5551,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3519
5551
|
}
|
|
3520
5552
|
if (!verdict.ok && !verdict.toolMissing && !verdict.timedOut) {
|
|
3521
5553
|
if (verdict.output) {
|
|
3522
|
-
|
|
5554
|
+
p2.note(verdict.output, theme.warn("Verifier output"));
|
|
3523
5555
|
}
|
|
3524
5556
|
const reverted = await maybeRevert(
|
|
3525
5557
|
repoPath,
|
|
@@ -3528,7 +5560,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3528
5560
|
);
|
|
3529
5561
|
if (reverted) {
|
|
3530
5562
|
await reportRun("reverted");
|
|
3531
|
-
|
|
5563
|
+
p2.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
3532
5564
|
return 1;
|
|
3533
5565
|
}
|
|
3534
5566
|
await reportRun("verify_failed");
|
|
@@ -3568,7 +5600,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3568
5600
|
)
|
|
3569
5601
|
)).filter(Boolean).length;
|
|
3570
5602
|
if (marked > 0) {
|
|
3571
|
-
|
|
5603
|
+
p2.log.info(
|
|
3572
5604
|
theme.muted(
|
|
3573
5605
|
`${marked} approved suggestion${marked === 1 ? "" : "s"} marked integrated.`
|
|
3574
5606
|
)
|
|
@@ -3579,7 +5611,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3579
5611
|
}
|
|
3580
5612
|
const eventLines = renderEventOutcomeLines(outcome.eventOutcomes, wiredMap);
|
|
3581
5613
|
if (eventLines) {
|
|
3582
|
-
|
|
5614
|
+
p2.note(eventLines, "Events");
|
|
3583
5615
|
}
|
|
3584
5616
|
const gapReport = buildGapReport({
|
|
3585
5617
|
manifest,
|
|
@@ -3590,7 +5622,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3590
5622
|
}))
|
|
3591
5623
|
});
|
|
3592
5624
|
if (gapReport.hasContent) {
|
|
3593
|
-
|
|
5625
|
+
p2.note(
|
|
3594
5626
|
renderGapReport(gapReport, theme),
|
|
3595
5627
|
theme.signal("What you could be doing")
|
|
3596
5628
|
);
|
|
@@ -3598,16 +5630,16 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3598
5630
|
const eventDenominator = manifest.universeSummary?.wireableHere ?? reportEvents.length;
|
|
3599
5631
|
const eventStatsLabel = manifest.universeSummary ? "events wired here" : "events wired";
|
|
3600
5632
|
const timingDetails = outcome.phaseTimings.map(({ phase, ms }) => `${shortPhaseLabel(phase)} ${formatDuration(ms)}`).join(" \xB7 ");
|
|
3601
|
-
|
|
5633
|
+
p2.log.info(
|
|
3602
5634
|
theme.muted(
|
|
3603
5635
|
`${wiredMap.size}/${eventDenominator} ${eventStatsLabel} \xB7 ${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ` + (verified === true ? "verified \xB7 " : verified === false ? "unverified \xB7 " : "") + formatDuration(outcome.durationMs) + (timingDetails ? ` (${timingDetails})` : "")
|
|
3604
5636
|
)
|
|
3605
5637
|
);
|
|
3606
5638
|
if (!config.offline) {
|
|
3607
|
-
|
|
5639
|
+
p2.log.step(
|
|
3608
5640
|
theme.bright("Run your app once") + theme.muted(" and trigger any tracked action \u2014 I'll watch for the first event.")
|
|
3609
5641
|
);
|
|
3610
|
-
const verifySpin =
|
|
5642
|
+
const verifySpin = p2.spinner();
|
|
3611
5643
|
verifySpin.start("Waiting for the first event from your app");
|
|
3612
5644
|
const first = await pollFirstEvent(config, session, { timeoutMs: 12e4 });
|
|
3613
5645
|
if (first.received) {
|
|
@@ -3631,8 +5663,8 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3631
5663
|
`${theme.signal("3.")} Commit & deploy \u2014 Whisperr starts working on real users.`,
|
|
3632
5664
|
undo ? theme.muted(`Undo everything: ${undo}`) : ""
|
|
3633
5665
|
].filter(Boolean).join("\n");
|
|
3634
|
-
|
|
3635
|
-
|
|
5666
|
+
p2.note(nextSteps, theme.bright("Next"));
|
|
5667
|
+
p2.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
|
|
3636
5668
|
return 0;
|
|
3637
5669
|
}
|
|
3638
5670
|
async function applyOpportunities(opts) {
|
|
@@ -3651,7 +5683,7 @@ async function applyOpportunities(opts) {
|
|
|
3651
5683
|
additions.proposed = opportunities.events.length + opportunities.interventions.length;
|
|
3652
5684
|
if (additions.proposed === 0) return [];
|
|
3653
5685
|
const describe = (kind, code, why) => `${theme.muted(`${kind}: `)}${theme.bright(code)}${why ? theme.muted(` \u2014 ${why}`) : ""}`;
|
|
3654
|
-
|
|
5686
|
+
p2.note(
|
|
3655
5687
|
[
|
|
3656
5688
|
...opportunities.events.map((e) => describe("event", e.code, e.rationale ?? e.description)),
|
|
3657
5689
|
...opportunities.interventions.map(
|
|
@@ -3661,7 +5693,7 @@ async function applyOpportunities(opts) {
|
|
|
3661
5693
|
theme.signal("Opportunities found beyond your onboarding plan")
|
|
3662
5694
|
);
|
|
3663
5695
|
if (config.offline) {
|
|
3664
|
-
|
|
5696
|
+
p2.log.info(theme.muted("Offline mode \u2014 proposals shown only, nothing submitted."));
|
|
3665
5697
|
return [];
|
|
3666
5698
|
}
|
|
3667
5699
|
if (config.proposeOnly) {
|
|
@@ -3675,7 +5707,7 @@ async function applyOpportunities(opts) {
|
|
|
3675
5707
|
() => submitAdditions(config, session, playbook.target.id, fingerprint, opportunities)
|
|
3676
5708
|
);
|
|
3677
5709
|
} catch (err) {
|
|
3678
|
-
|
|
5710
|
+
p2.log.warn(
|
|
3679
5711
|
theme.warn("Couldn't add the proposals") + theme.muted(` \u2014 ${err.message}. Your universe is unchanged.`)
|
|
3680
5712
|
);
|
|
3681
5713
|
return [];
|
|
@@ -3707,9 +5739,9 @@ async function applyOpportunities(opts) {
|
|
|
3707
5739
|
)
|
|
3708
5740
|
);
|
|
3709
5741
|
}
|
|
3710
|
-
|
|
5742
|
+
p2.note(lines.join("\n"), theme.signal("Universe updated"));
|
|
3711
5743
|
if (result.policyRegen?.status === "failed") {
|
|
3712
|
-
|
|
5744
|
+
p2.log.warn(
|
|
3713
5745
|
theme.warn("Live runtime NOT updated") + theme.muted(
|
|
3714
5746
|
" \u2014 the additions were recorded, but the runtime policy wasn't regenerated" + (result.policyRegen.reason ? `: ${result.policyRegen.reason}` : "") + ". They won't drive live decisions until the policy regenerates."
|
|
3715
5747
|
)
|
|
@@ -3730,13 +5762,13 @@ async function stageOpportunities(config, session, playbook, fingerprint, opport
|
|
|
3730
5762
|
() => submitSuggestions(config, session, playbook.target.id, fingerprint, opportunities)
|
|
3731
5763
|
);
|
|
3732
5764
|
} catch (err) {
|
|
3733
|
-
|
|
5765
|
+
p2.log.warn(
|
|
3734
5766
|
theme.warn("Couldn't send the proposals") + theme.muted(` \u2014 ${err.message}. Nothing was submitted.`)
|
|
3735
5767
|
);
|
|
3736
5768
|
return;
|
|
3737
5769
|
}
|
|
3738
5770
|
if (!stageResult) {
|
|
3739
|
-
|
|
5771
|
+
p2.log.warn(
|
|
3740
5772
|
theme.warn("This Whisperr backend has no approval queue") + theme.muted(" \u2014 nothing was submitted. Re-run without --propose-only to add them directly.")
|
|
3741
5773
|
);
|
|
3742
5774
|
return;
|
|
@@ -3751,9 +5783,9 @@ async function stageOpportunities(config, session, playbook, fingerprint, opport
|
|
|
3751
5783
|
`${stageResult.staged} staged \xB7 ${stageResult.duplicates} already queued \xB7 ${stageResult.invalid} rejected`
|
|
3752
5784
|
)
|
|
3753
5785
|
);
|
|
3754
|
-
|
|
5786
|
+
p2.note(lines.join("\n"), theme.signal("Suggestions submitted"));
|
|
3755
5787
|
const where = stageResult.approvalsUrl ? `approve at ${theme.bright(stageResult.approvalsUrl)}` : "approve them in your Whisperr dashboard";
|
|
3756
|
-
|
|
5788
|
+
p2.log.info(
|
|
3757
5789
|
theme.muted(
|
|
3758
5790
|
`${stageResult.staged} proposal${stageResult.staged === 1 ? "" : "s"} sent \u2014 ${where}. The next wizard run wires whatever you approve.`
|
|
3759
5791
|
)
|
|
@@ -3763,20 +5795,20 @@ async function maybeRevert(repoPath, checkpoint, reason) {
|
|
|
3763
5795
|
if (!checkpoint.isRepo) return false;
|
|
3764
5796
|
const files = await changedFiles(repoPath, checkpoint);
|
|
3765
5797
|
if (files.length === 0) return false;
|
|
3766
|
-
const doRevert = await
|
|
5798
|
+
const doRevert = await p2.confirm({
|
|
3767
5799
|
message: `${reason} Revert all of the wizard's changes now?`,
|
|
3768
5800
|
initialValue: true
|
|
3769
5801
|
});
|
|
3770
|
-
if (
|
|
5802
|
+
if (p2.isCancel(doRevert) || !doRevert) {
|
|
3771
5803
|
const hint = revertHint(checkpoint);
|
|
3772
|
-
if (hint)
|
|
5804
|
+
if (hint) p2.log.info(theme.muted(`Left in your working tree. Undo anytime: ${hint}`));
|
|
3773
5805
|
return false;
|
|
3774
5806
|
}
|
|
3775
5807
|
const ok = await revertToCheckpoint(repoPath, checkpoint);
|
|
3776
5808
|
if (ok) {
|
|
3777
|
-
|
|
5809
|
+
p2.log.success(theme.success("Reverted to your pre-wizard state."));
|
|
3778
5810
|
} else {
|
|
3779
|
-
|
|
5811
|
+
p2.log.warn(
|
|
3780
5812
|
theme.warn("Couldn't revert automatically \u2014 undo manually: ") + (revertHint(checkpoint) ?? "git reset --hard")
|
|
3781
5813
|
);
|
|
3782
5814
|
}
|
|
@@ -3807,11 +5839,11 @@ async function chooseTarget(detections) {
|
|
|
3807
5839
|
hint: pb.target.availability === "available" ? void 0 : "coming soon"
|
|
3808
5840
|
});
|
|
3809
5841
|
}
|
|
3810
|
-
const answer = await
|
|
5842
|
+
const answer = await p2.select({
|
|
3811
5843
|
message: detections.length ? "I found more than one possible stack \u2014 which should I integrate?" : "I couldn't auto-detect your stack. Which are you using?",
|
|
3812
5844
|
options
|
|
3813
5845
|
});
|
|
3814
|
-
if (
|
|
5846
|
+
if (p2.isCancel(answer)) return null;
|
|
3815
5847
|
const playbook = playbookByTargetId(answer);
|
|
3816
5848
|
if (!playbook) return null;
|
|
3817
5849
|
return {
|
|
@@ -3820,11 +5852,11 @@ async function chooseTarget(detections) {
|
|
|
3820
5852
|
};
|
|
3821
5853
|
}
|
|
3822
5854
|
async function withBrowserAuth(config) {
|
|
3823
|
-
|
|
5855
|
+
p2.log.step(
|
|
3824
5856
|
theme.bright("Authenticate") + theme.muted(" \u2014 opening your browser to approve this device\u2026")
|
|
3825
5857
|
);
|
|
3826
5858
|
const auth = await startDeviceAuth(config);
|
|
3827
|
-
|
|
5859
|
+
p2.note(
|
|
3828
5860
|
[
|
|
3829
5861
|
theme.bright(`Your code: ${auth.userCode}`),
|
|
3830
5862
|
`${theme.muted("Approval URL:")} ${auth.verificationUrl}`,
|
|
@@ -3838,7 +5870,7 @@ async function withBrowserAuth(config) {
|
|
|
3838
5870
|
await open2(auth.verificationUrlComplete ?? auth.verificationUrl);
|
|
3839
5871
|
} catch {
|
|
3840
5872
|
}
|
|
3841
|
-
const spin =
|
|
5873
|
+
const spin = p2.spinner();
|
|
3842
5874
|
spin.start("Waiting for you to approve in the browser");
|
|
3843
5875
|
try {
|
|
3844
5876
|
const session = await auth.poll();
|
|
@@ -3850,7 +5882,7 @@ async function withBrowserAuth(config) {
|
|
|
3850
5882
|
}
|
|
3851
5883
|
}
|
|
3852
5884
|
async function withSpinner(label, fn) {
|
|
3853
|
-
const spin =
|
|
5885
|
+
const spin = p2.spinner();
|
|
3854
5886
|
spin.start(label);
|
|
3855
5887
|
try {
|
|
3856
5888
|
const result = await fn();
|
|
@@ -3913,7 +5945,7 @@ function renderEventOutcomeLines(outcomes, wiredMap) {
|
|
|
3913
5945
|
}
|
|
3914
5946
|
function logAgentSummary(summary) {
|
|
3915
5947
|
const cleaned = scrubSummary(summary).trim();
|
|
3916
|
-
if (cleaned)
|
|
5948
|
+
if (cleaned) p2.log.message(cleaned);
|
|
3917
5949
|
}
|
|
3918
5950
|
function shortPhaseLabel(phase) {
|
|
3919
5951
|
switch (phase) {
|