@whisperr/wizard 0.6.0 → 0.7.1

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