@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.
Files changed (2) hide show
  1. package/dist/index.js +2116 -164
  2. 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 resolve2 } from "path";
9
- import * as p from "@clack/prompts";
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((p2) => p2.target.id === id);
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((p2) => p2.detect(ctx).catch(() => null))
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 join2 } from "path";
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:${basename(repoPath)}`;
1247
- return createHash("sha256").update(seed).digest("hex").slice(0, 16);
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(join2(repoPath, file), "utf8") });
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((resolve3) => {
1295
- const child = spawn("git", args, { cwd });
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) => resolve3({ ok: code === 0, stdout, stderr }));
1301
- child.on("error", () => resolve3({ ok: false, stdout, stderr }));
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 p2 of e.properties) {
1473
- const req = p2.required ? " (required)" : "";
1474
- const desc = p2.description ? ` \u2014 ${p2.description}` : "";
1475
- lines.push(` \xB7 ${p2.name}${req}${desc}`);
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 spawn2 } from "child_process";
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((resolve3) => {
2951
- const child = spawn2(command, { cwd: repoPath, shell: true, env: process.env });
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
- resolve3({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
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
- resolve3({
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
- resolve3({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
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 dirname2, join as join3, parse } from "path";
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 = dirname2(fileURLToPath(import.meta.url));
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(readFileSync(join3(dir, "package.json"), "utf8"));
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 = dirname2(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 = resolve2(options.path ?? process.cwd());
5119
+ const repoPath = resolve3(options.path ?? process.cwd());
3181
5120
  const config = resolveConfig(options);
3182
5121
  console.log(banner());
3183
- p.intro(theme.signal("Let's wire Whisperr into your app."));
5122
+ p2.intro(theme.signal("Let's wire Whisperr into your app."));
3184
5123
  if (config.offline) {
3185
- p.log.warn(
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
- p.cancel("No supported stack selected.");
5134
+ p2.cancel("No supported stack selected.");
3196
5135
  return 1;
3197
5136
  }
3198
5137
  if (chosen.playbook.target.availability === "planned") {
3199
- p.note(
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 p.confirm({
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 (p.isCancel(cont) || !cont) {
3211
- p.outro(theme.muted("No problem \u2014 come back when the SDK lands."));
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
- p.log.success(
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
- p.cancel(theme.alert(err.message));
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
- p.note(
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
- p.cancel(
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
- p.log.warn(
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
- p.cancel(
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
- p.log.warn(
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 = p.spinner();
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
- p.log.step(theme.bright(phaseLabel));
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
- p.note(renderPlacementPlan(plan), "Placement plan");
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 p.multiselect({
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 = p.isCancel(selection) ? [] : selection;
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
- p.log.step(theme.bright(label));
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
- p.log.error(err.message);
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
- p.log.warn(
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
- p.log.error(theme.alert("Integration stopped"));
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
- p.log.warn(stopLabel);
5381
+ p2.log.warn(stopLabel);
3430
5382
  } else {
3431
- p.log.success(stopLabel);
5383
+ p2.log.success(stopLabel);
3432
5384
  }
3433
5385
  let files = await changedFiles(repoPath, checkpoint);
3434
5386
  if (files.length) {
3435
- p.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
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
- p.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
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 = p.spinner();
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 = p.spinner();
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 = p.spinner();
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
- p.note(verdict.output, theme.warn("Verifier output"));
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
- p.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
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
- p.log.info(
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
- p.note(eventLines, "Events");
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
- p.note(
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
- p.log.info(
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
- p.log.step(
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 = p.spinner();
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
- p.note(nextSteps, theme.bright("Next"));
3635
- p.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
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
- p.note(
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
- p.log.info(theme.muted("Offline mode \u2014 proposals shown only, nothing submitted."));
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
- p.log.warn(
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
- p.note(lines.join("\n"), theme.signal("Universe updated"));
5662
+ p2.note(lines.join("\n"), theme.signal("Universe updated"));
3711
5663
  if (result.policyRegen?.status === "failed") {
3712
- p.log.warn(
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
- p.log.warn(
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
- p.log.warn(
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
- p.note(lines.join("\n"), theme.signal("Suggestions submitted"));
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
- p.log.info(
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 p.confirm({
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 (p.isCancel(doRevert) || !doRevert) {
5722
+ if (p2.isCancel(doRevert) || !doRevert) {
3771
5723
  const hint = revertHint(checkpoint);
3772
- if (hint) p.log.info(theme.muted(`Left in your working tree. Undo anytime: ${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
- p.log.success(theme.success("Reverted to your pre-wizard state."));
5729
+ p2.log.success(theme.success("Reverted to your pre-wizard state."));
3778
5730
  } else {
3779
- p.log.warn(
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 p.select({
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 (p.isCancel(answer)) return null;
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
- p.log.step(
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
- p.note(
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 = p.spinner();
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 = p.spinner();
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) p.log.message(cleaned);
5868
+ if (cleaned) p2.log.message(cleaned);
3917
5869
  }
3918
5870
  function shortPhaseLabel(phase) {
3919
5871
  switch (phase) {