@whisperr/wizard 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2116 -164
- 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,1580 @@ 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 });
|
|
1973
|
+
}
|
|
1974
|
+
getRun(runId) {
|
|
1975
|
+
return this.request("GET", `/wizard/runs/${runId}`);
|
|
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
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
completeRun(runId, completion) {
|
|
1989
|
+
return this.request(
|
|
1990
|
+
"POST",
|
|
1991
|
+
`/wizard/runs/${runId}/complete`,
|
|
1992
|
+
{ ...completion }
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
// src/engine/selection.ts
|
|
1998
|
+
import { z } from "zod";
|
|
1999
|
+
|
|
2000
|
+
// src/engine/prompts.ts
|
|
2001
|
+
var SURVEY_SYSTEM_PROMPT = [
|
|
2002
|
+
"You are the read-only survey pass of the Whisperr integration wizard.",
|
|
2003
|
+
"Map the repository so a later pass can pick the product's important lifecycle events.",
|
|
2004
|
+
"You cannot edit files. Be fast and decisive; read entry points and feature roots, not every file.",
|
|
2005
|
+
"",
|
|
2006
|
+
"Report, in plain markdown:",
|
|
2007
|
+
"1. Stack: frameworks, package manager, app entry point file.",
|
|
2008
|
+
"2. Feature areas: name, root directory, and the 1-3 files where committed user actions happen.",
|
|
2009
|
+
"3. The end-user identify() anchor: the login/signup success and session-restore call sites for the",
|
|
2010
|
+
" PAYING end user \u2014 never admin, staff, operator, or seller paths.",
|
|
2011
|
+
"4. Existing analytics calls, if any.",
|
|
2012
|
+
"5. Committed-action call sites worth instrumenting: file, function/symbol, and what the user just did.",
|
|
2013
|
+
"Cite concrete file paths for every claim. Do not propose event names yet."
|
|
2014
|
+
].join("\n");
|
|
2015
|
+
function surveyUserPrompt(repoPath, onboardingContext) {
|
|
2016
|
+
return [
|
|
2017
|
+
`Survey the repository at ${repoPath}.`,
|
|
2018
|
+
"",
|
|
2019
|
+
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
2020
|
+
"",
|
|
2021
|
+
"Produce the survey report now."
|
|
2022
|
+
].join("\n");
|
|
2023
|
+
}
|
|
2024
|
+
var SELECTION_SYSTEM_PROMPT = [
|
|
2025
|
+
"You are the event-selection pass of the Whisperr integration wizard.",
|
|
2026
|
+
"From the survey report and business context, choose the IMPORTANT product events this codebase",
|
|
2027
|
+
"can actually emit, and propose each one with the propose_event tool.",
|
|
2028
|
+
"",
|
|
2029
|
+
"Rules \u2014 these are hard constraints:",
|
|
2030
|
+
"- Only propose an event with a concrete call site: a real file and function where the committed",
|
|
2031
|
+
" user action happens. Never propose absence/timeout/derived concepts (daily_log_missed,",
|
|
2032
|
+
" streak_broken) \u2014 the server derives those later.",
|
|
2033
|
+
"- Emit after confirmed outcomes, not intent: a purchase event fires on confirmed success, not on",
|
|
2034
|
+
" button tap; denied permissions are not grants.",
|
|
2035
|
+
"- Importance is anchored in the business context: the activation moment, churn-risk and healthy",
|
|
2036
|
+
" signals, and how the product charges. A handful of load-bearing events beats wide coverage.",
|
|
2037
|
+
"- An empty or small set is a valid outcome. There is no quota. Never invent events to look thorough.",
|
|
2038
|
+
"- Codes are lowercase snake_case, named for what happened (checkout_completed, not do_checkout).",
|
|
2039
|
+
"- payloadSchema lists the fields worth capturing with a short description of each \u2014 the important",
|
|
2040
|
+
" info only. NEVER include personal identifiers: no email, phone, names, addresses, birth dates,",
|
|
2041
|
+
" government ids, usernames, passwords, device ids, or IP addresses. Domain metrics are welcome.",
|
|
2042
|
+
"- The end user is the paying customer/consumer. Never instrument admin, staff, or operator actions.",
|
|
2043
|
+
"",
|
|
2044
|
+
"Call propose_event once per event. When done, call finish_selection with a one-line summary."
|
|
2045
|
+
].join("\n");
|
|
2046
|
+
function selectionUserPrompt(surveyReport, onboardingContext, existingEventCodes, rejectedCodes) {
|
|
2047
|
+
return [
|
|
2048
|
+
"Survey report:",
|
|
2049
|
+
surveyReport,
|
|
2050
|
+
"",
|
|
2051
|
+
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
2052
|
+
existingEventCodes.length > 0 ? "Already registered for this app (do not re-propose; other projects may own them):\n- " + existingEventCodes.join("\n- ") : "",
|
|
2053
|
+
rejectedCodes.length > 0 ? "Previously rejected by the user (do not re-propose):\n- " + rejectedCodes.join("\n- ") : "",
|
|
2054
|
+
"",
|
|
2055
|
+
"Propose the important events now."
|
|
2056
|
+
].filter((section) => section !== "").join("\n");
|
|
2057
|
+
}
|
|
2058
|
+
function renderOnboardingContext(context) {
|
|
2059
|
+
if (!context) {
|
|
2060
|
+
return "";
|
|
2061
|
+
}
|
|
2062
|
+
const rendered = JSON.stringify(context, null, 1);
|
|
2063
|
+
return rendered.length > 6e3 ? rendered.slice(0, 6e3) + "\n\u2026(truncated)" : rendered;
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// src/engine/selection.ts
|
|
2067
|
+
var PII_SEGMENTS = /* @__PURE__ */ new Set([
|
|
2068
|
+
"email",
|
|
2069
|
+
"phone",
|
|
2070
|
+
"address",
|
|
2071
|
+
"surname",
|
|
2072
|
+
"dob",
|
|
2073
|
+
"birthday",
|
|
2074
|
+
"birthdate",
|
|
2075
|
+
"ssn",
|
|
2076
|
+
"passport",
|
|
2077
|
+
"password",
|
|
2078
|
+
"username"
|
|
2079
|
+
]);
|
|
2080
|
+
var PII_PAIRS = [
|
|
2081
|
+
["first", "name"],
|
|
2082
|
+
["last", "name"],
|
|
2083
|
+
["full", "name"],
|
|
2084
|
+
["user", "name"],
|
|
2085
|
+
["middle", "name"],
|
|
2086
|
+
["maiden", "name"],
|
|
2087
|
+
["birth", "date"],
|
|
2088
|
+
["date", "birth"],
|
|
2089
|
+
["national", "id"],
|
|
2090
|
+
["government", "id"],
|
|
2091
|
+
["device", "id"],
|
|
2092
|
+
["personal", "number"],
|
|
2093
|
+
["social", "security"],
|
|
2094
|
+
["tax", "id"],
|
|
2095
|
+
["drivers", "license"]
|
|
2096
|
+
];
|
|
2097
|
+
function isPIIField(name) {
|
|
2098
|
+
const segments = name.split("_");
|
|
2099
|
+
if (segments.some((segment) => PII_SEGMENTS.has(segment))) {
|
|
2100
|
+
return true;
|
|
2101
|
+
}
|
|
2102
|
+
for (let index = 0; index + 1 < segments.length; index += 1) {
|
|
2103
|
+
if (PII_PAIRS.some(([a, b]) => segments[index] === a && segments[index + 1] === b)) {
|
|
2104
|
+
return true;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
return false;
|
|
2108
|
+
}
|
|
2109
|
+
function normalizeEventCode(value) {
|
|
2110
|
+
const collapsed = value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_{2,}/g, "_");
|
|
2111
|
+
return /^[a-z]/.test(collapsed) ? collapsed : "";
|
|
2112
|
+
}
|
|
2113
|
+
function validateProposedEvent(input, seen, rejected) {
|
|
2114
|
+
const code = normalizeEventCode(input.code);
|
|
2115
|
+
if (code === "") {
|
|
2116
|
+
return { ok: false, reason: `event code ${JSON.stringify(input.code)} is not snake_case` };
|
|
2117
|
+
}
|
|
2118
|
+
if (seen.has(code)) {
|
|
2119
|
+
return { ok: false, reason: `event ${code} was already proposed` };
|
|
2120
|
+
}
|
|
2121
|
+
if (rejected.has(code)) {
|
|
2122
|
+
return { ok: false, reason: `event ${code} was rejected by the user \u2014 do not re-propose it` };
|
|
2123
|
+
}
|
|
2124
|
+
if (!input.name.trim() || !input.reasoning.trim() || !input.anchorFile.trim()) {
|
|
2125
|
+
return { ok: false, reason: "name, reasoning, and anchorFile are required" };
|
|
2126
|
+
}
|
|
2127
|
+
const schema = {};
|
|
2128
|
+
for (const [field, description] of Object.entries(input.payloadSchema ?? {})) {
|
|
2129
|
+
const normalizedField = normalizeEventCode(field);
|
|
2130
|
+
if (normalizedField === "" || !description.trim()) {
|
|
2131
|
+
return { ok: false, reason: `payload field ${JSON.stringify(field)} needs a snake_case name and a description` };
|
|
2132
|
+
}
|
|
2133
|
+
if (isPIIField(normalizedField)) {
|
|
2134
|
+
return { ok: false, reason: `payload field ${normalizedField} is a personal identifier and is not allowed` };
|
|
2135
|
+
}
|
|
2136
|
+
schema[normalizedField] = description.trim();
|
|
2137
|
+
}
|
|
2138
|
+
if (Object.keys(schema).length > 20) {
|
|
2139
|
+
return { ok: false, reason: "payload schema exceeds 20 fields" };
|
|
2140
|
+
}
|
|
2141
|
+
return {
|
|
2142
|
+
ok: true,
|
|
2143
|
+
event: {
|
|
2144
|
+
code,
|
|
2145
|
+
name: input.name.trim(),
|
|
2146
|
+
reasoning: input.reasoning.trim(),
|
|
2147
|
+
anchorFile: input.anchorFile.trim(),
|
|
2148
|
+
anchorSymbol: input.anchorSymbol?.trim() || void 0,
|
|
2149
|
+
payloadSchema: schema
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSessionId) {
|
|
2154
|
+
sink.emit({ phase: "surveying", action: "mapping the repository" });
|
|
2155
|
+
const outcome = await provider.invoke(
|
|
2156
|
+
{
|
|
2157
|
+
role: "survey",
|
|
2158
|
+
systemPrompt: SURVEY_SYSTEM_PROMPT,
|
|
2159
|
+
userPrompt: surveyUserPrompt(repoPath, renderOnboardingContext(bootstrap.onboardingContext)),
|
|
2160
|
+
tools: [],
|
|
2161
|
+
allowRepoWrite: false,
|
|
2162
|
+
cwd: repoPath,
|
|
2163
|
+
resumeSessionId
|
|
2164
|
+
},
|
|
2165
|
+
models.survey,
|
|
2166
|
+
(action) => sink.emit({ phase: "surveying", action })
|
|
2167
|
+
);
|
|
2168
|
+
return { report: outcome.kind === "completed" ? outcome.text : "", outcome };
|
|
2169
|
+
}
|
|
2170
|
+
async function runSelection(provider, models, repoPath, bootstrap, surveyReport, rejectedCodes, sink) {
|
|
2171
|
+
const proposed = [];
|
|
2172
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2173
|
+
const rejected = new Set(rejectedCodes);
|
|
2174
|
+
let summary = "";
|
|
2175
|
+
const proposeTool = {
|
|
2176
|
+
name: "propose_event",
|
|
2177
|
+
description: "Propose one important, concretely-anchored product event. Returns ok:true when accepted.",
|
|
2178
|
+
inputSchema: {
|
|
2179
|
+
code: z.string().describe("lowercase snake_case event code"),
|
|
2180
|
+
name: z.string().describe("short human-readable name"),
|
|
2181
|
+
reasoning: z.string().describe("why this event matters for this business"),
|
|
2182
|
+
anchorFile: z.string().describe("repository file where the committed action happens"),
|
|
2183
|
+
anchorSymbol: z.string().optional().describe("function/symbol at the anchor"),
|
|
2184
|
+
payloadSchema: z.record(z.string(), z.string()).optional().describe("important payload fields: name -> short description; no personal identifiers")
|
|
2185
|
+
},
|
|
2186
|
+
jsonSchema: {
|
|
2187
|
+
type: "object",
|
|
2188
|
+
properties: {
|
|
2189
|
+
code: { type: "string", description: "lowercase snake_case event code" },
|
|
2190
|
+
name: { type: "string", description: "short human-readable name" },
|
|
2191
|
+
reasoning: { type: "string", description: "why this event matters for this business" },
|
|
2192
|
+
anchorFile: { type: "string", description: "repository file where the committed action happens" },
|
|
2193
|
+
anchorSymbol: { type: "string", description: "function/symbol at the anchor" },
|
|
2194
|
+
payloadSchema: {
|
|
2195
|
+
type: "object",
|
|
2196
|
+
additionalProperties: { type: "string" },
|
|
2197
|
+
description: "important payload fields: name -> short description; no personal identifiers"
|
|
2198
|
+
}
|
|
2199
|
+
},
|
|
2200
|
+
required: ["code", "name", "reasoning", "anchorFile"]
|
|
2201
|
+
},
|
|
2202
|
+
handler: async (input) => {
|
|
2203
|
+
const candidate = {
|
|
2204
|
+
code: String(input.code ?? ""),
|
|
2205
|
+
name: String(input.name ?? ""),
|
|
2206
|
+
reasoning: String(input.reasoning ?? ""),
|
|
2207
|
+
anchorFile: String(input.anchorFile ?? ""),
|
|
2208
|
+
anchorSymbol: input.anchorSymbol === void 0 ? void 0 : String(input.anchorSymbol),
|
|
2209
|
+
payloadSchema: input.payloadSchema ?? {}
|
|
2210
|
+
};
|
|
2211
|
+
const validated = validateProposedEvent(candidate, seen, rejected);
|
|
2212
|
+
if (!validated.ok) {
|
|
2213
|
+
return { ok: false, reason: validated.reason };
|
|
2214
|
+
}
|
|
2215
|
+
seen.add(validated.event.code);
|
|
2216
|
+
proposed.push(validated.event);
|
|
2217
|
+
sink.emit({
|
|
2218
|
+
phase: "designing",
|
|
2219
|
+
file: validated.event.anchorFile,
|
|
2220
|
+
action: `proposed ${validated.event.code}`
|
|
2221
|
+
});
|
|
2222
|
+
return { ok: true, code: validated.event.code };
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
const finishTool = {
|
|
2226
|
+
name: "finish_selection",
|
|
2227
|
+
description: "Signal that every important event has been proposed.",
|
|
2228
|
+
inputSchema: { summary: z.string().describe("one line on what was selected and why") },
|
|
2229
|
+
jsonSchema: {
|
|
2230
|
+
type: "object",
|
|
2231
|
+
properties: { summary: { type: "string", description: "one line on what was selected and why" } },
|
|
2232
|
+
required: ["summary"]
|
|
2233
|
+
},
|
|
2234
|
+
handler: async (input) => {
|
|
2235
|
+
summary = String(input.summary ?? "");
|
|
2236
|
+
return { ok: true };
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
sink.emit({ phase: "designing", action: "selecting important events" });
|
|
2240
|
+
const outcome = await provider.invoke(
|
|
2241
|
+
{
|
|
2242
|
+
role: "design",
|
|
2243
|
+
systemPrompt: SELECTION_SYSTEM_PROMPT,
|
|
2244
|
+
userPrompt: selectionUserPrompt(
|
|
2245
|
+
surveyReport,
|
|
2246
|
+
renderOnboardingContext(bootstrap.onboardingContext),
|
|
2247
|
+
bootstrap.snapshot.events.map((event) => event.code),
|
|
2248
|
+
rejectedCodes
|
|
2249
|
+
),
|
|
2250
|
+
tools: [proposeTool, finishTool],
|
|
2251
|
+
allowRepoWrite: false,
|
|
2252
|
+
cwd: repoPath
|
|
2253
|
+
},
|
|
2254
|
+
models.design,
|
|
2255
|
+
(action) => sink.emit({ phase: "designing", action })
|
|
2256
|
+
);
|
|
2257
|
+
return {
|
|
2258
|
+
outcome: outcome.kind,
|
|
2259
|
+
proposed,
|
|
2260
|
+
summary,
|
|
2261
|
+
surveyReport,
|
|
2262
|
+
sessionId: outcome.sessionId
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
async function registerApprovedEvents(runs, runId, approved, sink) {
|
|
2266
|
+
const registered = [];
|
|
2267
|
+
for (const event of approved) {
|
|
2268
|
+
const result = await runs.writeItem(runId, "event", {
|
|
2269
|
+
code: event.code,
|
|
2270
|
+
name: event.name,
|
|
2271
|
+
reasoning: event.reasoning,
|
|
2272
|
+
...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
|
|
2273
|
+
});
|
|
2274
|
+
const item = result.item;
|
|
2275
|
+
registered.push({ ...event, eventId: item?.id ?? result.id ?? "" });
|
|
2276
|
+
sink.emit({ phase: "persisting", file: event.anchorFile, action: `registered ${event.code}` });
|
|
2277
|
+
}
|
|
2278
|
+
return registered;
|
|
2279
|
+
}
|
|
2280
|
+
function placementsFor(registered) {
|
|
2281
|
+
return registered.map((event) => ({
|
|
2282
|
+
eventId: event.eventId,
|
|
2283
|
+
eventCode: event.code,
|
|
2284
|
+
file: event.anchorFile,
|
|
2285
|
+
symbol: event.anchorSymbol,
|
|
2286
|
+
status: "planned"
|
|
2287
|
+
}));
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
// src/engine/state.ts
|
|
2291
|
+
import { createHash as createHash3 } from "crypto";
|
|
2292
|
+
import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
|
|
2293
|
+
import { homedir } from "os";
|
|
2294
|
+
import { join as join4 } from "path";
|
|
2295
|
+
|
|
2296
|
+
// src/engine/types.ts
|
|
2297
|
+
var ENGINE_PHASES = [
|
|
2298
|
+
"authorizing",
|
|
2299
|
+
"surveying",
|
|
2300
|
+
"designing",
|
|
2301
|
+
"persisting",
|
|
2302
|
+
"binding",
|
|
2303
|
+
"integrating",
|
|
2304
|
+
"reviewing",
|
|
2305
|
+
"verifying",
|
|
2306
|
+
"reporting",
|
|
2307
|
+
"completed"
|
|
2308
|
+
];
|
|
2309
|
+
|
|
2310
|
+
// src/engine/state.ts
|
|
2311
|
+
function stateDirectory() {
|
|
2312
|
+
const override = process.env.WHISPERR_WIZARD_STATE_DIR?.trim();
|
|
2313
|
+
if (override) {
|
|
2314
|
+
return override;
|
|
2315
|
+
}
|
|
2316
|
+
return join4(homedir(), ".whisperr", "wizard-runs");
|
|
2317
|
+
}
|
|
2318
|
+
function stateKey(apiBaseUrl, appId, repoFingerprint2) {
|
|
2319
|
+
return createHash3("sha256").update(`${apiBaseUrl}
|
|
2320
|
+
${appId}
|
|
2321
|
+
${repoFingerprint2}`).digest("hex").slice(0, 24);
|
|
2322
|
+
}
|
|
2323
|
+
function statePath(apiBaseUrl, appId, repoFingerprint2) {
|
|
2324
|
+
return join4(stateDirectory(), `${stateKey(apiBaseUrl, appId, repoFingerprint2)}.json`);
|
|
2325
|
+
}
|
|
2326
|
+
function saveRunState(state) {
|
|
2327
|
+
const path = statePath(state.apiBaseUrl, state.appId, state.repoFingerprint);
|
|
2328
|
+
mkdirSync(stateDirectory(), { recursive: true, mode: 448 });
|
|
2329
|
+
const payload = JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2);
|
|
2330
|
+
const temporary = `${path}.tmp`;
|
|
2331
|
+
writeFileSync(temporary, payload, { mode: 384 });
|
|
2332
|
+
renameSync(temporary, path);
|
|
2333
|
+
}
|
|
2334
|
+
function loadRunState(apiBaseUrl, appId, repoFingerprint2) {
|
|
2335
|
+
const path = statePath(apiBaseUrl, appId, repoFingerprint2);
|
|
2336
|
+
let raw;
|
|
2337
|
+
try {
|
|
2338
|
+
raw = readFileSync2(path, "utf8");
|
|
2339
|
+
} catch {
|
|
2340
|
+
return void 0;
|
|
2341
|
+
}
|
|
2342
|
+
let parsed;
|
|
2343
|
+
try {
|
|
2344
|
+
parsed = JSON.parse(raw);
|
|
2345
|
+
} catch {
|
|
2346
|
+
return void 0;
|
|
2347
|
+
}
|
|
2348
|
+
if (!isEngineRunState(parsed)) {
|
|
2349
|
+
return void 0;
|
|
2350
|
+
}
|
|
2351
|
+
if (parsed.apiBaseUrl !== apiBaseUrl || parsed.appId !== appId || parsed.repoFingerprint !== repoFingerprint2) {
|
|
2352
|
+
return void 0;
|
|
2353
|
+
}
|
|
2354
|
+
return parsed;
|
|
2355
|
+
}
|
|
2356
|
+
function isEngineRunState(value) {
|
|
2357
|
+
if (typeof value !== "object" || value === null) {
|
|
2358
|
+
return false;
|
|
2359
|
+
}
|
|
2360
|
+
const candidate = value;
|
|
2361
|
+
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;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
// src/engine/worktree.ts
|
|
2365
|
+
import { spawn as spawn2 } from "child_process";
|
|
2366
|
+
import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
|
|
2367
|
+
import { tmpdir } from "os";
|
|
2368
|
+
import { join as join5 } from "path";
|
|
2369
|
+
var WorktreeError = class extends Error {
|
|
2370
|
+
constructor(code, message) {
|
|
2371
|
+
super(message);
|
|
2372
|
+
this.code = code;
|
|
2373
|
+
this.name = "WorktreeError";
|
|
2374
|
+
}
|
|
2375
|
+
code;
|
|
2376
|
+
};
|
|
2377
|
+
function git(cwd, args) {
|
|
2378
|
+
return new Promise((resolve4) => {
|
|
2379
|
+
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2380
|
+
let stdout = "";
|
|
2381
|
+
let stderr = "";
|
|
2382
|
+
child.stdout.on("data", (chunk) => {
|
|
2383
|
+
stdout += String(chunk);
|
|
2384
|
+
});
|
|
2385
|
+
child.stderr.on("data", (chunk) => {
|
|
2386
|
+
stderr += String(chunk);
|
|
2387
|
+
});
|
|
2388
|
+
child.on("error", () => resolve4({ ok: false, stdout, stderr: "git is not available" }));
|
|
2389
|
+
child.on("close", (exitCode) => resolve4({ ok: exitCode === 0, stdout, stderr }));
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
async function must(cwd, args) {
|
|
2393
|
+
const result = await git(cwd, args);
|
|
2394
|
+
if (!result.ok) {
|
|
2395
|
+
throw new WorktreeError("git_failed", `git ${args.join(" ")}: ${result.stderr.trim()}`);
|
|
2396
|
+
}
|
|
2397
|
+
return result.stdout;
|
|
2398
|
+
}
|
|
2399
|
+
async function createWorktree(repoPath, force = false) {
|
|
2400
|
+
const inside = await git(repoPath, ["rev-parse", "--is-inside-work-tree"]);
|
|
2401
|
+
if (!inside.ok) {
|
|
2402
|
+
throw new WorktreeError("not_a_repo", "the target directory is not a git repository");
|
|
2403
|
+
}
|
|
2404
|
+
const status = await must(repoPath, ["status", "--porcelain"]);
|
|
2405
|
+
if (status.trim() !== "" && !force) {
|
|
2406
|
+
throw new WorktreeError(
|
|
2407
|
+
"dirty_repo",
|
|
2408
|
+
"the repository has uncommitted changes \u2014 commit or stash them, or rerun with --force"
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
const baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
|
|
2412
|
+
const path = await mkdtemp(join5(tmpdir(), "whisperr-worktree-"));
|
|
2413
|
+
await must(repoPath, ["worktree", "add", "--detach", "--force", path, "HEAD"]);
|
|
2414
|
+
return { path, baseRef, repoPath };
|
|
2415
|
+
}
|
|
2416
|
+
async function worktreePatch(handle) {
|
|
2417
|
+
await must(handle.path, ["add", "-A"]);
|
|
2418
|
+
return must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
|
|
2419
|
+
}
|
|
2420
|
+
async function applyPatchToRepo(handle, patch) {
|
|
2421
|
+
if (patch.trim() === "") {
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
await new Promise((resolve4, reject) => {
|
|
2425
|
+
const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
|
|
2426
|
+
cwd: handle.repoPath,
|
|
2427
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
2428
|
+
});
|
|
2429
|
+
let stderr = "";
|
|
2430
|
+
child.stderr.on("data", (chunk) => {
|
|
2431
|
+
stderr += String(chunk);
|
|
2432
|
+
});
|
|
2433
|
+
child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
|
|
2434
|
+
child.on("close", (exitCode) => {
|
|
2435
|
+
if (exitCode === 0) {
|
|
2436
|
+
resolve4();
|
|
2437
|
+
} else {
|
|
2438
|
+
reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
|
|
2439
|
+
}
|
|
2440
|
+
});
|
|
2441
|
+
child.stdin.end(patch);
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
async function savePartialPatch(handle, destination) {
|
|
2445
|
+
const patch = await worktreePatch(handle);
|
|
2446
|
+
if (patch.trim() === "") {
|
|
2447
|
+
return void 0;
|
|
2448
|
+
}
|
|
2449
|
+
await writeFile2(destination, patch, { mode: 384 });
|
|
2450
|
+
return destination;
|
|
2451
|
+
}
|
|
2452
|
+
async function removeWorktree(handle) {
|
|
2453
|
+
await git(handle.repoPath, ["worktree", "remove", "--force", handle.path]);
|
|
2454
|
+
await git(handle.repoPath, ["worktree", "prune"]);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/engine/orchestrator.ts
|
|
2458
|
+
async function runEngine(input) {
|
|
2459
|
+
const runs = new RunsClient(input.config, input.session);
|
|
2460
|
+
let bootstrap;
|
|
2461
|
+
try {
|
|
2462
|
+
bootstrap = await runs.createOrResumeRun({
|
|
2463
|
+
repoFingerprint: input.repoFingerprint,
|
|
2464
|
+
displayName: basename(input.repoPath),
|
|
2465
|
+
target: input.target,
|
|
2466
|
+
kind: input.projectKind
|
|
2467
|
+
});
|
|
2468
|
+
} catch (error) {
|
|
2469
|
+
if (error instanceof RunsApiError && error.code === "wrong_universe_mode") {
|
|
2470
|
+
return { kind: "wrong_mode" };
|
|
2471
|
+
}
|
|
2472
|
+
throw error;
|
|
2473
|
+
}
|
|
2474
|
+
const runId = bootstrap.run.id;
|
|
2475
|
+
input.provider.onRunReady?.(runId);
|
|
2476
|
+
const patchPhase = async (phase, message) => {
|
|
2477
|
+
try {
|
|
2478
|
+
await runs.patchRun(runId, { phase, ...message ? { message } : {} });
|
|
2479
|
+
} catch {
|
|
2480
|
+
}
|
|
2481
|
+
};
|
|
2482
|
+
const failRun = async (error) => {
|
|
2483
|
+
try {
|
|
2484
|
+
await runs.patchRun(runId, { status: "failed", error: error.slice(0, 500) });
|
|
2485
|
+
} catch {
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
let state = loadRunState(input.config.apiBaseUrl, input.session.appId, input.repoFingerprint) ?? freshState(input, runId);
|
|
2489
|
+
if (state.runId !== runId) {
|
|
2490
|
+
state = freshState(input, runId);
|
|
2491
|
+
}
|
|
2492
|
+
const persist = () => saveRunState(state);
|
|
2493
|
+
let registered;
|
|
2494
|
+
if (!state.designComplete) {
|
|
2495
|
+
await patchPhase("surveying");
|
|
2496
|
+
const survey = await runSurvey(
|
|
2497
|
+
input.provider,
|
|
2498
|
+
input.models,
|
|
2499
|
+
input.repoPath,
|
|
2500
|
+
bootstrap,
|
|
2501
|
+
input.sink,
|
|
2502
|
+
state.providerSessions.survey
|
|
2503
|
+
);
|
|
2504
|
+
if (survey.outcome.kind !== "completed") {
|
|
2505
|
+
await failRun(`survey ${survey.outcome.kind}`);
|
|
2506
|
+
return { kind: "aborted", reason: `survey ${survey.outcome.kind}` };
|
|
2507
|
+
}
|
|
2508
|
+
state.providerSessions.survey = survey.outcome.sessionId;
|
|
2509
|
+
persist();
|
|
2510
|
+
await patchPhase("designing");
|
|
2511
|
+
const selection = await runSelection(
|
|
2512
|
+
input.provider,
|
|
2513
|
+
input.models,
|
|
2514
|
+
input.repoPath,
|
|
2515
|
+
bootstrap,
|
|
2516
|
+
survey.report,
|
|
2517
|
+
state.tombstonedCodes,
|
|
2518
|
+
input.sink
|
|
2519
|
+
);
|
|
2520
|
+
if (selection.outcome !== "completed" && selection.proposed.length === 0) {
|
|
2521
|
+
await failRun(`selection ${selection.outcome}`);
|
|
2522
|
+
return { kind: "aborted", reason: `selection ${selection.outcome}` };
|
|
2523
|
+
}
|
|
2524
|
+
const approved = await input.callbacks.reviewEvents(selection.proposed);
|
|
2525
|
+
if (approved === null) {
|
|
2526
|
+
await failRun("selection rejected by user");
|
|
2527
|
+
return { kind: "aborted", reason: "selection rejected by user" };
|
|
2528
|
+
}
|
|
2529
|
+
const approvedCodes = new Set(approved.map((event) => event.code));
|
|
2530
|
+
for (const event of selection.proposed) {
|
|
2531
|
+
if (!approvedCodes.has(event.code) && !state.tombstonedCodes.includes(event.code)) {
|
|
2532
|
+
state.tombstonedCodes.push(event.code);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
persist();
|
|
2536
|
+
await patchPhase("persisting", `registering ${approved.length} events`);
|
|
2537
|
+
try {
|
|
2538
|
+
registered = await registerApprovedEvents(runs, runId, approved, input.sink);
|
|
2539
|
+
} catch (error) {
|
|
2540
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2541
|
+
await failRun(`event registration failed: ${reason}`);
|
|
2542
|
+
persist();
|
|
2543
|
+
return { kind: "aborted", reason: `event registration failed: ${reason}` };
|
|
2544
|
+
}
|
|
2545
|
+
state.designComplete = true;
|
|
2546
|
+
state.clusters = deriveClusters(placementsFor(registered));
|
|
2547
|
+
persist();
|
|
2548
|
+
} else {
|
|
2549
|
+
registered = rebuildRegistered(state, bootstrap);
|
|
2550
|
+
input.sink.emit({ phase: "designing", action: `resumed with ${registered.length} registered events` });
|
|
2551
|
+
}
|
|
2552
|
+
if (registered.length === 0) {
|
|
2553
|
+
await patchPhase("reporting", "no events registered for this project");
|
|
2554
|
+
await completeRun(runs, runId, [], /* @__PURE__ */ new Map(), [], true, "unavailable", void 0);
|
|
2555
|
+
return {
|
|
2556
|
+
kind: "completed",
|
|
2557
|
+
runId,
|
|
2558
|
+
registered,
|
|
2559
|
+
eventStatuses: /* @__PURE__ */ new Map(),
|
|
2560
|
+
reportEvents: [],
|
|
2561
|
+
changedFiles: [],
|
|
2562
|
+
identifyWired: false,
|
|
2563
|
+
applied: false,
|
|
2564
|
+
summary: "No important events found for this project; nothing was instrumented."
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
await patchPhase("binding");
|
|
2568
|
+
let worktree;
|
|
2569
|
+
try {
|
|
2570
|
+
worktree = await createWorktree(input.repoPath, input.force ?? false);
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2573
|
+
await failRun(reason);
|
|
2574
|
+
return { kind: "aborted", reason };
|
|
2575
|
+
}
|
|
2576
|
+
state.worktreePath = worktree.path;
|
|
2577
|
+
persist();
|
|
2578
|
+
try {
|
|
2579
|
+
const bindings = await writeBindingsModule(worktree, input.target, registered);
|
|
2580
|
+
const deps = {
|
|
2581
|
+
provider: input.provider,
|
|
2582
|
+
models: input.models,
|
|
2583
|
+
worktree,
|
|
2584
|
+
target: input.target,
|
|
2585
|
+
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
2586
|
+
registered,
|
|
2587
|
+
ingestion: bootstrap.ingestion,
|
|
2588
|
+
sink: input.sink,
|
|
2589
|
+
saveClusters: (clusters) => {
|
|
2590
|
+
state.clusters = clusters;
|
|
2591
|
+
persist();
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
const identifyWired = await runSdkSetupPass(deps, bindings);
|
|
2595
|
+
await patchPhase("integrating");
|
|
2596
|
+
const integration = await runClusterIntegration(
|
|
2597
|
+
deps,
|
|
2598
|
+
state.clusters,
|
|
2599
|
+
async () => worktreePatch(worktree)
|
|
2600
|
+
);
|
|
2601
|
+
state.clusters = integration.clusters;
|
|
2602
|
+
persist();
|
|
2603
|
+
const finalVerify = await finalizeIntegration(deps);
|
|
2604
|
+
const patch = await worktreePatch(worktree);
|
|
2605
|
+
const changedFiles2 = patchFiles(patch);
|
|
2606
|
+
await patchPhase("reporting");
|
|
2607
|
+
const evidence = buildEvidence(registered, integration.eventStatuses, changedFiles2);
|
|
2608
|
+
await completeRun(
|
|
2609
|
+
runs,
|
|
2610
|
+
runId,
|
|
2611
|
+
evidence,
|
|
2612
|
+
integration.eventStatuses,
|
|
2613
|
+
changedFiles2,
|
|
2614
|
+
identifyWired,
|
|
2615
|
+
finalVerify.status,
|
|
2616
|
+
finalVerify.command
|
|
2617
|
+
);
|
|
2618
|
+
const summaryLines = summarize(registered, integration.eventStatuses);
|
|
2619
|
+
const applied = patch.trim() !== "" && await input.callbacks.confirmApply(patch, summaryLines);
|
|
2620
|
+
if (applied) {
|
|
2621
|
+
await applyPatchToRepo(worktree, patch);
|
|
2622
|
+
}
|
|
2623
|
+
const partialPatchPath = applied ? void 0 : await savePartialPatch(worktree, join6(input.repoPath, ".whisperr-wizard.patch"));
|
|
2624
|
+
await removeWorktree(worktree);
|
|
2625
|
+
state.worktreePath = void 0;
|
|
2626
|
+
persist();
|
|
2627
|
+
const hasFailures = [...integration.eventStatuses.values()].some(
|
|
2628
|
+
(status) => status === "failed"
|
|
2629
|
+
);
|
|
2630
|
+
return {
|
|
2631
|
+
kind: hasFailures ? "partial" : "completed",
|
|
2632
|
+
runId,
|
|
2633
|
+
registered,
|
|
2634
|
+
eventStatuses: integration.eventStatuses,
|
|
2635
|
+
reportEvents: buildReportEvents(registered, integration.eventStatuses),
|
|
2636
|
+
changedFiles: changedFiles2,
|
|
2637
|
+
identifyWired,
|
|
2638
|
+
applied,
|
|
2639
|
+
partialPatchPath: partialPatchPath ?? void 0,
|
|
2640
|
+
summary: summaryLines.join("\n")
|
|
2641
|
+
};
|
|
2642
|
+
} catch (error) {
|
|
2643
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2644
|
+
await failRun(reason);
|
|
2645
|
+
const partialPatchPath = await savePartialPatch(
|
|
2646
|
+
worktree,
|
|
2647
|
+
join6(input.repoPath, ".whisperr-wizard.patch")
|
|
2648
|
+
).catch(() => void 0);
|
|
2649
|
+
persist();
|
|
2650
|
+
return {
|
|
2651
|
+
kind: "partial",
|
|
2652
|
+
runId,
|
|
2653
|
+
registered,
|
|
2654
|
+
eventStatuses: /* @__PURE__ */ new Map(),
|
|
2655
|
+
reportEvents: buildReportEvents(registered, /* @__PURE__ */ new Map()),
|
|
2656
|
+
changedFiles: [],
|
|
2657
|
+
identifyWired: false,
|
|
2658
|
+
applied: false,
|
|
2659
|
+
partialPatchPath: partialPatchPath ?? void 0,
|
|
2660
|
+
summary: `Integration stopped early: ${reason}. Registered events are saved; rerun to resume.`
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
function freshState(input, runId) {
|
|
2665
|
+
return {
|
|
2666
|
+
version: 1,
|
|
2667
|
+
apiBaseUrl: input.config.apiBaseUrl,
|
|
2668
|
+
appId: input.session.appId,
|
|
2669
|
+
repoFingerprint: input.repoFingerprint,
|
|
2670
|
+
runId,
|
|
2671
|
+
phase: "authorizing",
|
|
2672
|
+
providerSessions: {},
|
|
2673
|
+
designComplete: false,
|
|
2674
|
+
tombstonedCodes: [],
|
|
2675
|
+
clusters: [],
|
|
2676
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1186
2677
|
};
|
|
1187
2678
|
}
|
|
2679
|
+
function rebuildRegistered(state, bootstrap) {
|
|
2680
|
+
const byCode = new Map(bootstrap.snapshot.events.map((event) => [event.code, event]));
|
|
2681
|
+
const registered = [];
|
|
2682
|
+
for (const cluster of state.clusters) {
|
|
2683
|
+
for (const placement of cluster.events) {
|
|
2684
|
+
const event = byCode.get(placement.eventCode);
|
|
2685
|
+
if (!event) {
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
registered.push({
|
|
2689
|
+
code: event.code,
|
|
2690
|
+
name: event.name,
|
|
2691
|
+
reasoning: event.reasoning,
|
|
2692
|
+
eventId: event.id,
|
|
2693
|
+
anchorFile: placement.file,
|
|
2694
|
+
anchorSymbol: placement.symbol,
|
|
2695
|
+
payloadSchema: event.payloadSchema ?? {}
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return registered;
|
|
2700
|
+
}
|
|
2701
|
+
function buildEvidence(registered, statuses, changedFiles2) {
|
|
2702
|
+
const changed = new Set(changedFiles2);
|
|
2703
|
+
return registered.map((event) => {
|
|
2704
|
+
const status = statuses.get(event.code) ?? "planned";
|
|
2705
|
+
const needsFile = status !== "planned" && status !== "unsupported" && status !== "failed";
|
|
2706
|
+
const file = changed.has(event.anchorFile) ? event.anchorFile : changedFiles2[0];
|
|
2707
|
+
return {
|
|
2708
|
+
eventId: event.eventId,
|
|
2709
|
+
eventCode: event.code,
|
|
2710
|
+
...needsFile && file ? { file } : {},
|
|
2711
|
+
status
|
|
2712
|
+
};
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
async function completeRun(runs, runId, evidence, _statuses, changedFiles2, identifyWired, verificationStatus, verificationCommand2) {
|
|
2716
|
+
try {
|
|
2717
|
+
await runs.completeRun(runId, {
|
|
2718
|
+
changedFiles: changedFiles2,
|
|
2719
|
+
identifyWired,
|
|
2720
|
+
verificationStatus,
|
|
2721
|
+
...verificationCommand2 ? { verificationCommand: verificationCommand2 } : {},
|
|
2722
|
+
events: evidence
|
|
2723
|
+
});
|
|
2724
|
+
} catch {
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
function buildReportEvents(registered, statuses) {
|
|
2728
|
+
return registered.map((event) => {
|
|
2729
|
+
const status = statuses.get(event.code);
|
|
2730
|
+
const wired = status !== void 0 && status !== "failed" && status !== "unsupported" && status !== "planned";
|
|
2731
|
+
return {
|
|
2732
|
+
event_type: event.code,
|
|
2733
|
+
status: wired ? "wired" : "skipped",
|
|
2734
|
+
...wired ? { file: event.anchorFile } : {},
|
|
2735
|
+
...wired ? {} : { reason: status ?? "not integrated" }
|
|
2736
|
+
};
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
function summarize(registered, statuses) {
|
|
2740
|
+
return registered.map((event) => {
|
|
2741
|
+
const status = statuses.get(event.code) ?? "planned";
|
|
2742
|
+
return `${event.code}: ${status} (${event.anchorFile})`;
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
function patchFiles(patch) {
|
|
2746
|
+
const files = /* @__PURE__ */ new Set();
|
|
2747
|
+
for (const line of patch.split("\n")) {
|
|
2748
|
+
const match = /^diff --git a\/(.+?) b\//.exec(line);
|
|
2749
|
+
if (match?.[1]) {
|
|
2750
|
+
files.add(match[1]);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
return [...files];
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// src/engine/providers/claude.ts
|
|
2757
|
+
import { createSdkMcpServer, query as query2, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
1188
2758
|
|
|
1189
2759
|
// src/core/agent.ts
|
|
1190
2760
|
import {
|
|
@@ -1192,10 +2762,10 @@ import {
|
|
|
1192
2762
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
1193
2763
|
|
|
1194
2764
|
// src/core/git.ts
|
|
1195
|
-
import { spawn } from "child_process";
|
|
1196
|
-
import { createHash } from "crypto";
|
|
2765
|
+
import { spawn as spawn3 } from "child_process";
|
|
2766
|
+
import { createHash as createHash4 } from "crypto";
|
|
1197
2767
|
import { readFile as readFile2 } from "fs/promises";
|
|
1198
|
-
import { basename, join as
|
|
2768
|
+
import { basename as basename2, join as join7 } from "path";
|
|
1199
2769
|
async function takeCheckpoint(repoPath) {
|
|
1200
2770
|
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
1201
2771
|
if (!isRepo) return { isRepo: false };
|
|
@@ -1243,8 +2813,8 @@ async function revertToCheckpoint(repoPath, checkpoint) {
|
|
|
1243
2813
|
}
|
|
1244
2814
|
async function repoFingerprint(repoPath) {
|
|
1245
2815
|
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
1246
|
-
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${
|
|
1247
|
-
return
|
|
2816
|
+
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename2(repoPath)}`;
|
|
2817
|
+
return createHash4("sha256").update(seed).digest("hex").slice(0, 16);
|
|
1248
2818
|
}
|
|
1249
2819
|
function normalizeRemote(url) {
|
|
1250
2820
|
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
@@ -1261,7 +2831,7 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
|
1261
2831
|
const contents = [];
|
|
1262
2832
|
for (const file of files) {
|
|
1263
2833
|
try {
|
|
1264
|
-
contents.push({ file, content: await readFile2(
|
|
2834
|
+
contents.push({ file, content: await readFile2(join7(repoPath, file), "utf8") });
|
|
1265
2835
|
} catch {
|
|
1266
2836
|
continue;
|
|
1267
2837
|
}
|
|
@@ -1291,14 +2861,14 @@ function escapeRegExp(s) {
|
|
|
1291
2861
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1292
2862
|
}
|
|
1293
2863
|
function run(cwd, args) {
|
|
1294
|
-
return new Promise((
|
|
1295
|
-
const child =
|
|
2864
|
+
return new Promise((resolve4) => {
|
|
2865
|
+
const child = spawn3("git", args, { cwd });
|
|
1296
2866
|
let stdout = "";
|
|
1297
2867
|
let stderr = "";
|
|
1298
2868
|
child.stdout.on("data", (d) => stdout += d.toString());
|
|
1299
2869
|
child.stderr.on("data", (d) => stderr += d.toString());
|
|
1300
|
-
child.on("close", (code) =>
|
|
1301
|
-
child.on("error", () =>
|
|
2870
|
+
child.on("close", (code) => resolve4({ ok: code === 0, stdout, stderr }));
|
|
2871
|
+
child.on("error", () => resolve4({ ok: false, stdout, stderr }));
|
|
1302
2872
|
});
|
|
1303
2873
|
}
|
|
1304
2874
|
|
|
@@ -1469,10 +3039,10 @@ function renderEventsBrief(m) {
|
|
|
1469
3039
|
if (e.description) lines.push(` ${e.description}`);
|
|
1470
3040
|
if (e.properties?.length) {
|
|
1471
3041
|
lines.push(" properties to capture if available:");
|
|
1472
|
-
for (const
|
|
1473
|
-
const req =
|
|
1474
|
-
const desc =
|
|
1475
|
-
lines.push(` \xB7 ${
|
|
3042
|
+
for (const p3 of e.properties) {
|
|
3043
|
+
const req = p3.required ? " (required)" : "";
|
|
3044
|
+
const desc = p3.description ? ` \u2014 ${p3.description}` : "";
|
|
3045
|
+
lines.push(` \xB7 ${p3.name}${req}${desc}`);
|
|
1476
3046
|
}
|
|
1477
3047
|
}
|
|
1478
3048
|
}
|
|
@@ -2691,6 +4261,446 @@ function short(s, max = 60) {
|
|
|
2691
4261
|
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
2692
4262
|
}
|
|
2693
4263
|
|
|
4264
|
+
// src/engine/providers/claude.ts
|
|
4265
|
+
var READ_ONLY_TOOLS2 = ["Read", "Glob", "Grep"];
|
|
4266
|
+
var EDIT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
4267
|
+
function createClaudeProvider(config, session) {
|
|
4268
|
+
return {
|
|
4269
|
+
name: "claude",
|
|
4270
|
+
async invoke(invocation, roleConfig, onProgress) {
|
|
4271
|
+
applyModelAuthEnv(config, session);
|
|
4272
|
+
const hostTools = invocation.tools.map(
|
|
4273
|
+
(engineTool) => tool(engineTool.name, engineTool.description, engineTool.inputSchema, async (args) => {
|
|
4274
|
+
const result = await engineTool.handler(args);
|
|
4275
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4276
|
+
})
|
|
4277
|
+
);
|
|
4278
|
+
const allowedTools = [
|
|
4279
|
+
...invocation.allowRepoWrite ? EDIT_TOOLS : READ_ONLY_TOOLS2,
|
|
4280
|
+
...invocation.tools.map((engineTool) => `mcp__engine__${engineTool.name}`)
|
|
4281
|
+
];
|
|
4282
|
+
let sessionId = invocation.resumeSessionId;
|
|
4283
|
+
let costUsd = 0;
|
|
4284
|
+
let turns = 0;
|
|
4285
|
+
const deadline = Date.now() + roleConfig.maxMs;
|
|
4286
|
+
const outcome = (kind, text = "", error = "") => kind === "completed" ? { kind, text, sessionId, costUsd, turns } : kind === "failed" ? { kind, error, sessionId, costUsd, turns } : { kind, sessionId, costUsd, turns };
|
|
4287
|
+
try {
|
|
4288
|
+
const response = query2({
|
|
4289
|
+
prompt: invocation.userPrompt,
|
|
4290
|
+
options: {
|
|
4291
|
+
model: roleConfig.model,
|
|
4292
|
+
cwd: invocation.cwd,
|
|
4293
|
+
systemPrompt: invocation.systemPrompt,
|
|
4294
|
+
thinking: { type: "adaptive" },
|
|
4295
|
+
...roleConfig.effort ? { effort: roleConfig.effort } : {},
|
|
4296
|
+
tools: [...allowedTools],
|
|
4297
|
+
...hostTools.length > 0 ? { mcpServers: { engine: createSdkMcpServer({ name: "engine", tools: hostTools }) } } : {},
|
|
4298
|
+
hooks: buildToolPolicyHooks(invocation.cwd),
|
|
4299
|
+
canUseTool: createToolPermissionCallback(invocation.cwd),
|
|
4300
|
+
maxTurns: roleConfig.maxTurns,
|
|
4301
|
+
...invocation.resumeSessionId ? { resume: invocation.resumeSessionId } : {},
|
|
4302
|
+
settingSources: []
|
|
4303
|
+
}
|
|
4304
|
+
});
|
|
4305
|
+
for await (const raw of response) {
|
|
4306
|
+
if (raw.session_id) {
|
|
4307
|
+
sessionId = raw.session_id;
|
|
4308
|
+
}
|
|
4309
|
+
if (raw.type === "assistant") {
|
|
4310
|
+
turns += 1;
|
|
4311
|
+
const block = raw.message?.content?.find(
|
|
4312
|
+
(candidate) => candidate.type === "text" || candidate.type === "tool_use"
|
|
4313
|
+
);
|
|
4314
|
+
if (block?.type === "text" && block.text?.trim()) {
|
|
4315
|
+
onProgress((block.text.trim().split("\n")[0] ?? "").slice(0, 120));
|
|
4316
|
+
} else if (block?.type === "tool_use" && block.name) {
|
|
4317
|
+
onProgress(`using ${block.name}`);
|
|
4318
|
+
}
|
|
4319
|
+
if (Date.now() > deadline) {
|
|
4320
|
+
await response.interrupt?.();
|
|
4321
|
+
return outcome("budget_exhausted");
|
|
4322
|
+
}
|
|
4323
|
+
} else if (raw.type === "result") {
|
|
4324
|
+
costUsd = raw.total_cost_usd ?? 0;
|
|
4325
|
+
const subtype = raw.subtype ?? "";
|
|
4326
|
+
if (subtype === "success") {
|
|
4327
|
+
return outcome("completed", raw.result ?? "");
|
|
4328
|
+
}
|
|
4329
|
+
if (subtype.includes("max_turns") || subtype.includes("max_budget")) {
|
|
4330
|
+
return outcome("budget_exhausted");
|
|
4331
|
+
}
|
|
4332
|
+
const detail = raw.result ?? subtype;
|
|
4333
|
+
if (classifyFailure(detail) === "context_overflow") {
|
|
4334
|
+
return outcome("context_overflow");
|
|
4335
|
+
}
|
|
4336
|
+
return outcome("failed", "", detail || "model run failed");
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
return outcome("failed", "", "model stream ended without a result");
|
|
4340
|
+
} catch (error) {
|
|
4341
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4342
|
+
if (classifyFailure(detail) === "context_overflow") {
|
|
4343
|
+
return outcome("context_overflow");
|
|
4344
|
+
}
|
|
4345
|
+
return outcome("failed", "", detail);
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
// src/engine/providers/openai.ts
|
|
4352
|
+
import { readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4353
|
+
import { join as join8, resolve as resolve2, sep } from "path";
|
|
4354
|
+
function jailedPath(cwd, candidate) {
|
|
4355
|
+
const resolved = resolve2(cwd, candidate);
|
|
4356
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + sep)) {
|
|
4357
|
+
throw new Error(`path ${candidate} escapes the repository`);
|
|
4358
|
+
}
|
|
4359
|
+
return resolved;
|
|
4360
|
+
}
|
|
4361
|
+
function fileTools(cwd, allowWrite) {
|
|
4362
|
+
const tools = [
|
|
4363
|
+
{
|
|
4364
|
+
name: "read_file",
|
|
4365
|
+
description: "Read a repository file (UTF-8, truncated at 40000 chars).",
|
|
4366
|
+
inputSchema: {},
|
|
4367
|
+
jsonSchema: {
|
|
4368
|
+
type: "object",
|
|
4369
|
+
properties: { path: { type: "string" } },
|
|
4370
|
+
required: ["path"]
|
|
4371
|
+
},
|
|
4372
|
+
handler: async (input) => {
|
|
4373
|
+
const content = readFileSync3(jailedPath(cwd, String(input.path ?? "")), "utf8");
|
|
4374
|
+
return { content: content.slice(0, 4e4) };
|
|
4375
|
+
}
|
|
4376
|
+
},
|
|
4377
|
+
{
|
|
4378
|
+
name: "list_dir",
|
|
4379
|
+
description: "List entries of a repository directory.",
|
|
4380
|
+
inputSchema: {},
|
|
4381
|
+
jsonSchema: {
|
|
4382
|
+
type: "object",
|
|
4383
|
+
properties: { path: { type: "string" } },
|
|
4384
|
+
required: ["path"]
|
|
4385
|
+
},
|
|
4386
|
+
handler: async (input) => {
|
|
4387
|
+
const target9 = jailedPath(cwd, String(input.path ?? "."));
|
|
4388
|
+
const entries = readdirSync(target9).map((entry) => {
|
|
4389
|
+
const kind = statSync(join8(target9, entry)).isDirectory() ? "dir" : "file";
|
|
4390
|
+
return `${kind}:${entry}`;
|
|
4391
|
+
});
|
|
4392
|
+
return { entries: entries.slice(0, 500) };
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
];
|
|
4396
|
+
if (allowWrite) {
|
|
4397
|
+
tools.push(
|
|
4398
|
+
{
|
|
4399
|
+
name: "write_file",
|
|
4400
|
+
description: "Create or overwrite a repository file with the given content.",
|
|
4401
|
+
inputSchema: {},
|
|
4402
|
+
jsonSchema: {
|
|
4403
|
+
type: "object",
|
|
4404
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
4405
|
+
required: ["path", "content"]
|
|
4406
|
+
},
|
|
4407
|
+
handler: async (input) => {
|
|
4408
|
+
writeFileSync2(jailedPath(cwd, String(input.path ?? "")), String(input.content ?? ""));
|
|
4409
|
+
return { ok: true };
|
|
4410
|
+
}
|
|
4411
|
+
},
|
|
4412
|
+
{
|
|
4413
|
+
name: "replace_in_file",
|
|
4414
|
+
description: "Replace one exact occurrence of a string in a repository file.",
|
|
4415
|
+
inputSchema: {},
|
|
4416
|
+
jsonSchema: {
|
|
4417
|
+
type: "object",
|
|
4418
|
+
properties: {
|
|
4419
|
+
path: { type: "string" },
|
|
4420
|
+
oldText: { type: "string" },
|
|
4421
|
+
newText: { type: "string" }
|
|
4422
|
+
},
|
|
4423
|
+
required: ["path", "oldText", "newText"]
|
|
4424
|
+
},
|
|
4425
|
+
handler: async (input) => {
|
|
4426
|
+
const path = jailedPath(cwd, String(input.path ?? ""));
|
|
4427
|
+
const content = readFileSync3(path, "utf8");
|
|
4428
|
+
const oldText = String(input.oldText ?? "");
|
|
4429
|
+
if (!content.includes(oldText)) {
|
|
4430
|
+
return { ok: false, reason: "oldText not found" };
|
|
4431
|
+
}
|
|
4432
|
+
writeFileSync2(path, content.replace(oldText, String(input.newText ?? "")));
|
|
4433
|
+
return { ok: true };
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
);
|
|
4437
|
+
}
|
|
4438
|
+
return tools;
|
|
4439
|
+
}
|
|
4440
|
+
function createOpenAIProvider(config, session) {
|
|
4441
|
+
let runId;
|
|
4442
|
+
let conversationId;
|
|
4443
|
+
const gatewayFetch = async (path, body) => {
|
|
4444
|
+
if (!runId) {
|
|
4445
|
+
throw new Error("OpenAI provider used before the run was created");
|
|
4446
|
+
}
|
|
4447
|
+
const response = await fetch(
|
|
4448
|
+
`${config.apiBaseUrl}/wizard/openai/runs/${encodeURIComponent(runId)}${path}`,
|
|
4449
|
+
{
|
|
4450
|
+
method: "POST",
|
|
4451
|
+
headers: {
|
|
4452
|
+
"Content-Type": "application/json",
|
|
4453
|
+
Authorization: `Bearer ${session.token}`
|
|
4454
|
+
},
|
|
4455
|
+
body: JSON.stringify(body)
|
|
4456
|
+
}
|
|
4457
|
+
);
|
|
4458
|
+
const text = await response.text();
|
|
4459
|
+
let payload;
|
|
4460
|
+
try {
|
|
4461
|
+
payload = text ? JSON.parse(text) : {};
|
|
4462
|
+
} catch {
|
|
4463
|
+
payload = {};
|
|
4464
|
+
}
|
|
4465
|
+
if (!response.ok) {
|
|
4466
|
+
const detail = payload.message ?? text;
|
|
4467
|
+
const error = new Error(detail || `gateway request failed (${response.status})`);
|
|
4468
|
+
error.code = payload.code;
|
|
4469
|
+
throw error;
|
|
4470
|
+
}
|
|
4471
|
+
return payload;
|
|
4472
|
+
};
|
|
4473
|
+
const ensureConversation = async (resumeId) => {
|
|
4474
|
+
if (conversationId) {
|
|
4475
|
+
return conversationId;
|
|
4476
|
+
}
|
|
4477
|
+
if (resumeId) {
|
|
4478
|
+
conversationId = resumeId;
|
|
4479
|
+
return resumeId;
|
|
4480
|
+
}
|
|
4481
|
+
try {
|
|
4482
|
+
const created = await gatewayFetch("/v1/conversations", {});
|
|
4483
|
+
conversationId = created.id;
|
|
4484
|
+
} catch (error) {
|
|
4485
|
+
if (error.code !== "conversation_already_bound") {
|
|
4486
|
+
throw error;
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
if (!conversationId) {
|
|
4490
|
+
throw new Error("wizard run already has a bound conversation; rerun with its resume state");
|
|
4491
|
+
}
|
|
4492
|
+
return conversationId;
|
|
4493
|
+
};
|
|
4494
|
+
return {
|
|
4495
|
+
name: "openai-gateway",
|
|
4496
|
+
onRunReady(readyRunId) {
|
|
4497
|
+
runId = readyRunId;
|
|
4498
|
+
},
|
|
4499
|
+
async invoke(invocation, roleConfig, onProgress) {
|
|
4500
|
+
let costUsd = 0;
|
|
4501
|
+
let turns = 0;
|
|
4502
|
+
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 };
|
|
4503
|
+
const allTools = [...invocation.tools, ...fileTools(invocation.cwd, invocation.allowRepoWrite)];
|
|
4504
|
+
const toolsByName = new Map(allTools.map((tool2) => [tool2.name, tool2]));
|
|
4505
|
+
const toolDefs = allTools.map((tool2) => ({
|
|
4506
|
+
type: "function",
|
|
4507
|
+
name: tool2.name,
|
|
4508
|
+
description: tool2.description,
|
|
4509
|
+
parameters: tool2.jsonSchema
|
|
4510
|
+
}));
|
|
4511
|
+
try {
|
|
4512
|
+
const conversation = await ensureConversation(invocation.resumeSessionId);
|
|
4513
|
+
const deadline = Date.now() + roleConfig.maxMs;
|
|
4514
|
+
let input = [
|
|
4515
|
+
{ role: "user", content: invocation.userPrompt }
|
|
4516
|
+
];
|
|
4517
|
+
for (let turn = 0; turn < roleConfig.maxTurns; turn += 1) {
|
|
4518
|
+
turns = turn + 1;
|
|
4519
|
+
if (Date.now() > deadline) {
|
|
4520
|
+
return outcome("budget_exhausted");
|
|
4521
|
+
}
|
|
4522
|
+
const response = await gatewayFetch("/v1/responses", {
|
|
4523
|
+
model: roleConfig.model,
|
|
4524
|
+
instructions: invocation.systemPrompt,
|
|
4525
|
+
conversation,
|
|
4526
|
+
input,
|
|
4527
|
+
tools: toolDefs,
|
|
4528
|
+
...roleConfig.effort ? { reasoning: { effort: roleConfig.effort } } : {}
|
|
4529
|
+
});
|
|
4530
|
+
const items = response.output ?? [];
|
|
4531
|
+
const calls = items.filter(
|
|
4532
|
+
(item) => item.type === "function_call"
|
|
4533
|
+
);
|
|
4534
|
+
if (calls.length === 0) {
|
|
4535
|
+
const text = response.output_text ?? items.filter((item) => item.type === "message").map(
|
|
4536
|
+
(item) => Array.isArray(item.content) ? item.content.map((block) => String(block.text ?? "")).join("") : ""
|
|
4537
|
+
).join("\n");
|
|
4538
|
+
return outcome("completed", String(text ?? ""));
|
|
4539
|
+
}
|
|
4540
|
+
const results = [];
|
|
4541
|
+
for (const call of calls) {
|
|
4542
|
+
onProgress(`using ${call.name}`);
|
|
4543
|
+
const tool2 = toolsByName.get(call.name);
|
|
4544
|
+
let output;
|
|
4545
|
+
if (!tool2) {
|
|
4546
|
+
output = { ok: false, reason: `unknown tool ${call.name}` };
|
|
4547
|
+
} else {
|
|
4548
|
+
try {
|
|
4549
|
+
output = await tool2.handler(JSON.parse(call.arguments || "{}"));
|
|
4550
|
+
} catch (error) {
|
|
4551
|
+
output = { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
results.push({
|
|
4555
|
+
type: "function_call_output",
|
|
4556
|
+
call_id: call.call_id,
|
|
4557
|
+
output: JSON.stringify(output)
|
|
4558
|
+
});
|
|
4559
|
+
}
|
|
4560
|
+
input = results;
|
|
4561
|
+
}
|
|
4562
|
+
return outcome("budget_exhausted");
|
|
4563
|
+
} catch (error) {
|
|
4564
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4565
|
+
if (classifyFailure(detail) === "context_overflow") {
|
|
4566
|
+
return outcome("context_overflow");
|
|
4567
|
+
}
|
|
4568
|
+
return outcome("failed", "", detail);
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
// src/engine/providers/router.ts
|
|
4575
|
+
function createRoutedProvider(routes, fallback) {
|
|
4576
|
+
const providers = /* @__PURE__ */ new Set([fallback, ...Object.values(routes)]);
|
|
4577
|
+
return {
|
|
4578
|
+
name: "routed",
|
|
4579
|
+
onRunReady(runId) {
|
|
4580
|
+
for (const provider of providers) {
|
|
4581
|
+
provider.onRunReady?.(runId);
|
|
4582
|
+
}
|
|
4583
|
+
},
|
|
4584
|
+
invoke(invocation, config, onProgress) {
|
|
4585
|
+
const provider = routes[invocation.role] ?? fallback;
|
|
4586
|
+
return provider.invoke(invocation, config, onProgress);
|
|
4587
|
+
}
|
|
4588
|
+
};
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
// src/engine/cli.ts
|
|
4592
|
+
var MINUTE = 6e4;
|
|
4593
|
+
function engineModelConfig(config) {
|
|
4594
|
+
const model = (role, fallback) => process.env[`WHISPERR_WIZARD_ENGINE_${role.toUpperCase()}_MODEL`]?.trim() || fallback;
|
|
4595
|
+
return {
|
|
4596
|
+
survey: { model: model("survey", "claude-sonnet-5"), effort: "medium", maxTurns: 40, maxMs: 12 * MINUTE },
|
|
4597
|
+
design: { model: model("design", config.plannerModel), effort: "high", maxTurns: 40, maxMs: 15 * MINUTE },
|
|
4598
|
+
edit: { model: model("edit", config.model), effort: config.effort, maxTurns: 50, maxMs: 15 * MINUTE },
|
|
4599
|
+
review: { model: model("review", config.model), effort: "medium", maxTurns: 15, maxMs: 8 * MINUTE }
|
|
4600
|
+
};
|
|
4601
|
+
}
|
|
4602
|
+
function engineProvider(config, session) {
|
|
4603
|
+
const claude = createClaudeProvider(config, session);
|
|
4604
|
+
const preset = process.env.WHISPERR_WIZARD_PRESET?.trim() ?? "claude-default";
|
|
4605
|
+
if (preset === "sol-design") {
|
|
4606
|
+
const openai = createOpenAIProvider(config, session);
|
|
4607
|
+
return createRoutedProvider({ survey: openai, design: openai, review: openai }, claude);
|
|
4608
|
+
}
|
|
4609
|
+
return claude;
|
|
4610
|
+
}
|
|
4611
|
+
async function tryEngineFlow(options) {
|
|
4612
|
+
const { repoPath, fingerprint, config, session, playbook, theme: theme2 } = options;
|
|
4613
|
+
const startedAt = Date.now();
|
|
4614
|
+
const sink = createProgressSink({
|
|
4615
|
+
json: false,
|
|
4616
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
4617
|
+
write: (line) => p.log.message(theme2.muted(line))
|
|
4618
|
+
});
|
|
4619
|
+
const result = await runEngine({
|
|
4620
|
+
repoPath,
|
|
4621
|
+
repoFingerprint: fingerprint,
|
|
4622
|
+
config,
|
|
4623
|
+
session,
|
|
4624
|
+
provider: engineProvider(config, session),
|
|
4625
|
+
models: engineModelConfig(config),
|
|
4626
|
+
target: playbook.target.id,
|
|
4627
|
+
projectKind: ["node", "python", "php", "go", "express", "django", "fastapi", "laravel", "spring-boot"].includes(
|
|
4628
|
+
playbook.target.id
|
|
4629
|
+
) ? "backend" : "frontend",
|
|
4630
|
+
playbookSystemPrompt: playbook.systemPrompt,
|
|
4631
|
+
sink,
|
|
4632
|
+
callbacks: {
|
|
4633
|
+
reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2),
|
|
4634
|
+
confirmApply: async (patch, summaryLines) => {
|
|
4635
|
+
p.note(summaryLines.join("\n"), "Integration result");
|
|
4636
|
+
const lineCount = patch.split("\n").length;
|
|
4637
|
+
const answer = await p.confirm({
|
|
4638
|
+
message: `Apply these changes to your working tree? (${lineCount} patch lines)`,
|
|
4639
|
+
initialValue: true
|
|
4640
|
+
});
|
|
4641
|
+
return answer === true;
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
});
|
|
4645
|
+
if (result.kind === "wrong_mode") {
|
|
4646
|
+
return { handled: false, exitCode: 0 };
|
|
4647
|
+
}
|
|
4648
|
+
if (result.kind === "aborted") {
|
|
4649
|
+
p.cancel(result.reason);
|
|
4650
|
+
return { handled: true, exitCode: 1 };
|
|
4651
|
+
}
|
|
4652
|
+
p.note(result.summary || "Nothing to integrate.", "Run summary");
|
|
4653
|
+
if (result.partialPatchPath) {
|
|
4654
|
+
p.log.warn(`Unapplied changes saved to ${result.partialPatchPath}`);
|
|
4655
|
+
}
|
|
4656
|
+
await postRunReport(config, session, {
|
|
4657
|
+
target: playbook.target.id,
|
|
4658
|
+
repo_fingerprint: fingerprint,
|
|
4659
|
+
identify_wired: result.identifyWired,
|
|
4660
|
+
verified: null,
|
|
4661
|
+
cost_usd: 0,
|
|
4662
|
+
duration_ms: Date.now() - startedAt,
|
|
4663
|
+
summary: result.summary.slice(0, 2e3),
|
|
4664
|
+
events: result.reportEvents,
|
|
4665
|
+
exit_class: result.kind === "completed" ? "engine_completed" : "engine_partial"
|
|
4666
|
+
});
|
|
4667
|
+
if (result.applied && result.registered.length > 0) {
|
|
4668
|
+
const spin = p.spinner();
|
|
4669
|
+
spin.start("Waiting for the first event (deploy or run your app)");
|
|
4670
|
+
const first = await pollFirstEvent(config, session, { timeoutMs: 9e4 });
|
|
4671
|
+
spin.stop(
|
|
4672
|
+
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.")
|
|
4673
|
+
);
|
|
4674
|
+
}
|
|
4675
|
+
return { handled: true, exitCode: result.kind === "completed" ? 0 : 1 };
|
|
4676
|
+
}
|
|
4677
|
+
async function reviewEventsInTerminal(proposed, theme2) {
|
|
4678
|
+
if (proposed.length === 0) {
|
|
4679
|
+
return [];
|
|
4680
|
+
}
|
|
4681
|
+
const lines = proposed.map((event) => {
|
|
4682
|
+
const fields = Object.keys(event.payloadSchema);
|
|
4683
|
+
return [
|
|
4684
|
+
`${theme2.bright(event.code)} \u2014 ${event.name}`,
|
|
4685
|
+
` ${event.reasoning}`,
|
|
4686
|
+
` anchor: ${event.anchorFile}${event.anchorSymbol ? ` (${event.anchorSymbol})` : ""}`,
|
|
4687
|
+
fields.length > 0 ? ` payload: ${fields.join(", ")}` : " payload: none"
|
|
4688
|
+
].join("\n");
|
|
4689
|
+
});
|
|
4690
|
+
p.note(lines.join("\n\n"), `Proposed events (${proposed.length})`);
|
|
4691
|
+
const selection = await p.multiselect({
|
|
4692
|
+
message: "Register these events? Deselect any you don't want.",
|
|
4693
|
+
options: proposed.map((event) => ({ value: event.code, label: event.code, hint: event.name })),
|
|
4694
|
+
initialValues: proposed.map((event) => event.code),
|
|
4695
|
+
required: false
|
|
4696
|
+
});
|
|
4697
|
+
if (p.isCancel(selection)) {
|
|
4698
|
+
return null;
|
|
4699
|
+
}
|
|
4700
|
+
const chosen = new Set(selection);
|
|
4701
|
+
return proposed.filter((event) => chosen.has(event.code));
|
|
4702
|
+
}
|
|
4703
|
+
|
|
2694
4704
|
// src/core/opportunities.ts
|
|
2695
4705
|
function normalizeCode(value) {
|
|
2696
4706
|
return value.trim().toLowerCase().replace(/[ \-./]/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
@@ -2909,33 +4919,8 @@ function normalizeSide(value) {
|
|
|
2909
4919
|
}
|
|
2910
4920
|
}
|
|
2911
4921
|
|
|
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
4922
|
// src/core/postflight.ts
|
|
2938
|
-
import { spawn as
|
|
4923
|
+
import { spawn as spawn4 } from "child_process";
|
|
2939
4924
|
function verdictToVerified(verdict) {
|
|
2940
4925
|
if (verdict.toolMissing || verdict.timedOut) return null;
|
|
2941
4926
|
return verdict.ok;
|
|
@@ -2947,8 +4932,8 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2947
4932
|
return Promise.resolve({ ran: false, ok: true, output: "" });
|
|
2948
4933
|
}
|
|
2949
4934
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2950
|
-
return new Promise((
|
|
2951
|
-
const child =
|
|
4935
|
+
return new Promise((resolve4) => {
|
|
4936
|
+
const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
|
|
2952
4937
|
let out = "";
|
|
2953
4938
|
const append = (d) => {
|
|
2954
4939
|
out += d.toString();
|
|
@@ -2958,11 +4943,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2958
4943
|
child.stderr?.on("data", append);
|
|
2959
4944
|
const timer = setTimeout(() => {
|
|
2960
4945
|
child.kill("SIGKILL");
|
|
2961
|
-
|
|
4946
|
+
resolve4({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
|
|
2962
4947
|
}, timeoutMs);
|
|
2963
4948
|
child.on("close", (code) => {
|
|
2964
4949
|
clearTimeout(timer);
|
|
2965
|
-
|
|
4950
|
+
resolve4({
|
|
2966
4951
|
ran: true,
|
|
2967
4952
|
ok: code === 0,
|
|
2968
4953
|
command,
|
|
@@ -2974,7 +4959,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2974
4959
|
});
|
|
2975
4960
|
child.on("error", () => {
|
|
2976
4961
|
clearTimeout(timer);
|
|
2977
|
-
|
|
4962
|
+
resolve4({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
|
|
2978
4963
|
});
|
|
2979
4964
|
});
|
|
2980
4965
|
}
|
|
@@ -2983,68 +4968,22 @@ function tail2(s) {
|
|
|
2983
4968
|
return t.length > MAX_OUTPUT ? `\u2026${t.slice(-MAX_OUTPUT)}` : t;
|
|
2984
4969
|
}
|
|
2985
4970
|
|
|
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
4971
|
// src/core/version.ts
|
|
3033
|
-
import { readFileSync } from "fs";
|
|
3034
|
-
import { dirname as
|
|
4972
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4973
|
+
import { dirname as dirname3, join as join9, parse } from "path";
|
|
3035
4974
|
import { fileURLToPath } from "url";
|
|
3036
4975
|
var PACKAGE_NAME = "@whisperr/wizard";
|
|
3037
4976
|
function packageVersion() {
|
|
3038
|
-
let dir =
|
|
4977
|
+
let dir = dirname3(fileURLToPath(import.meta.url));
|
|
3039
4978
|
const { root } = parse(dir);
|
|
3040
4979
|
while (true) {
|
|
3041
4980
|
try {
|
|
3042
|
-
const pkg = JSON.parse(
|
|
4981
|
+
const pkg = JSON.parse(readFileSync4(join9(dir, "package.json"), "utf8"));
|
|
3043
4982
|
if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
|
|
3044
4983
|
} catch {
|
|
3045
4984
|
}
|
|
3046
4985
|
if (dir === root) return "0.0.0";
|
|
3047
|
-
dir =
|
|
4986
|
+
dir = dirname3(dir);
|
|
3048
4987
|
}
|
|
3049
4988
|
}
|
|
3050
4989
|
|
|
@@ -3177,12 +5116,12 @@ function banner() {
|
|
|
3177
5116
|
|
|
3178
5117
|
// src/cli.ts
|
|
3179
5118
|
async function run2(options) {
|
|
3180
|
-
const repoPath =
|
|
5119
|
+
const repoPath = resolve3(options.path ?? process.cwd());
|
|
3181
5120
|
const config = resolveConfig(options);
|
|
3182
5121
|
console.log(banner());
|
|
3183
|
-
|
|
5122
|
+
p2.intro(theme.signal("Let's wire Whisperr into your app."));
|
|
3184
5123
|
if (config.offline) {
|
|
3185
|
-
|
|
5124
|
+
p2.log.warn(
|
|
3186
5125
|
theme.warn("offline mode") + theme.muted(" \u2014 using a demo manifest, no account needed.")
|
|
3187
5126
|
);
|
|
3188
5127
|
}
|
|
@@ -3192,27 +5131,27 @@ async function run2(options) {
|
|
|
3192
5131
|
);
|
|
3193
5132
|
const chosen = await chooseTarget(detections);
|
|
3194
5133
|
if (!chosen) {
|
|
3195
|
-
|
|
5134
|
+
p2.cancel("No supported stack selected.");
|
|
3196
5135
|
return 1;
|
|
3197
5136
|
}
|
|
3198
5137
|
if (chosen.playbook.target.availability === "planned") {
|
|
3199
|
-
|
|
5138
|
+
p2.note(
|
|
3200
5139
|
`We can detect ${theme.bright(chosen.playbook.target.displayName)} but the Whisperr SDK for it isn't shipped yet.
|
|
3201
5140
|
Flutter is live today; ${theme.bright(
|
|
3202
5141
|
chosen.playbook.target.displayName
|
|
3203
5142
|
)} is next on the roadmap.`,
|
|
3204
5143
|
theme.warn("SDK coming soon")
|
|
3205
5144
|
);
|
|
3206
|
-
const cont = await
|
|
5145
|
+
const cont = await p2.confirm({
|
|
3207
5146
|
message: "Continue anyway and let the agent scaffold against the planned SDK?",
|
|
3208
5147
|
initialValue: false
|
|
3209
5148
|
});
|
|
3210
|
-
if (
|
|
3211
|
-
|
|
5149
|
+
if (p2.isCancel(cont) || !cont) {
|
|
5150
|
+
p2.outro(theme.muted("No problem \u2014 come back when the SDK lands."));
|
|
3212
5151
|
return 0;
|
|
3213
5152
|
}
|
|
3214
5153
|
} else {
|
|
3215
|
-
|
|
5154
|
+
p2.log.success(
|
|
3216
5155
|
`Detected ${theme.bright(chosen.playbook.target.displayName)} ` + theme.muted(`(${chosen.detection?.evidence.join(", ") ?? "selected"})`)
|
|
3217
5156
|
);
|
|
3218
5157
|
}
|
|
@@ -3220,43 +5159,56 @@ Flutter is live today; ${theme.bright(
|
|
|
3220
5159
|
try {
|
|
3221
5160
|
session = config.offline ? await authenticate(config) : await withBrowserAuth(config);
|
|
3222
5161
|
} catch (err) {
|
|
3223
|
-
|
|
5162
|
+
p2.cancel(theme.alert(err.message));
|
|
3224
5163
|
return 1;
|
|
3225
5164
|
}
|
|
3226
5165
|
startSessionKeepalive(config, session);
|
|
3227
5166
|
const fingerprint = await repoFingerprint(repoPath);
|
|
5167
|
+
if (!config.offline) {
|
|
5168
|
+
const engineOutcome = await tryEngineFlow({
|
|
5169
|
+
repoPath,
|
|
5170
|
+
fingerprint,
|
|
5171
|
+
config,
|
|
5172
|
+
session,
|
|
5173
|
+
playbook: chosen.playbook,
|
|
5174
|
+
theme
|
|
5175
|
+
});
|
|
5176
|
+
if (engineOutcome.handled) {
|
|
5177
|
+
return engineOutcome.exitCode;
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
3228
5180
|
const manifest = await withSpinner(
|
|
3229
5181
|
"Loading your onboarding context",
|
|
3230
5182
|
() => fetchManifest(config, session, chosen.playbook.target.id, fingerprint)
|
|
3231
5183
|
);
|
|
3232
5184
|
const approvedSuggestionsPromise = config.offline ? null : fetchSuggestions(config, session, ["approved"]);
|
|
3233
|
-
|
|
5185
|
+
p2.note(
|
|
3234
5186
|
summarizeManifest(manifest),
|
|
3235
5187
|
theme.signal(`Plan for ${manifest.appName ?? manifest.appId}`)
|
|
3236
5188
|
);
|
|
3237
5189
|
const checkpoint = await takeCheckpoint(repoPath);
|
|
3238
5190
|
if (!checkpoint.isRepo) {
|
|
3239
5191
|
if (!options.force) {
|
|
3240
|
-
|
|
5192
|
+
p2.cancel(
|
|
3241
5193
|
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
5194
|
);
|
|
3243
5195
|
return 1;
|
|
3244
5196
|
}
|
|
3245
|
-
|
|
5197
|
+
p2.log.warn(
|
|
3246
5198
|
theme.warn("not a git repo") + theme.muted(" \u2014 proceeding with --force; there is no automatic undo.")
|
|
3247
5199
|
);
|
|
3248
5200
|
} else if (!await isWorkingTreeClean(repoPath)) {
|
|
3249
5201
|
if (!options.force) {
|
|
3250
|
-
|
|
5202
|
+
p2.cancel(
|
|
3251
5203
|
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
5204
|
);
|
|
3253
5205
|
return 1;
|
|
3254
5206
|
}
|
|
3255
|
-
|
|
5207
|
+
p2.log.warn(
|
|
3256
5208
|
theme.warn("uncommitted changes present") + theme.muted(" \u2014 proceeding with --force; a revert would also drop your own changes.")
|
|
3257
5209
|
);
|
|
3258
5210
|
}
|
|
3259
|
-
const spin =
|
|
5211
|
+
const spin = p2.spinner();
|
|
3260
5212
|
const useIntegrationSpinner = Boolean(process.stdout.isTTY);
|
|
3261
5213
|
let phaseLabel = "Integrating";
|
|
3262
5214
|
let lastLine = "";
|
|
@@ -3272,7 +5224,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3272
5224
|
if (useIntegrationSpinner) {
|
|
3273
5225
|
spin.start(theme.bright(phaseLabel));
|
|
3274
5226
|
} else {
|
|
3275
|
-
|
|
5227
|
+
p2.log.step(theme.bright(phaseLabel));
|
|
3276
5228
|
}
|
|
3277
5229
|
const tick = useIntegrationSpinner ? setInterval(updateIntegrationMessage, 1e3) : void 0;
|
|
3278
5230
|
const additions = { proposed: 0, applied: [] };
|
|
@@ -3290,11 +5242,11 @@ Flutter is live today; ${theme.bright(
|
|
|
3290
5242
|
if (useIntegrationSpinner) {
|
|
3291
5243
|
spin.stop(theme.success("Placement plan ready"));
|
|
3292
5244
|
}
|
|
3293
|
-
|
|
5245
|
+
p2.note(renderPlacementPlan(plan), "Placement plan");
|
|
3294
5246
|
const placeEntries = plan.filter((entry) => entry.decision === "place");
|
|
3295
5247
|
let selectedEvents = placeEntries.map((entry) => entry.event);
|
|
3296
5248
|
if (config.reviewPlan && interactive && placeEntries.length) {
|
|
3297
|
-
const selection = await
|
|
5249
|
+
const selection = await p2.multiselect({
|
|
3298
5250
|
message: "Wire these events?",
|
|
3299
5251
|
options: placeEntries.map((entry) => ({
|
|
3300
5252
|
value: entry.event,
|
|
@@ -3304,7 +5256,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3304
5256
|
initialValues: selectedEvents,
|
|
3305
5257
|
required: false
|
|
3306
5258
|
});
|
|
3307
|
-
selectedEvents =
|
|
5259
|
+
selectedEvents = p2.isCancel(selection) ? [] : selection;
|
|
3308
5260
|
}
|
|
3309
5261
|
if (useIntegrationSpinner) {
|
|
3310
5262
|
spin.start(theme.bright("Continuing integration"));
|
|
@@ -3333,7 +5285,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3333
5285
|
if (useIntegrationSpinner) {
|
|
3334
5286
|
updateIntegrationMessage();
|
|
3335
5287
|
} else {
|
|
3336
|
-
|
|
5288
|
+
p2.log.step(theme.bright(label));
|
|
3337
5289
|
}
|
|
3338
5290
|
},
|
|
3339
5291
|
onActivity(line) {
|
|
@@ -3344,7 +5296,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3344
5296
|
}
|
|
3345
5297
|
}
|
|
3346
5298
|
}).catch((err) => {
|
|
3347
|
-
|
|
5299
|
+
p2.log.error(err.message);
|
|
3348
5300
|
return null;
|
|
3349
5301
|
});
|
|
3350
5302
|
if (tick) clearInterval(tick);
|
|
@@ -3400,7 +5352,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3400
5352
|
opportunities_applied: additions.applied.length
|
|
3401
5353
|
});
|
|
3402
5354
|
if (!result.ok) {
|
|
3403
|
-
|
|
5355
|
+
p2.log.warn(
|
|
3404
5356
|
theme.warn("Couldn't record this run's coverage on the server") + theme.muted(
|
|
3405
5357
|
` (${formatReportFailure(result)}) \u2014 future runs may re-propose events already wired here.`
|
|
3406
5358
|
)
|
|
@@ -3412,7 +5364,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3412
5364
|
if (useIntegrationSpinner) {
|
|
3413
5365
|
spin.stop(theme.alert("Integration stopped"));
|
|
3414
5366
|
} else {
|
|
3415
|
-
|
|
5367
|
+
p2.log.error(theme.alert("Integration stopped"));
|
|
3416
5368
|
}
|
|
3417
5369
|
const reverted = await maybeRevert(
|
|
3418
5370
|
repoPath,
|
|
@@ -3426,13 +5378,13 @@ Flutter is live today; ${theme.bright(
|
|
|
3426
5378
|
if (useIntegrationSpinner) {
|
|
3427
5379
|
spin.stop(stopLabel);
|
|
3428
5380
|
} else if (!outcome.coreOk) {
|
|
3429
|
-
|
|
5381
|
+
p2.log.warn(stopLabel);
|
|
3430
5382
|
} else {
|
|
3431
|
-
|
|
5383
|
+
p2.log.success(stopLabel);
|
|
3432
5384
|
}
|
|
3433
5385
|
let files = await changedFiles(repoPath, checkpoint);
|
|
3434
5386
|
if (files.length) {
|
|
3435
|
-
|
|
5387
|
+
p2.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
3436
5388
|
}
|
|
3437
5389
|
logAgentSummary(outcome.summary);
|
|
3438
5390
|
if (!outcome.coreOk) {
|
|
@@ -3443,14 +5395,14 @@ Flutter is live today; ${theme.bright(
|
|
|
3443
5395
|
);
|
|
3444
5396
|
if (reverted) {
|
|
3445
5397
|
await reportRun("reverted");
|
|
3446
|
-
|
|
5398
|
+
p2.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
3447
5399
|
return 1;
|
|
3448
5400
|
}
|
|
3449
5401
|
await reportRun("core_failed");
|
|
3450
5402
|
}
|
|
3451
5403
|
if (chosen.playbook.verifyCommand && files.length) {
|
|
3452
5404
|
const cmd = chosen.playbook.verifyCommand;
|
|
3453
|
-
const vspin =
|
|
5405
|
+
const vspin = p2.spinner();
|
|
3454
5406
|
vspin.start(`Verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
3455
5407
|
let verdict = await runVerifyCommand(repoPath, cmd);
|
|
3456
5408
|
verified = verdictToVerified(verdict);
|
|
@@ -3470,7 +5422,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3470
5422
|
vspin.stop(
|
|
3471
5423
|
theme.warn(`Verification failed \u2014 ${cmd}`) + theme.muted(" \u2014 attempting one repair pass")
|
|
3472
5424
|
);
|
|
3473
|
-
const repairSpin =
|
|
5425
|
+
const repairSpin = p2.spinner();
|
|
3474
5426
|
repairSpin.start(`Repairing verifier failures \u2014 ${theme.muted(cmd)}`);
|
|
3475
5427
|
try {
|
|
3476
5428
|
const repair = await runRepairPass({
|
|
@@ -3495,7 +5447,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3495
5447
|
);
|
|
3496
5448
|
}
|
|
3497
5449
|
files = await changedFiles(repoPath, checkpoint);
|
|
3498
|
-
const reverifySpin =
|
|
5450
|
+
const reverifySpin = p2.spinner();
|
|
3499
5451
|
reverifySpin.start(`Re-verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
3500
5452
|
verdict = await runVerifyCommand(repoPath, cmd);
|
|
3501
5453
|
verified = verdictToVerified(verdict);
|
|
@@ -3519,7 +5471,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3519
5471
|
}
|
|
3520
5472
|
if (!verdict.ok && !verdict.toolMissing && !verdict.timedOut) {
|
|
3521
5473
|
if (verdict.output) {
|
|
3522
|
-
|
|
5474
|
+
p2.note(verdict.output, theme.warn("Verifier output"));
|
|
3523
5475
|
}
|
|
3524
5476
|
const reverted = await maybeRevert(
|
|
3525
5477
|
repoPath,
|
|
@@ -3528,7 +5480,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3528
5480
|
);
|
|
3529
5481
|
if (reverted) {
|
|
3530
5482
|
await reportRun("reverted");
|
|
3531
|
-
|
|
5483
|
+
p2.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
3532
5484
|
return 1;
|
|
3533
5485
|
}
|
|
3534
5486
|
await reportRun("verify_failed");
|
|
@@ -3568,7 +5520,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3568
5520
|
)
|
|
3569
5521
|
)).filter(Boolean).length;
|
|
3570
5522
|
if (marked > 0) {
|
|
3571
|
-
|
|
5523
|
+
p2.log.info(
|
|
3572
5524
|
theme.muted(
|
|
3573
5525
|
`${marked} approved suggestion${marked === 1 ? "" : "s"} marked integrated.`
|
|
3574
5526
|
)
|
|
@@ -3579,7 +5531,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3579
5531
|
}
|
|
3580
5532
|
const eventLines = renderEventOutcomeLines(outcome.eventOutcomes, wiredMap);
|
|
3581
5533
|
if (eventLines) {
|
|
3582
|
-
|
|
5534
|
+
p2.note(eventLines, "Events");
|
|
3583
5535
|
}
|
|
3584
5536
|
const gapReport = buildGapReport({
|
|
3585
5537
|
manifest,
|
|
@@ -3590,7 +5542,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3590
5542
|
}))
|
|
3591
5543
|
});
|
|
3592
5544
|
if (gapReport.hasContent) {
|
|
3593
|
-
|
|
5545
|
+
p2.note(
|
|
3594
5546
|
renderGapReport(gapReport, theme),
|
|
3595
5547
|
theme.signal("What you could be doing")
|
|
3596
5548
|
);
|
|
@@ -3598,16 +5550,16 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3598
5550
|
const eventDenominator = manifest.universeSummary?.wireableHere ?? reportEvents.length;
|
|
3599
5551
|
const eventStatsLabel = manifest.universeSummary ? "events wired here" : "events wired";
|
|
3600
5552
|
const timingDetails = outcome.phaseTimings.map(({ phase, ms }) => `${shortPhaseLabel(phase)} ${formatDuration(ms)}`).join(" \xB7 ");
|
|
3601
|
-
|
|
5553
|
+
p2.log.info(
|
|
3602
5554
|
theme.muted(
|
|
3603
5555
|
`${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
5556
|
)
|
|
3605
5557
|
);
|
|
3606
5558
|
if (!config.offline) {
|
|
3607
|
-
|
|
5559
|
+
p2.log.step(
|
|
3608
5560
|
theme.bright("Run your app once") + theme.muted(" and trigger any tracked action \u2014 I'll watch for the first event.")
|
|
3609
5561
|
);
|
|
3610
|
-
const verifySpin =
|
|
5562
|
+
const verifySpin = p2.spinner();
|
|
3611
5563
|
verifySpin.start("Waiting for the first event from your app");
|
|
3612
5564
|
const first = await pollFirstEvent(config, session, { timeoutMs: 12e4 });
|
|
3613
5565
|
if (first.received) {
|
|
@@ -3631,8 +5583,8 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3631
5583
|
`${theme.signal("3.")} Commit & deploy \u2014 Whisperr starts working on real users.`,
|
|
3632
5584
|
undo ? theme.muted(`Undo everything: ${undo}`) : ""
|
|
3633
5585
|
].filter(Boolean).join("\n");
|
|
3634
|
-
|
|
3635
|
-
|
|
5586
|
+
p2.note(nextSteps, theme.bright("Next"));
|
|
5587
|
+
p2.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
|
|
3636
5588
|
return 0;
|
|
3637
5589
|
}
|
|
3638
5590
|
async function applyOpportunities(opts) {
|
|
@@ -3651,7 +5603,7 @@ async function applyOpportunities(opts) {
|
|
|
3651
5603
|
additions.proposed = opportunities.events.length + opportunities.interventions.length;
|
|
3652
5604
|
if (additions.proposed === 0) return [];
|
|
3653
5605
|
const describe = (kind, code, why) => `${theme.muted(`${kind}: `)}${theme.bright(code)}${why ? theme.muted(` \u2014 ${why}`) : ""}`;
|
|
3654
|
-
|
|
5606
|
+
p2.note(
|
|
3655
5607
|
[
|
|
3656
5608
|
...opportunities.events.map((e) => describe("event", e.code, e.rationale ?? e.description)),
|
|
3657
5609
|
...opportunities.interventions.map(
|
|
@@ -3661,7 +5613,7 @@ async function applyOpportunities(opts) {
|
|
|
3661
5613
|
theme.signal("Opportunities found beyond your onboarding plan")
|
|
3662
5614
|
);
|
|
3663
5615
|
if (config.offline) {
|
|
3664
|
-
|
|
5616
|
+
p2.log.info(theme.muted("Offline mode \u2014 proposals shown only, nothing submitted."));
|
|
3665
5617
|
return [];
|
|
3666
5618
|
}
|
|
3667
5619
|
if (config.proposeOnly) {
|
|
@@ -3675,7 +5627,7 @@ async function applyOpportunities(opts) {
|
|
|
3675
5627
|
() => submitAdditions(config, session, playbook.target.id, fingerprint, opportunities)
|
|
3676
5628
|
);
|
|
3677
5629
|
} catch (err) {
|
|
3678
|
-
|
|
5630
|
+
p2.log.warn(
|
|
3679
5631
|
theme.warn("Couldn't add the proposals") + theme.muted(` \u2014 ${err.message}. Your universe is unchanged.`)
|
|
3680
5632
|
);
|
|
3681
5633
|
return [];
|
|
@@ -3707,9 +5659,9 @@ async function applyOpportunities(opts) {
|
|
|
3707
5659
|
)
|
|
3708
5660
|
);
|
|
3709
5661
|
}
|
|
3710
|
-
|
|
5662
|
+
p2.note(lines.join("\n"), theme.signal("Universe updated"));
|
|
3711
5663
|
if (result.policyRegen?.status === "failed") {
|
|
3712
|
-
|
|
5664
|
+
p2.log.warn(
|
|
3713
5665
|
theme.warn("Live runtime NOT updated") + theme.muted(
|
|
3714
5666
|
" \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
5667
|
)
|
|
@@ -3730,13 +5682,13 @@ async function stageOpportunities(config, session, playbook, fingerprint, opport
|
|
|
3730
5682
|
() => submitSuggestions(config, session, playbook.target.id, fingerprint, opportunities)
|
|
3731
5683
|
);
|
|
3732
5684
|
} catch (err) {
|
|
3733
|
-
|
|
5685
|
+
p2.log.warn(
|
|
3734
5686
|
theme.warn("Couldn't send the proposals") + theme.muted(` \u2014 ${err.message}. Nothing was submitted.`)
|
|
3735
5687
|
);
|
|
3736
5688
|
return;
|
|
3737
5689
|
}
|
|
3738
5690
|
if (!stageResult) {
|
|
3739
|
-
|
|
5691
|
+
p2.log.warn(
|
|
3740
5692
|
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
5693
|
);
|
|
3742
5694
|
return;
|
|
@@ -3751,9 +5703,9 @@ async function stageOpportunities(config, session, playbook, fingerprint, opport
|
|
|
3751
5703
|
`${stageResult.staged} staged \xB7 ${stageResult.duplicates} already queued \xB7 ${stageResult.invalid} rejected`
|
|
3752
5704
|
)
|
|
3753
5705
|
);
|
|
3754
|
-
|
|
5706
|
+
p2.note(lines.join("\n"), theme.signal("Suggestions submitted"));
|
|
3755
5707
|
const where = stageResult.approvalsUrl ? `approve at ${theme.bright(stageResult.approvalsUrl)}` : "approve them in your Whisperr dashboard";
|
|
3756
|
-
|
|
5708
|
+
p2.log.info(
|
|
3757
5709
|
theme.muted(
|
|
3758
5710
|
`${stageResult.staged} proposal${stageResult.staged === 1 ? "" : "s"} sent \u2014 ${where}. The next wizard run wires whatever you approve.`
|
|
3759
5711
|
)
|
|
@@ -3763,20 +5715,20 @@ async function maybeRevert(repoPath, checkpoint, reason) {
|
|
|
3763
5715
|
if (!checkpoint.isRepo) return false;
|
|
3764
5716
|
const files = await changedFiles(repoPath, checkpoint);
|
|
3765
5717
|
if (files.length === 0) return false;
|
|
3766
|
-
const doRevert = await
|
|
5718
|
+
const doRevert = await p2.confirm({
|
|
3767
5719
|
message: `${reason} Revert all of the wizard's changes now?`,
|
|
3768
5720
|
initialValue: true
|
|
3769
5721
|
});
|
|
3770
|
-
if (
|
|
5722
|
+
if (p2.isCancel(doRevert) || !doRevert) {
|
|
3771
5723
|
const hint = revertHint(checkpoint);
|
|
3772
|
-
if (hint)
|
|
5724
|
+
if (hint) p2.log.info(theme.muted(`Left in your working tree. Undo anytime: ${hint}`));
|
|
3773
5725
|
return false;
|
|
3774
5726
|
}
|
|
3775
5727
|
const ok = await revertToCheckpoint(repoPath, checkpoint);
|
|
3776
5728
|
if (ok) {
|
|
3777
|
-
|
|
5729
|
+
p2.log.success(theme.success("Reverted to your pre-wizard state."));
|
|
3778
5730
|
} else {
|
|
3779
|
-
|
|
5731
|
+
p2.log.warn(
|
|
3780
5732
|
theme.warn("Couldn't revert automatically \u2014 undo manually: ") + (revertHint(checkpoint) ?? "git reset --hard")
|
|
3781
5733
|
);
|
|
3782
5734
|
}
|
|
@@ -3807,11 +5759,11 @@ async function chooseTarget(detections) {
|
|
|
3807
5759
|
hint: pb.target.availability === "available" ? void 0 : "coming soon"
|
|
3808
5760
|
});
|
|
3809
5761
|
}
|
|
3810
|
-
const answer = await
|
|
5762
|
+
const answer = await p2.select({
|
|
3811
5763
|
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
5764
|
options
|
|
3813
5765
|
});
|
|
3814
|
-
if (
|
|
5766
|
+
if (p2.isCancel(answer)) return null;
|
|
3815
5767
|
const playbook = playbookByTargetId(answer);
|
|
3816
5768
|
if (!playbook) return null;
|
|
3817
5769
|
return {
|
|
@@ -3820,11 +5772,11 @@ async function chooseTarget(detections) {
|
|
|
3820
5772
|
};
|
|
3821
5773
|
}
|
|
3822
5774
|
async function withBrowserAuth(config) {
|
|
3823
|
-
|
|
5775
|
+
p2.log.step(
|
|
3824
5776
|
theme.bright("Authenticate") + theme.muted(" \u2014 opening your browser to approve this device\u2026")
|
|
3825
5777
|
);
|
|
3826
5778
|
const auth = await startDeviceAuth(config);
|
|
3827
|
-
|
|
5779
|
+
p2.note(
|
|
3828
5780
|
[
|
|
3829
5781
|
theme.bright(`Your code: ${auth.userCode}`),
|
|
3830
5782
|
`${theme.muted("Approval URL:")} ${auth.verificationUrl}`,
|
|
@@ -3838,7 +5790,7 @@ async function withBrowserAuth(config) {
|
|
|
3838
5790
|
await open2(auth.verificationUrlComplete ?? auth.verificationUrl);
|
|
3839
5791
|
} catch {
|
|
3840
5792
|
}
|
|
3841
|
-
const spin =
|
|
5793
|
+
const spin = p2.spinner();
|
|
3842
5794
|
spin.start("Waiting for you to approve in the browser");
|
|
3843
5795
|
try {
|
|
3844
5796
|
const session = await auth.poll();
|
|
@@ -3850,7 +5802,7 @@ async function withBrowserAuth(config) {
|
|
|
3850
5802
|
}
|
|
3851
5803
|
}
|
|
3852
5804
|
async function withSpinner(label, fn) {
|
|
3853
|
-
const spin =
|
|
5805
|
+
const spin = p2.spinner();
|
|
3854
5806
|
spin.start(label);
|
|
3855
5807
|
try {
|
|
3856
5808
|
const result = await fn();
|
|
@@ -3913,7 +5865,7 @@ function renderEventOutcomeLines(outcomes, wiredMap) {
|
|
|
3913
5865
|
}
|
|
3914
5866
|
function logAgentSummary(summary) {
|
|
3915
5867
|
const cleaned = scrubSummary(summary).trim();
|
|
3916
|
-
if (cleaned)
|
|
5868
|
+
if (cleaned) p2.log.message(cleaned);
|
|
3917
5869
|
}
|
|
3918
5870
|
function shortPhaseLabel(phase) {
|
|
3919
5871
|
switch (phase) {
|