@whisperr/wizard 0.7.3 → 0.7.4

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 +522 -392
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { realpathSync } from "fs";
5
5
  import { fileURLToPath as fileURLToPath2 } from "url";
6
6
 
7
7
  // src/cli.ts
8
- import { resolve as resolve3 } from "path";
8
+ import { resolve as resolve5 } from "path";
9
9
  import * as p2 from "@clack/prompts";
10
10
  import open2 from "open";
11
11
 
@@ -1308,84 +1308,8 @@ function createProgressSink(options) {
1308
1308
  }
1309
1309
 
1310
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";
1311
+ import { readFile as readFile2 } from "fs/promises";
1312
+ import { basename, resolve as resolve2, sep as sep2 } from "path";
1389
1313
 
1390
1314
  // src/engine/bindings.ts
1391
1315
  function bindingsTargetFor(target9) {
@@ -1582,6 +1506,89 @@ function generateDart(events) {
1582
1506
  bindInstruction: "Call WhisperrEvents.bind(track) once, immediately after the Whisperr SDK is initialized, forwarding to the SDK's track call."
1583
1507
  };
1584
1508
  }
1509
+ function bindingsClassName(target9) {
1510
+ return bindingsTargetFor(target9) === "typescript" ? "WhisperrEvents" : "WhisperrEvents";
1511
+ }
1512
+ function eventWrapperName(code) {
1513
+ return `${bindingsClassName("")}.${camelCase(code)}`;
1514
+ }
1515
+
1516
+ // src/engine/clusters.ts
1517
+ import { createHash } from "crypto";
1518
+ import { dirname } from "path";
1519
+ var MIN_CLUSTER_EVENTS = 3;
1520
+ var MAX_CLUSTER_EVENTS = 8;
1521
+ function clusterIdFor(files) {
1522
+ const digest = createHash("sha256").update([...files].sort().join("\n")).digest("hex");
1523
+ return `cluster_${digest.slice(0, 12)}`;
1524
+ }
1525
+ function deriveClusters(placements) {
1526
+ const byFile = /* @__PURE__ */ new Map();
1527
+ for (const placement of placements) {
1528
+ const existing = byFile.get(placement.file) ?? [];
1529
+ existing.push(placement);
1530
+ byFile.set(placement.file, existing);
1531
+ }
1532
+ const byDirectory = /* @__PURE__ */ new Map();
1533
+ for (const [file, events] of byFile) {
1534
+ const directory = dirname(file);
1535
+ const bucket = byDirectory.get(directory) ?? { files: /* @__PURE__ */ new Set(), events: [] };
1536
+ bucket.files.add(file);
1537
+ bucket.events.push(...events);
1538
+ byDirectory.set(directory, bucket);
1539
+ }
1540
+ const clusters = [];
1541
+ const pending = [];
1542
+ for (const directory of [...byDirectory.keys()].sort()) {
1543
+ const bucket = byDirectory.get(directory);
1544
+ if (bucket.events.length >= MIN_CLUSTER_EVENTS) {
1545
+ pending.push(bucket);
1546
+ continue;
1547
+ }
1548
+ const last = pending[pending.length - 1];
1549
+ if (last && last.events.length + bucket.events.length <= MAX_CLUSTER_EVENTS) {
1550
+ for (const file of bucket.files) last.files.add(file);
1551
+ last.events.push(...bucket.events);
1552
+ } else {
1553
+ pending.push(bucket);
1554
+ }
1555
+ }
1556
+ for (const bucket of pending) {
1557
+ const events = [...bucket.events].sort((a, b) => a.eventCode.localeCompare(b.eventCode));
1558
+ for (let start = 0; start < events.length; start += MAX_CLUSTER_EVENTS) {
1559
+ const slice = events.slice(start, start + MAX_CLUSTER_EVENTS);
1560
+ const files = [...new Set(slice.map((event) => event.file))].sort();
1561
+ clusters.push({
1562
+ id: clusterIdFor(files.length > 0 ? files : [`slice_${start}`]),
1563
+ files,
1564
+ events: slice,
1565
+ status: "pending",
1566
+ attempts: 0
1567
+ });
1568
+ }
1569
+ }
1570
+ return clusters;
1571
+ }
1572
+ var TERMINAL_STATUSES = ["reviewed", "failed_resumable", "blocked"];
1573
+ function nextPendingCluster(clusters) {
1574
+ return clusters.find((cluster) => cluster.status === "pending" || cluster.status === "editing");
1575
+ }
1576
+ function clustersComplete(clusters) {
1577
+ return clusters.every((cluster) => TERMINAL_STATUSES.includes(cluster.status));
1578
+ }
1579
+ function transitionCluster(clusters, clusterId, status, note3) {
1580
+ return clusters.map((cluster) => {
1581
+ if (cluster.id !== clusterId) {
1582
+ return cluster;
1583
+ }
1584
+ const attempts = status === "editing" ? cluster.attempts + 1 : cluster.attempts;
1585
+ return { ...cluster, status, attempts, note: note3 ?? cluster.note };
1586
+ });
1587
+ }
1588
+
1589
+ // src/engine/integrate.ts
1590
+ import { writeFile } from "fs/promises";
1591
+ import { join as join3 } from "path";
1585
1592
 
1586
1593
  // src/engine/budgets.ts
1587
1594
  var OVERFLOW_PATTERNS = [
@@ -1666,7 +1673,7 @@ function npmCommand() {
1666
1673
  return process.platform === "win32" ? "npm.cmd" : "npm";
1667
1674
  }
1668
1675
  function runVerifyStep(cwd, step, timeoutMs = 3e5) {
1669
- return new Promise((resolve4) => {
1676
+ return new Promise((resolve6) => {
1670
1677
  const child = spawn(step.command, step.args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
1671
1678
  let output = "";
1672
1679
  const capture = (chunk) => {
@@ -1681,11 +1688,11 @@ function runVerifyStep(cwd, step, timeoutMs = 3e5) {
1681
1688
  }, timeoutMs);
1682
1689
  child.on("error", (error) => {
1683
1690
  clearTimeout(timer);
1684
- resolve4({ ...step, ok: false, output: String(error) });
1691
+ resolve6({ ...step, ok: false, output: String(error) });
1685
1692
  });
1686
1693
  child.on("close", (exitCode) => {
1687
1694
  clearTimeout(timer);
1688
- resolve4({ ...step, ok: exitCode === 0, output });
1695
+ resolve6({ ...step, ok: exitCode === 0, output });
1689
1696
  });
1690
1697
  });
1691
1698
  }
@@ -1857,6 +1864,7 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
1857
1864
  for (const placement of cluster.events) {
1858
1865
  eventStatuses.set(placement.eventCode, "failed");
1859
1866
  }
1867
+ await deps.discardChanges();
1860
1868
  continue;
1861
1869
  }
1862
1870
  let statuses = "applied";
@@ -1896,6 +1904,7 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
1896
1904
  for (const placement of cluster.events) {
1897
1905
  eventStatuses.set(placement.eventCode, finalStatus);
1898
1906
  }
1907
+ await deps.onClusterIntegrated(cluster, finalStatus);
1899
1908
  }
1900
1909
  if (!clustersComplete(clusters)) {
1901
1910
  throw new Error("cluster queue did not reach a terminal state");
@@ -1977,6 +1986,9 @@ var RunsClient = class {
1977
1986
  patchRun(runId, patch) {
1978
1987
  return this.request("PATCH", `/wizard/runs/${runId}`, { ...patch });
1979
1988
  }
1989
+ saveProgress(runId, progress) {
1990
+ return this.patchRun(runId, { integrationEvidence: progress });
1991
+ }
1980
1992
  writeItem(runId, kind, payload, idempotencyExtra = "") {
1981
1993
  const code = typeof payload.code === "string" ? payload.code : JSON.stringify(payload);
1982
1994
  return this.request("POST", `/wizard/runs/${runId}/items`, {
@@ -2029,7 +2041,13 @@ function normalizeRunBootstrap(payload) {
2029
2041
  target: stringValue(project.target),
2030
2042
  kind: stringValue(project.kind)
2031
2043
  },
2032
- run: { id: run3.id, status: stringValue(run3.status), phase: stringValue(run3.phase) },
2044
+ run: {
2045
+ id: run3.id,
2046
+ status: stringValue(run3.status),
2047
+ phase: stringValue(run3.phase),
2048
+ ...stringValue(run3.modelConversationId) ? { modelConversationId: stringValue(run3.modelConversationId) } : {},
2049
+ ...normalizeIntegrationProgress(run3.integrationEvidence) ? { integrationEvidence: normalizeIntegrationProgress(run3.integrationEvidence) } : {}
2050
+ },
2033
2051
  resumed: value.resumed === true,
2034
2052
  universeMode: stringValue(value.universeMode),
2035
2053
  ...record(value.onboardingContext) ? { onboardingContext: record(value.onboardingContext) } : {},
@@ -2041,6 +2059,31 @@ function normalizeRunBootstrap(payload) {
2041
2059
  ingestion: { apiKey: ingestion.apiKey, baseUrl: ingestion.baseUrl }
2042
2060
  };
2043
2061
  }
2062
+ function normalizeIntegrationProgress(payload) {
2063
+ const value = record(payload);
2064
+ if (!value || !Array.isArray(value.changedFiles) || !Array.isArray(value.events)) {
2065
+ return void 0;
2066
+ }
2067
+ const events = value.events.map((candidate) => {
2068
+ const event = record(candidate);
2069
+ if (!event || !stringValue(event.eventId) || !stringValue(event.eventCode)) {
2070
+ return void 0;
2071
+ }
2072
+ return {
2073
+ eventId: stringValue(event.eventId),
2074
+ eventCode: stringValue(event.eventCode),
2075
+ ...stringValue(event.file) ? { file: stringValue(event.file) } : {},
2076
+ status: stringValue(event.status)
2077
+ };
2078
+ }).filter((event) => event !== void 0);
2079
+ return {
2080
+ changedFiles: value.changedFiles.filter((file) => typeof file === "string"),
2081
+ identifyWired: value.identifyWired === true,
2082
+ verificationStatus: stringValue(value.verificationStatus) || "pending",
2083
+ ...stringValue(value.verificationCommand) ? { verificationCommand: stringValue(value.verificationCommand) } : {},
2084
+ events
2085
+ };
2086
+ }
2044
2087
  function record(value) {
2045
2088
  return typeof value === "object" && value !== null ? value : void 0;
2046
2089
  }
@@ -2049,7 +2092,7 @@ function stringValue(value) {
2049
2092
  }
2050
2093
  function isSnapshotEvent(value) {
2051
2094
  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);
2095
+ return !!event && typeof event.id === "string" && typeof event.code === "string" && typeof event.name === "string" && typeof event.reasoning === "string" && typeof event.projectId === "string" && (event.anchorFile === void 0 || typeof event.anchorFile === "string") && (event.anchorSymbol === void 0 || typeof event.anchorSymbol === "string") && !!record(event.payloadSchema);
2053
2096
  }
2054
2097
 
2055
2098
  // src/engine/selection.ts
@@ -2193,9 +2236,14 @@ function validateProposedEvent(input, seen, rejected) {
2193
2236
  if (rejected.has(code)) {
2194
2237
  return { ok: false, reason: `event ${code} was rejected by the user \u2014 do not re-propose it` };
2195
2238
  }
2196
- if (!input.name.trim() || !input.reasoning.trim() || !input.anchorFile.trim()) {
2239
+ const anchorFile = input.anchorFile.trim().replaceAll("\\", "/");
2240
+ const anchorParts = anchorFile.split("/");
2241
+ if (!input.name.trim() || !input.reasoning.trim() || !anchorFile) {
2197
2242
  return { ok: false, reason: "name, reasoning, and anchorFile are required" };
2198
2243
  }
2244
+ if (anchorFile.startsWith("/") || /^[a-z]:/i.test(anchorFile) || anchorParts.some((part) => part === "" || part === "." || part === "..")) {
2245
+ return { ok: false, reason: "anchorFile must be a safe repository-relative path" };
2246
+ }
2199
2247
  const schema = {};
2200
2248
  for (const [field, description] of Object.entries(input.payloadSchema ?? {})) {
2201
2249
  const normalizedField = normalizeEventCode(field);
@@ -2216,7 +2264,7 @@ function validateProposedEvent(input, seen, rejected) {
2216
2264
  code,
2217
2265
  name: input.name.trim(),
2218
2266
  reasoning: input.reasoning.trim(),
2219
- anchorFile: input.anchorFile.trim(),
2267
+ anchorFile,
2220
2268
  anchorSymbol: input.anchorSymbol?.trim() || void 0,
2221
2269
  payloadSchema: schema
2222
2270
  }
@@ -2239,7 +2287,7 @@ async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSess
2239
2287
  );
2240
2288
  return { report: outcome.kind === "completed" ? outcome.text : "", outcome };
2241
2289
  }
2242
- async function runSelection(provider, models, repoPath, bootstrap, surveyReport, rejectedCodes, sink) {
2290
+ async function runSelection(provider, models, repoPath, bootstrap, surveyReport, rejectedCodes, sink, existingEventCodes = bootstrap.snapshot.events.map((event) => event.code)) {
2243
2291
  const proposed = [];
2244
2292
  const seen = /* @__PURE__ */ new Set();
2245
2293
  const rejected = new Set(rejectedCodes);
@@ -2318,7 +2366,7 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
2318
2366
  userPrompt: selectionUserPrompt(
2319
2367
  surveyReport,
2320
2368
  renderOnboardingContext(bootstrap.onboardingContext),
2321
- bootstrap.snapshot.events.map((event) => event.code),
2369
+ existingEventCodes,
2322
2370
  rejectedCodes
2323
2371
  ),
2324
2372
  tools: [proposeTool, finishTool],
@@ -2340,12 +2388,27 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
2340
2388
  async function registerApprovedEvents(runs, runId, approved, sink) {
2341
2389
  const registered = [];
2342
2390
  for (const event of approved) {
2343
- const result = await runs.writeItem(runId, "event", {
2344
- code: event.code,
2345
- name: event.name,
2346
- reasoning: event.reasoning,
2347
- ...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
2348
- });
2391
+ let result;
2392
+ try {
2393
+ result = await runs.writeItem(runId, "event", {
2394
+ code: event.code,
2395
+ name: event.name,
2396
+ reasoning: event.reasoning,
2397
+ anchorFile: event.anchorFile,
2398
+ ...event.anchorSymbol ? { anchorSymbol: event.anchorSymbol } : {},
2399
+ ...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
2400
+ }, "placement-v2");
2401
+ } catch (error) {
2402
+ if (error instanceof RunsApiError && error.code === "wizard_project_conflict") {
2403
+ sink.emit({
2404
+ phase: "persisting",
2405
+ file: event.anchorFile,
2406
+ action: `skipped ${event.code} \u2014 already registered by another project`
2407
+ });
2408
+ continue;
2409
+ }
2410
+ throw error;
2411
+ }
2349
2412
  registered.push({ ...event, eventId: result.id || result.item?.id || "" });
2350
2413
  sink.emit({ phase: "persisting", file: event.anchorFile, action: `registered ${event.code}` });
2351
2414
  }
@@ -2361,99 +2424,11 @@ function placementsFor(registered) {
2361
2424
  }));
2362
2425
  }
2363
2426
 
2364
- // src/engine/state.ts
2365
- import { createHash as createHash3 } from "crypto";
2366
- import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
2367
- import { homedir, tmpdir } from "os";
2368
- import { join as join4 } from "path";
2369
-
2370
- // src/engine/types.ts
2371
- var ENGINE_PHASES = [
2372
- "authorizing",
2373
- "surveying",
2374
- "designing",
2375
- "persisting",
2376
- "binding",
2377
- "integrating",
2378
- "reviewing",
2379
- "verifying",
2380
- "reporting",
2381
- "completed"
2382
- ];
2383
-
2384
- // src/engine/state.ts
2385
- function stateDirectory() {
2386
- const override = process.env.WHISPERR_WIZARD_STATE_DIR?.trim();
2387
- const preferred = override || join4(homedir(), ".whisperr", "wizard-runs");
2388
- if (canCreateStateDirectory(preferred)) {
2389
- return preferred;
2390
- }
2391
- const user = typeof process.getuid === "function" ? process.getuid() : "user";
2392
- const fallback = join4(tmpdir(), `whisperr-${user}`, "wizard-runs");
2393
- if (canCreateStateDirectory(fallback)) {
2394
- return fallback;
2395
- }
2396
- throw new Error("Whisperr Wizard could not create a directory for run state");
2397
- }
2398
- function stateKey(apiBaseUrl, appId, repoFingerprint2) {
2399
- return createHash3("sha256").update(`${apiBaseUrl}
2400
- ${appId}
2401
- ${repoFingerprint2}`).digest("hex").slice(0, 24);
2402
- }
2403
- function statePath(apiBaseUrl, appId, repoFingerprint2) {
2404
- return join4(stateDirectory(), `${stateKey(apiBaseUrl, appId, repoFingerprint2)}.json`);
2405
- }
2406
- function saveRunState(state) {
2407
- const directory = stateDirectory();
2408
- const path = join4(directory, `${stateKey(state.apiBaseUrl, state.appId, state.repoFingerprint)}.json`);
2409
- const payload = JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2);
2410
- const temporary = `${path}.tmp`;
2411
- writeFileSync(temporary, payload, { mode: 384 });
2412
- renameSync(temporary, path);
2413
- }
2414
- function canCreateStateDirectory(directory) {
2415
- try {
2416
- mkdirSync(directory, { recursive: true, mode: 448 });
2417
- return true;
2418
- } catch {
2419
- return false;
2420
- }
2421
- }
2422
- function loadRunState(apiBaseUrl, appId, repoFingerprint2) {
2423
- const path = statePath(apiBaseUrl, appId, repoFingerprint2);
2424
- let raw;
2425
- try {
2426
- raw = readFileSync2(path, "utf8");
2427
- } catch {
2428
- return void 0;
2429
- }
2430
- let parsed;
2431
- try {
2432
- parsed = JSON.parse(raw);
2433
- } catch {
2434
- return void 0;
2435
- }
2436
- if (!isEngineRunState(parsed)) {
2437
- return void 0;
2438
- }
2439
- if (parsed.apiBaseUrl !== apiBaseUrl || parsed.appId !== appId || parsed.repoFingerprint !== repoFingerprint2) {
2440
- return void 0;
2441
- }
2442
- return parsed;
2443
- }
2444
- function isEngineRunState(value) {
2445
- if (typeof value !== "object" || value === null) {
2446
- return false;
2447
- }
2448
- const candidate = value;
2449
- 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;
2450
- }
2451
-
2452
2427
  // src/engine/worktree.ts
2453
2428
  import { spawn as spawn2 } from "child_process";
2454
- import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
2455
- import { tmpdir as tmpdir2 } from "os";
2456
- import { join as join5 } from "path";
2429
+ import { cp, lstat, mkdir, mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
2430
+ import { tmpdir } from "os";
2431
+ import { dirname as dirname2, join as join4, relative, resolve, sep } from "path";
2457
2432
  var WorktreeError = class extends Error {
2458
2433
  constructor(code, message) {
2459
2434
  super(message);
@@ -2463,7 +2438,7 @@ var WorktreeError = class extends Error {
2463
2438
  code;
2464
2439
  };
2465
2440
  function git(cwd, args) {
2466
- return new Promise((resolve4) => {
2441
+ return new Promise((resolve6) => {
2467
2442
  const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
2468
2443
  let stdout = "";
2469
2444
  let stderr = "";
@@ -2473,8 +2448,8 @@ function git(cwd, args) {
2473
2448
  child.stderr.on("data", (chunk) => {
2474
2449
  stderr += String(chunk);
2475
2450
  });
2476
- child.on("error", () => resolve4({ ok: false, stdout, stderr: "git is not available" }));
2477
- child.on("close", (exitCode) => resolve4({ ok: exitCode === 0, stdout, stderr }));
2451
+ child.on("error", () => resolve6({ ok: false, stdout, stderr: "git is not available" }));
2452
+ child.on("close", (exitCode) => resolve6({ ok: exitCode === 0, stdout, stderr }));
2478
2453
  });
2479
2454
  }
2480
2455
  async function must(cwd, args) {
@@ -2484,21 +2459,64 @@ async function must(cwd, args) {
2484
2459
  }
2485
2460
  return result.stdout;
2486
2461
  }
2487
- async function createWorktree(repoPath, force = false) {
2462
+ function safeRepoFile(repoPath, file) {
2463
+ const path = resolve(repoPath, file);
2464
+ if (path === repoPath || !path.startsWith(repoPath + sep) || relative(repoPath, path).startsWith("..")) {
2465
+ throw new WorktreeError("git_failed", `unsafe progress file path: ${file}`);
2466
+ }
2467
+ return path;
2468
+ }
2469
+ async function dirtyFiles(repoPath) {
2470
+ const tracked = await must(repoPath, ["diff", "--name-only", "-z", "HEAD"]);
2471
+ const untracked = await must(repoPath, ["ls-files", "--others", "--exclude-standard", "-z"]);
2472
+ return [...new Set((tracked + untracked).split("\0").filter(Boolean))];
2473
+ }
2474
+ async function seedWorktree(repoPath, worktreePath, files) {
2475
+ for (const file of files) {
2476
+ const source = safeRepoFile(repoPath, file);
2477
+ const destination = safeRepoFile(worktreePath, file);
2478
+ try {
2479
+ await lstat(source);
2480
+ await mkdir(dirname2(destination), { recursive: true });
2481
+ await cp(source, destination, { recursive: true, force: true });
2482
+ } catch {
2483
+ await rm(destination, { recursive: true, force: true });
2484
+ }
2485
+ }
2486
+ }
2487
+ async function createWorktree(repoPath, force = false, allowedDirtyFiles = []) {
2488
2488
  const inside = await git(repoPath, ["rev-parse", "--is-inside-work-tree"]);
2489
2489
  if (!inside.ok) {
2490
2490
  throw new WorktreeError("not_a_repo", "the target directory is not a git repository");
2491
2491
  }
2492
- const status = await must(repoPath, ["status", "--porcelain"]);
2493
- if (status.trim() !== "" && !force) {
2492
+ const dirty = await dirtyFiles(repoPath);
2493
+ const allowed = new Set(allowedDirtyFiles);
2494
+ const unexpected = dirty.filter((file) => !allowed.has(file));
2495
+ if (unexpected.length > 0 && !force) {
2494
2496
  throw new WorktreeError(
2495
2497
  "dirty_repo",
2496
- "the repository has uncommitted changes \u2014 commit or stash them, or rerun with --force"
2498
+ `the repository has unrelated uncommitted changes (${unexpected.slice(0, 3).join(", ")}) \u2014 commit or stash them, or rerun with --force`
2497
2499
  );
2498
2500
  }
2499
- const baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
2500
- const path = await mkdtemp(join5(tmpdir2(), "whisperr-worktree-"));
2501
+ let baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
2502
+ const path = await mkdtemp(join4(tmpdir(), "whisperr-worktree-"));
2501
2503
  await must(repoPath, ["worktree", "add", "--detach", "--force", path, "HEAD"]);
2504
+ const seedFiles = force ? dirty : dirty.filter((file) => allowed.has(file));
2505
+ if (seedFiles.length > 0) {
2506
+ await seedWorktree(repoPath, path, seedFiles);
2507
+ await must(path, ["add", "-A"]);
2508
+ await must(path, [
2509
+ "-c",
2510
+ "user.name=Whisperr Wizard",
2511
+ "-c",
2512
+ "user.email=wizard@whisperr.net",
2513
+ "commit",
2514
+ "--no-gpg-sign",
2515
+ "-m",
2516
+ "Whisperr resume checkpoint"
2517
+ ]);
2518
+ baseRef = (await must(path, ["rev-parse", "HEAD"])).trim();
2519
+ }
2502
2520
  return { path, baseRef, repoPath };
2503
2521
  }
2504
2522
  async function worktreePatch(handle) {
@@ -2509,7 +2527,7 @@ async function applyPatchToRepo(handle, patch) {
2509
2527
  if (patch.trim() === "") {
2510
2528
  return;
2511
2529
  }
2512
- await new Promise((resolve4, reject) => {
2530
+ await new Promise((resolve6, reject) => {
2513
2531
  const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
2514
2532
  cwd: handle.repoPath,
2515
2533
  stdio: ["pipe", "ignore", "pipe"]
@@ -2521,7 +2539,7 @@ async function applyPatchToRepo(handle, patch) {
2521
2539
  child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
2522
2540
  child.on("close", (exitCode) => {
2523
2541
  if (exitCode === 0) {
2524
- resolve4();
2542
+ resolve6();
2525
2543
  } else {
2526
2544
  reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
2527
2545
  }
@@ -2529,13 +2547,27 @@ async function applyPatchToRepo(handle, patch) {
2529
2547
  child.stdin.end(patch);
2530
2548
  });
2531
2549
  }
2532
- async function savePartialPatch(handle, destination) {
2533
- const patch = await worktreePatch(handle);
2550
+ async function commitWorktreeCheckpoint(handle) {
2551
+ await must(handle.path, ["add", "-A"]);
2552
+ const patch = await must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
2534
2553
  if (patch.trim() === "") {
2535
- return void 0;
2554
+ return;
2536
2555
  }
2537
- await writeFile2(destination, patch, { mode: 384 });
2538
- return destination;
2556
+ await must(handle.path, [
2557
+ "-c",
2558
+ "user.name=Whisperr Wizard",
2559
+ "-c",
2560
+ "user.email=wizard@whisperr.net",
2561
+ "commit",
2562
+ "--no-gpg-sign",
2563
+ "-m",
2564
+ "Whisperr integration checkpoint"
2565
+ ]);
2566
+ handle.baseRef = (await must(handle.path, ["rev-parse", "HEAD"])).trim();
2567
+ }
2568
+ async function discardWorktreeChanges(handle) {
2569
+ await must(handle.path, ["reset", "--hard", handle.baseRef]);
2570
+ await must(handle.path, ["clean", "-fd"]);
2539
2571
  }
2540
2572
  async function removeWorktree(handle) {
2541
2573
  await git(handle.repoPath, ["worktree", "remove", "--force", handle.path]);
@@ -2560,7 +2592,7 @@ async function runEngine(input) {
2560
2592
  throw error;
2561
2593
  }
2562
2594
  const runId = bootstrap.run.id;
2563
- input.provider.onRunReady?.(runId);
2595
+ input.provider.onRunReady?.(runId, bootstrap.run.modelConversationId);
2564
2596
  const patchPhase = async (phase, message) => {
2565
2597
  try {
2566
2598
  await runs.patchRun(runId, { phase, ...message ? { message } : {} });
@@ -2573,45 +2605,42 @@ async function runEngine(input) {
2573
2605
  } catch {
2574
2606
  }
2575
2607
  };
2576
- let state;
2577
- try {
2578
- state = loadRunState(input.config.apiBaseUrl, input.session.appId, input.repoFingerprint) ?? freshState(input, runId);
2579
- if (state.runId !== runId) {
2580
- state = freshState(input, runId);
2581
- }
2582
- } catch (error) {
2583
- const reason = error instanceof Error ? error.message : String(error);
2584
- await failRun(`state initialization failed: ${reason}`);
2585
- return { kind: "aborted", reason: `state initialization failed: ${reason}` };
2586
- }
2587
- const persist = () => saveRunState(state);
2588
2608
  let registered;
2589
- if (!state.designComplete) {
2590
- try {
2609
+ try {
2610
+ const projectEvents = bootstrap.snapshot.events.filter(
2611
+ (event) => event.projectId === bootstrap.project.id
2612
+ );
2613
+ const placed = registeredFromSnapshot(projectEvents);
2614
+ const missingPlacement = projectEvents.filter((event) => !event.anchorFile);
2615
+ if (placed.length === 0 || missingPlacement.length > 0) {
2616
+ input.sink.emit({
2617
+ phase: "surveying",
2618
+ action: projectEvents.length === 0 ? "no saved progress found; starting a fresh survey" : "saved events need placement recovery; verifying the repository"
2619
+ });
2591
2620
  await patchPhase("surveying");
2592
2621
  const survey = await runSurvey(
2593
2622
  input.provider,
2594
2623
  input.models,
2595
2624
  input.repoPath,
2596
2625
  bootstrap,
2597
- input.sink,
2598
- state.providerSessions.survey
2626
+ input.sink
2599
2627
  );
2600
2628
  if (survey.outcome.kind !== "completed") {
2601
2629
  await failRun(`survey ${survey.outcome.kind}`);
2602
2630
  return { kind: "aborted", reason: `survey ${survey.outcome.kind}` };
2603
2631
  }
2604
- state.providerSessions.survey = survey.outcome.sessionId;
2605
- persist();
2606
2632
  await patchPhase("designing");
2633
+ const recoverableCodes = new Set(missingPlacement.map((event) => event.code));
2634
+ const existingCodes = bootstrap.snapshot.events.filter((event) => !recoverableCodes.has(event.code)).map((event) => event.code);
2607
2635
  const selection = await runSelection(
2608
2636
  input.provider,
2609
2637
  input.models,
2610
2638
  input.repoPath,
2611
2639
  bootstrap,
2612
2640
  survey.report,
2613
- state.tombstonedCodes,
2614
- input.sink
2641
+ [],
2642
+ input.sink,
2643
+ existingCodes
2615
2644
  );
2616
2645
  if (!selection.finished) {
2617
2646
  const reason = selection.outcome === "completed" ? "selection did not call finish_selection" : `selection ${selection.outcome}`;
@@ -2631,44 +2660,114 @@ async function runEngine(input) {
2631
2660
  await failRun("no events accepted for instrumentation");
2632
2661
  return { kind: "aborted", reason: "no events accepted for instrumentation" };
2633
2662
  }
2634
- const approvedCodes = new Set(approved.map((event) => event.code));
2635
- for (const event of selection.proposed) {
2636
- if (!approvedCodes.has(event.code) && !state.tombstonedCodes.includes(event.code)) {
2637
- state.tombstonedCodes.push(event.code);
2638
- }
2639
- }
2640
- persist();
2641
2663
  await patchPhase("persisting", `registering ${approved.length} events`);
2642
- registered = await registerApprovedEvents(runs, runId, approved, input.sink);
2643
- state.designComplete = true;
2644
- state.clusters = deriveClusters(placementsFor(registered));
2645
- persist();
2646
- } catch (error) {
2647
- const reason = error instanceof Error ? error.message : String(error);
2648
- await failRun(`design failed: ${reason}`);
2649
- return { kind: "aborted", reason: `design failed: ${reason}` };
2664
+ const newlyRegistered = await registerApprovedEvents(runs, runId, approved, input.sink);
2665
+ registered = mergeRegistered(placed, newlyRegistered);
2666
+ const unresolved = projectEvents.filter((event) => !registered.some((candidate) => candidate.code === event.code)).map((event) => event.code);
2667
+ if (unresolved.length > 0) {
2668
+ throw new Error(`could not recover placements for: ${unresolved.join(", ")}`);
2669
+ }
2670
+ } else {
2671
+ registered = placed;
2672
+ input.sink.emit({
2673
+ phase: "integrating",
2674
+ action: `found ${registered.length} saved events; verifying current files`
2675
+ });
2650
2676
  }
2651
- } else {
2652
- registered = rebuildRegistered(state, bootstrap);
2653
- input.sink.emit({ phase: "designing", action: `resumed with ${registered.length} registered events` });
2677
+ } catch (error) {
2678
+ const reason = error instanceof Error ? error.message : String(error);
2679
+ await failRun(`design failed: ${reason}`);
2680
+ return { kind: "aborted", reason: `design failed: ${reason}` };
2654
2681
  }
2655
2682
  if (registered.length === 0) {
2656
2683
  await failRun("no events registered for this project");
2657
2684
  return { kind: "aborted", reason: "no events registered for this project" };
2658
2685
  }
2686
+ const prior = bootstrap.run.integrationEvidence;
2687
+ const changedFiles2 = new Set(prior?.changedFiles ?? []);
2688
+ const eventStatuses = await reconcileProgress(input.repoPath, registered, prior, input.sink);
2689
+ for (const event of registered) {
2690
+ const status = eventStatuses.get(event.code) ?? "planned";
2691
+ if (status !== "planned" && status !== "failed" && status !== "unsupported") {
2692
+ changedFiles2.add(event.anchorFile);
2693
+ }
2694
+ }
2695
+ let identifyWired = prior?.identifyWired === true && await setupLooksPresent(input.repoPath, changedFiles2);
2696
+ const progress = () => ({
2697
+ changedFiles: [...changedFiles2].sort(),
2698
+ identifyWired,
2699
+ verificationStatus: "pending",
2700
+ events: buildEvidence(registered, eventStatuses)
2701
+ });
2702
+ const saveProgress = async () => {
2703
+ await runs.saveProgress(runId, progress());
2704
+ };
2705
+ try {
2706
+ await saveProgress();
2707
+ } catch (error) {
2708
+ const reason = error instanceof Error ? error.message : String(error);
2709
+ await failRun(`progress initialization failed: ${reason}`);
2710
+ return { kind: "aborted", reason: `progress initialization failed: ${reason}` };
2711
+ }
2659
2712
  await patchPhase("binding");
2660
2713
  let worktree;
2661
2714
  try {
2662
- worktree = await createWorktree(input.repoPath, input.force ?? false);
2715
+ worktree = await createWorktree(
2716
+ input.repoPath,
2717
+ input.force ?? false,
2718
+ [...changedFiles2]
2719
+ );
2663
2720
  } catch (error) {
2664
2721
  const reason = error instanceof Error ? error.message : String(error);
2665
2722
  await failRun(reason);
2666
2723
  return { kind: "aborted", reason };
2667
2724
  }
2668
- state.worktreePath = worktree.path;
2669
- persist();
2725
+ const checkpoint = async () => {
2726
+ const patch = await worktreePatch(worktree);
2727
+ if (patch.trim() === "") {
2728
+ return [];
2729
+ }
2730
+ const files = patchFiles(patch);
2731
+ for (const file of files) {
2732
+ changedFiles2.add(file);
2733
+ }
2734
+ await saveProgress();
2735
+ await applyPatchToRepo(worktree, patch);
2736
+ await commitWorktreeCheckpoint(worktree);
2737
+ return files;
2738
+ };
2670
2739
  try {
2671
2740
  const bindings = await writeBindingsModule(worktree, input.target, registered);
2741
+ if (!identifyWired) {
2742
+ const setupCompleted = await runSdkSetupPass({
2743
+ provider: input.provider,
2744
+ models: input.models,
2745
+ worktree,
2746
+ target: input.target,
2747
+ playbookSystemPrompt: input.playbookSystemPrompt,
2748
+ registered,
2749
+ ingestion: bootstrap.ingestion,
2750
+ sink: input.sink,
2751
+ saveClusters: () => {
2752
+ },
2753
+ discardChanges: async () => discardWorktreeChanges(worktree),
2754
+ onClusterIntegrated: async () => {
2755
+ }
2756
+ }, bindings);
2757
+ if (!setupCompleted) {
2758
+ throw new Error("SDK setup did not complete");
2759
+ }
2760
+ await checkpoint();
2761
+ identifyWired = true;
2762
+ await saveProgress();
2763
+ } else {
2764
+ await checkpoint();
2765
+ }
2766
+ const pending = registered.filter((event) => {
2767
+ const status = eventStatuses.get(event.code) ?? "planned";
2768
+ return status === "planned" || status === "failed";
2769
+ });
2770
+ const clusters = deriveClusters(placementsFor(pending));
2672
2771
  const deps = {
2673
2772
  provider: input.provider,
2674
2773
  models: input.models,
@@ -2678,140 +2777,179 @@ async function runEngine(input) {
2678
2777
  registered,
2679
2778
  ingestion: bootstrap.ingestion,
2680
2779
  sink: input.sink,
2681
- saveClusters: (clusters) => {
2682
- state.clusters = clusters;
2683
- persist();
2780
+ saveClusters: () => {
2781
+ },
2782
+ discardChanges: async () => discardWorktreeChanges(worktree),
2783
+ onClusterIntegrated: async (cluster, status) => {
2784
+ const verified = /* @__PURE__ */ new Map();
2785
+ for (const placement of cluster.events) {
2786
+ const present = await fileContainsWrapper(
2787
+ worktree.path,
2788
+ placement.file,
2789
+ placement.eventCode
2790
+ );
2791
+ verified.set(placement.eventCode, present ? status : "failed");
2792
+ }
2793
+ await checkpoint();
2794
+ for (const placement of cluster.events) {
2795
+ eventStatuses.set(
2796
+ placement.eventCode,
2797
+ verified.get(placement.eventCode) ?? "failed"
2798
+ );
2799
+ await saveProgress();
2800
+ input.sink.emit({
2801
+ phase: "integrating",
2802
+ file: placement.file,
2803
+ action: `saved ${placement.eventCode} progress`
2804
+ });
2805
+ }
2684
2806
  }
2685
2807
  };
2686
- const identifyWired = await runSdkSetupPass(deps, bindings);
2687
2808
  await patchPhase("integrating");
2688
2809
  const integration = await runClusterIntegration(
2689
2810
  deps,
2690
- state.clusters,
2811
+ clusters,
2691
2812
  async () => worktreePatch(worktree)
2692
2813
  );
2693
- state.clusters = integration.clusters;
2694
- persist();
2814
+ for (const [code, status] of integration.eventStatuses) {
2815
+ if ((eventStatuses.get(code) ?? "planned") === "planned") {
2816
+ eventStatuses.set(code, status);
2817
+ }
2818
+ }
2819
+ await saveProgress();
2820
+ const unfinished = registered.filter((event) => {
2821
+ const status = eventStatuses.get(event.code) ?? "planned";
2822
+ return status === "planned" || status === "failed";
2823
+ });
2824
+ if (unfinished.length > 0) {
2825
+ throw new Error(
2826
+ `${unfinished.length} event${unfinished.length === 1 ? "" : "s"} still need integration`
2827
+ );
2828
+ }
2829
+ await patchPhase("verifying");
2695
2830
  const finalVerify = await finalizeIntegration(deps);
2696
- const patch = await worktreePatch(worktree);
2697
- const changedFiles2 = patchFiles(patch);
2698
2831
  await patchPhase("reporting");
2699
- const evidence = buildEvidence(registered, integration.eventStatuses, changedFiles2);
2700
- await completeRun(
2701
- runs,
2702
- runId,
2703
- evidence,
2704
- integration.eventStatuses,
2705
- changedFiles2,
2832
+ await runs.completeRun(runId, {
2833
+ changedFiles: [...changedFiles2].sort(),
2706
2834
  identifyWired,
2707
- finalVerify.status,
2708
- finalVerify.command
2709
- );
2710
- const summaryLines = summarize(registered, integration.eventStatuses);
2711
- const applied = patch.trim() !== "" && await input.callbacks.confirmApply(patch, summaryLines);
2712
- if (applied) {
2713
- await applyPatchToRepo(worktree, patch);
2714
- }
2715
- const partialPatchPath = applied ? void 0 : await savePartialPatch(worktree, join6(input.repoPath, ".whisperr-wizard.patch"));
2716
- await removeWorktree(worktree);
2717
- state.worktreePath = void 0;
2718
- persist();
2719
- const hasFailures = [...integration.eventStatuses.values()].some(
2720
- (status) => status === "failed"
2721
- );
2835
+ verificationStatus: finalVerify.status,
2836
+ ...finalVerify.command ? { verificationCommand: finalVerify.command } : {},
2837
+ events: buildEvidence(registered, eventStatuses)
2838
+ });
2839
+ await removeWorktree(worktree).catch(() => {
2840
+ });
2841
+ const summaryLines = summarize(registered, eventStatuses);
2842
+ const hasFailures = [...eventStatuses.values()].some((status) => status === "failed");
2722
2843
  return {
2723
2844
  kind: hasFailures ? "partial" : "completed",
2724
2845
  runId,
2725
2846
  registered,
2726
- eventStatuses: integration.eventStatuses,
2727
- reportEvents: buildReportEvents(registered, integration.eventStatuses),
2728
- changedFiles: changedFiles2,
2847
+ eventStatuses,
2848
+ reportEvents: buildReportEvents(registered, eventStatuses),
2849
+ changedFiles: [...changedFiles2].sort(),
2729
2850
  identifyWired,
2730
- applied,
2731
- partialPatchPath: partialPatchPath ?? void 0,
2851
+ applied: changedFiles2.size > 0,
2732
2852
  summary: summaryLines.join("\n")
2733
2853
  };
2734
2854
  } catch (error) {
2735
2855
  const reason = error instanceof Error ? error.message : String(error);
2736
2856
  await failRun(reason);
2737
- const partialPatchPath = await savePartialPatch(
2738
- worktree,
2739
- join6(input.repoPath, ".whisperr-wizard.patch")
2740
- ).catch(() => void 0);
2741
- persist();
2857
+ await removeWorktree(worktree).catch(() => {
2858
+ });
2742
2859
  return {
2743
2860
  kind: "partial",
2744
2861
  runId,
2745
2862
  registered,
2746
- eventStatuses: /* @__PURE__ */ new Map(),
2747
- reportEvents: buildReportEvents(registered, /* @__PURE__ */ new Map()),
2748
- changedFiles: [],
2749
- identifyWired: false,
2750
- applied: false,
2751
- partialPatchPath: partialPatchPath ?? void 0,
2752
- summary: `Integration stopped early: ${reason}. Registered events are saved; rerun to resume.`
2863
+ eventStatuses,
2864
+ reportEvents: buildReportEvents(registered, eventStatuses),
2865
+ changedFiles: [...changedFiles2].sort(),
2866
+ identifyWired,
2867
+ applied: changedFiles2.size > 0,
2868
+ summary: `Integration stopped early: ${reason}. Saved event progress will be verified on rerun.`
2753
2869
  };
2754
2870
  }
2755
2871
  }
2756
- function freshState(input, runId) {
2757
- return {
2758
- version: 1,
2759
- apiBaseUrl: input.config.apiBaseUrl,
2760
- appId: input.session.appId,
2761
- repoFingerprint: input.repoFingerprint,
2762
- runId,
2763
- phase: "authorizing",
2764
- providerSessions: {},
2765
- designComplete: false,
2766
- tombstonedCodes: [],
2767
- clusters: [],
2768
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2769
- };
2872
+ function registeredFromSnapshot(events) {
2873
+ return events.filter((event) => Boolean(event.anchorFile)).map((event) => ({
2874
+ code: event.code,
2875
+ name: event.name,
2876
+ reasoning: event.reasoning,
2877
+ eventId: event.id,
2878
+ anchorFile: event.anchorFile.replaceAll("\\", "/"),
2879
+ ...event.anchorSymbol ? { anchorSymbol: event.anchorSymbol } : {},
2880
+ payloadSchema: event.payloadSchema ?? {}
2881
+ }));
2770
2882
  }
2771
- function rebuildRegistered(state, bootstrap) {
2772
- const byCode = new Map(bootstrap.snapshot.events.map((event) => [event.code, event]));
2773
- const registered = [];
2774
- for (const cluster of state.clusters) {
2775
- for (const placement of cluster.events) {
2776
- const event = byCode.get(placement.eventCode);
2777
- if (!event) {
2778
- continue;
2779
- }
2780
- registered.push({
2781
- code: event.code,
2782
- name: event.name,
2783
- reasoning: event.reasoning,
2784
- eventId: event.id,
2785
- anchorFile: placement.file,
2786
- anchorSymbol: placement.symbol,
2787
- payloadSchema: event.payloadSchema ?? {}
2883
+ function mergeRegistered(existing, added) {
2884
+ const merged = new Map(existing.map((event) => [event.code, event]));
2885
+ for (const event of added) {
2886
+ merged.set(event.code, event);
2887
+ }
2888
+ return [...merged.values()].sort((a, b) => a.code.localeCompare(b.code));
2889
+ }
2890
+ async function reconcileProgress(repoPath, registered, prior, sink) {
2891
+ const priorById = new Map((prior?.events ?? []).map((event) => [event.eventId, event]));
2892
+ const statuses = /* @__PURE__ */ new Map();
2893
+ for (const event of registered) {
2894
+ const saved = priorById.get(event.eventId);
2895
+ if (saved?.status === "unsupported") {
2896
+ statuses.set(event.code, "unsupported");
2897
+ continue;
2898
+ }
2899
+ const present = await fileContainsWrapper(repoPath, event.anchorFile, event.code);
2900
+ const savedIntegrated = saved && saved.status !== "planned" && saved.status !== "failed";
2901
+ const status = present ? savedIntegrated ? saved.status : "applied" : "planned";
2902
+ statuses.set(event.code, status);
2903
+ if (saved && saved.status !== status) {
2904
+ sink.emit({
2905
+ phase: "verifying",
2906
+ file: event.anchorFile,
2907
+ action: present ? `recovered ${event.code} from the current code` : `${event.code} is missing and will be integrated again`
2788
2908
  });
2789
2909
  }
2790
2910
  }
2791
- return registered;
2911
+ return statuses;
2792
2912
  }
2793
- function buildEvidence(registered, statuses, changedFiles2) {
2794
- const changed = new Set(changedFiles2);
2795
- return registered.map((event) => {
2796
- const status = statuses.get(event.code) ?? "planned";
2797
- const needsFile = status !== "planned" && status !== "unsupported" && status !== "failed";
2798
- const file = changed.has(event.anchorFile) ? event.anchorFile : changedFiles2[0];
2799
- return {
2800
- eventId: event.eventId,
2801
- eventCode: event.code,
2802
- ...needsFile && file ? { file } : {},
2803
- status
2804
- };
2805
- });
2913
+ async function fileContainsWrapper(repoPath, file, eventCode) {
2914
+ try {
2915
+ const path = safeRepoPath(repoPath, file);
2916
+ const content = await readFile2(path, "utf8");
2917
+ return content.includes(eventWrapperName(eventCode));
2918
+ } catch {
2919
+ return false;
2920
+ }
2806
2921
  }
2807
- async function completeRun(runs, runId, evidence, _statuses, changedFiles2, identifyWired, verificationStatus, verificationCommand2) {
2808
- await runs.completeRun(runId, {
2809
- changedFiles: changedFiles2,
2810
- identifyWired,
2811
- verificationStatus,
2812
- ...verificationCommand2 ? { verificationCommand: verificationCommand2 } : {},
2813
- events: evidence
2814
- });
2922
+ async function setupLooksPresent(repoPath, changedFiles2) {
2923
+ let bindingsFound = false;
2924
+ let bindingCallFound = false;
2925
+ for (const file of changedFiles2) {
2926
+ try {
2927
+ const content = await readFile2(safeRepoPath(repoPath, file), "utf8");
2928
+ const generated = content.includes("Generated by @whisperr/wizard");
2929
+ bindingsFound ||= generated;
2930
+ if (!generated) {
2931
+ bindingCallFound ||= content.includes("bindWhisperr(") || content.includes("WhisperrEvents.bind(");
2932
+ }
2933
+ } catch {
2934
+ }
2935
+ }
2936
+ return bindingsFound && bindingCallFound;
2937
+ }
2938
+ function safeRepoPath(repoPath, file) {
2939
+ const root = resolve2(repoPath);
2940
+ const path = resolve2(root, file);
2941
+ if (path === root || !path.startsWith(root + sep2)) {
2942
+ throw new Error(`unsafe progress file path: ${file}`);
2943
+ }
2944
+ return path;
2945
+ }
2946
+ function buildEvidence(registered, statuses) {
2947
+ return registered.map((event) => ({
2948
+ eventId: event.eventId,
2949
+ eventCode: event.code,
2950
+ file: event.anchorFile,
2951
+ status: statuses.get(event.code) ?? "planned"
2952
+ }));
2815
2953
  }
2816
2954
  function buildReportEvents(registered, statuses) {
2817
2955
  return registered.map((event) => {
@@ -2852,9 +2990,9 @@ import {
2852
2990
 
2853
2991
  // src/core/git.ts
2854
2992
  import { spawn as spawn3 } from "child_process";
2855
- import { createHash as createHash4 } from "crypto";
2856
- import { readFile as readFile2 } from "fs/promises";
2857
- import { basename as basename2, join as join7 } from "path";
2993
+ import { createHash as createHash3 } from "crypto";
2994
+ import { readFile as readFile3 } from "fs/promises";
2995
+ import { basename as basename2, join as join5 } from "path";
2858
2996
  async function takeCheckpoint(repoPath) {
2859
2997
  const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
2860
2998
  if (!isRepo) return { isRepo: false };
@@ -2903,7 +3041,7 @@ async function revertToCheckpoint(repoPath, checkpoint) {
2903
3041
  async function repoFingerprint(repoPath) {
2904
3042
  const remote = await run(repoPath, ["remote", "get-url", "origin"]);
2905
3043
  const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename2(repoPath)}`;
2906
- return createHash4("sha256").update(seed).digest("hex").slice(0, 16);
3044
+ return createHash3("sha256").update(seed).digest("hex").slice(0, 16);
2907
3045
  }
2908
3046
  function normalizeRemote(url) {
2909
3047
  return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
@@ -2920,7 +3058,7 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
2920
3058
  const contents = [];
2921
3059
  for (const file of files) {
2922
3060
  try {
2923
- contents.push({ file, content: await readFile2(join7(repoPath, file), "utf8") });
3061
+ contents.push({ file, content: await readFile3(join5(repoPath, file), "utf8") });
2924
3062
  } catch {
2925
3063
  continue;
2926
3064
  }
@@ -2950,14 +3088,14 @@ function escapeRegExp(s) {
2950
3088
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2951
3089
  }
2952
3090
  function run(cwd, args) {
2953
- return new Promise((resolve4) => {
3091
+ return new Promise((resolve6) => {
2954
3092
  const child = spawn3("git", args, { cwd });
2955
3093
  let stdout = "";
2956
3094
  let stderr = "";
2957
3095
  child.stdout.on("data", (d) => stdout += d.toString());
2958
3096
  child.stderr.on("data", (d) => stderr += d.toString());
2959
- child.on("close", (code) => resolve4({ ok: code === 0, stdout, stderr }));
2960
- child.on("error", () => resolve4({ ok: false, stdout, stderr }));
3097
+ child.on("close", (code) => resolve6({ ok: code === 0, stdout, stderr }));
3098
+ child.on("error", () => resolve6({ ok: false, stdout, stderr }));
2961
3099
  });
2962
3100
  }
2963
3101
 
@@ -3196,7 +3334,7 @@ function coverageNote(coverage) {
3196
3334
  }
3197
3335
 
3198
3336
  // src/core/toolPolicy.ts
3199
- import { isAbsolute, relative, resolve } from "path";
3337
+ import { isAbsolute, relative as relative2, resolve as resolve3 } from "path";
3200
3338
  var SECRET_MATERIAL_DENIAL = "blocked: secret material must not be accessed";
3201
3339
  var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
3202
3340
  var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
@@ -3292,9 +3430,9 @@ function isSecretPathLike(value) {
3292
3430
  }
3293
3431
  function isPathInsideRepo(pathValue, repoPath) {
3294
3432
  if (pathValue.includes("..")) return false;
3295
- const repoRoot = resolve(repoPath);
3296
- const candidate = isAbsolute(pathValue) ? resolve(pathValue) : resolve(repoRoot, pathValue);
3297
- const rel = relative(repoRoot, candidate);
3433
+ const repoRoot = resolve3(repoPath);
3434
+ const candidate = isAbsolute(pathValue) ? resolve3(pathValue) : resolve3(repoRoot, pathValue);
3435
+ const rel = relative2(repoRoot, candidate);
3298
3436
  return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
3299
3437
  }
3300
3438
  function evaluateReadLike(secretValues, repoPathValues, context) {
@@ -3417,9 +3555,9 @@ function normalizePathLike(value) {
3417
3555
  return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
3418
3556
  }
3419
3557
  function repoRelativePath(pathValue, repoPath) {
3420
- const repoRoot = resolve(repoPath);
3421
- const candidate = isAbsolute(pathValue) ? resolve(pathValue) : resolve(repoRoot, pathValue);
3422
- return normalizePathLike(relative(repoRoot, candidate));
3558
+ const repoRoot = resolve3(repoPath);
3559
+ const candidate = isAbsolute(pathValue) ? resolve3(pathValue) : resolve3(repoRoot, pathValue);
3560
+ return normalizePathLike(relative2(repoRoot, candidate));
3423
3561
  }
3424
3562
  function isSensitiveWritePath(pathValue, repoPath) {
3425
3563
  const parts = repoRelativePath(pathValue, repoPath).split("/").map((part) => stripOuterQuotes(part.trim().toLowerCase())).filter(Boolean);
@@ -4473,11 +4611,11 @@ function createClaudeProvider(config, session) {
4473
4611
  }
4474
4612
 
4475
4613
  // src/engine/providers/openai.ts
4476
- import { readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
4477
- import { join as join8, resolve as resolve2, sep } from "path";
4614
+ import { readdirSync, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
4615
+ import { join as join6, resolve as resolve4, sep as sep3 } from "path";
4478
4616
  function jailedPath(cwd, candidate) {
4479
- const resolved = resolve2(cwd, candidate);
4480
- if (resolved !== cwd && !resolved.startsWith(cwd + sep)) {
4617
+ const resolved = resolve4(cwd, candidate);
4618
+ if (resolved !== cwd && !resolved.startsWith(cwd + sep3)) {
4481
4619
  throw new Error(`path ${candidate} escapes the repository`);
4482
4620
  }
4483
4621
  return resolved;
@@ -4507,7 +4645,7 @@ function fileTools(cwd, allowWrite) {
4507
4645
  },
4508
4646
  handler: async (input) => {
4509
4647
  const path = guardedPath(cwd, "Read", String(input.path ?? ""));
4510
- const content = readFileSync3(path, "utf8");
4648
+ const content = readFileSync2(path, "utf8");
4511
4649
  return { content: content.slice(0, 4e4) };
4512
4650
  }
4513
4651
  },
@@ -4523,7 +4661,7 @@ function fileTools(cwd, allowWrite) {
4523
4661
  handler: async (input) => {
4524
4662
  const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."));
4525
4663
  const entries = readdirSync(target9).filter((entry) => !isSecretPathLike(entry)).map((entry) => {
4526
- const kind = statSync(join8(target9, entry)).isDirectory() ? "dir" : "file";
4664
+ const kind = statSync(join6(target9, entry)).isDirectory() ? "dir" : "file";
4527
4665
  return `${kind}:${entry}`;
4528
4666
  });
4529
4667
  return { entries: entries.slice(0, 500) };
@@ -4543,7 +4681,7 @@ function fileTools(cwd, allowWrite) {
4543
4681
  },
4544
4682
  handler: async (input) => {
4545
4683
  const path = guardedPath(cwd, "Write", String(input.path ?? ""));
4546
- writeFileSync2(path, String(input.content ?? ""));
4684
+ writeFileSync(path, String(input.content ?? ""));
4547
4685
  return { ok: true };
4548
4686
  }
4549
4687
  },
@@ -4562,12 +4700,12 @@ function fileTools(cwd, allowWrite) {
4562
4700
  },
4563
4701
  handler: async (input) => {
4564
4702
  const path = guardedPath(cwd, "Edit", String(input.path ?? ""));
4565
- const content = readFileSync3(path, "utf8");
4703
+ const content = readFileSync2(path, "utf8");
4566
4704
  const oldText = String(input.oldText ?? "");
4567
4705
  if (!content.includes(oldText)) {
4568
4706
  return { ok: false, reason: "oldText not found" };
4569
4707
  }
4570
- writeFileSync2(path, content.replace(oldText, String(input.newText ?? "")));
4708
+ writeFileSync(path, content.replace(oldText, String(input.newText ?? "")));
4571
4709
  return { ok: true };
4572
4710
  }
4573
4711
  }
@@ -4631,8 +4769,9 @@ function createOpenAIProvider(config, session) {
4631
4769
  };
4632
4770
  return {
4633
4771
  name: "openai-gateway",
4634
- onRunReady(readyRunId) {
4772
+ onRunReady(readyRunId, resumeSessionId) {
4635
4773
  runId = readyRunId;
4774
+ conversationId = resumeSessionId;
4636
4775
  },
4637
4776
  async invoke(invocation, roleConfig, onProgress) {
4638
4777
  let costUsd = 0;
@@ -4714,9 +4853,9 @@ function createRoutedProvider(routes, fallback) {
4714
4853
  const providers = /* @__PURE__ */ new Set([fallback, ...Object.values(routes)]);
4715
4854
  return {
4716
4855
  name: "routed",
4717
- onRunReady(runId) {
4856
+ onRunReady(runId, resumeSessionId) {
4718
4857
  for (const provider of providers) {
4719
- provider.onRunReady?.(runId);
4858
+ provider.onRunReady?.(runId, resumeSessionId);
4720
4859
  }
4721
4860
  },
4722
4861
  invoke(invocation, config, onProgress) {
@@ -4768,16 +4907,7 @@ async function tryEngineFlow(options) {
4768
4907
  playbookSystemPrompt: playbook.systemPrompt,
4769
4908
  sink,
4770
4909
  callbacks: {
4771
- reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2),
4772
- confirmApply: async (patch, summaryLines) => {
4773
- p.note(summaryLines.join("\n"), "Integration result");
4774
- const lineCount = patch.split("\n").length;
4775
- const answer = await p.confirm({
4776
- message: `Apply these changes to your working tree? (${lineCount} patch lines)`,
4777
- initialValue: true
4778
- });
4779
- return answer === true;
4780
- }
4910
+ reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2)
4781
4911
  }
4782
4912
  });
4783
4913
  if (result.kind === "wrong_mode") {
@@ -4827,7 +4957,7 @@ async function reviewEventsInTerminal(proposed, theme2) {
4827
4957
  });
4828
4958
  p.note(lines.join("\n\n"), `Proposed events (${proposed.length})`);
4829
4959
  const selection = await p.multiselect({
4830
- message: "Register these events? Deselect any you don't want.",
4960
+ message: "Register and integrate these events? Deselect any you don't want.",
4831
4961
  options: proposed.map((event) => ({ value: event.code, label: event.code, hint: event.name })),
4832
4962
  initialValues: proposed.map((event) => event.code),
4833
4963
  required: false
@@ -5070,7 +5200,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5070
5200
  return Promise.resolve({ ran: false, ok: true, output: "" });
5071
5201
  }
5072
5202
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
5073
- return new Promise((resolve4) => {
5203
+ return new Promise((resolve6) => {
5074
5204
  const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
5075
5205
  let out = "";
5076
5206
  const append = (d) => {
@@ -5081,11 +5211,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5081
5211
  child.stderr?.on("data", append);
5082
5212
  const timer = setTimeout(() => {
5083
5213
  child.kill("SIGKILL");
5084
- resolve4({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
5214
+ resolve6({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
5085
5215
  }, timeoutMs);
5086
5216
  child.on("close", (code) => {
5087
5217
  clearTimeout(timer);
5088
- resolve4({
5218
+ resolve6({
5089
5219
  ran: true,
5090
5220
  ok: code === 0,
5091
5221
  command,
@@ -5097,7 +5227,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5097
5227
  });
5098
5228
  child.on("error", () => {
5099
5229
  clearTimeout(timer);
5100
- resolve4({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
5230
+ resolve6({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
5101
5231
  });
5102
5232
  });
5103
5233
  }
@@ -5107,21 +5237,21 @@ function tail2(s) {
5107
5237
  }
5108
5238
 
5109
5239
  // src/core/version.ts
5110
- import { readFileSync as readFileSync4 } from "fs";
5111
- import { dirname as dirname3, join as join9, parse } from "path";
5240
+ import { readFileSync as readFileSync3 } from "fs";
5241
+ import { dirname as dirname4, join as join7, parse } from "path";
5112
5242
  import { fileURLToPath } from "url";
5113
5243
  var PACKAGE_NAME = "@whisperr/wizard";
5114
5244
  function packageVersion() {
5115
- let dir = dirname3(fileURLToPath(import.meta.url));
5245
+ let dir = dirname4(fileURLToPath(import.meta.url));
5116
5246
  const { root } = parse(dir);
5117
5247
  while (true) {
5118
5248
  try {
5119
- const pkg = JSON.parse(readFileSync4(join9(dir, "package.json"), "utf8"));
5249
+ const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf8"));
5120
5250
  if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
5121
5251
  } catch {
5122
5252
  }
5123
5253
  if (dir === root) return "0.0.0";
5124
- dir = dirname3(dir);
5254
+ dir = dirname4(dir);
5125
5255
  }
5126
5256
  }
5127
5257
 
@@ -5254,7 +5384,7 @@ function banner() {
5254
5384
 
5255
5385
  // src/cli.ts
5256
5386
  async function run2(options) {
5257
- const repoPath = resolve3(options.path ?? process.cwd());
5387
+ const repoPath = resolve5(options.path ?? process.cwd());
5258
5388
  const config = resolveConfig(options);
5259
5389
  console.log(banner());
5260
5390
  p2.intro(theme.signal("Let's wire Whisperr into your app."));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whisperr/wizard",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Whisperr Wizard \u2014 one command to integrate the Whisperr SDK into your app. Authenticates with your onboarded account and uses an AI coding agent to wire up identify() and your business events automatically.",
5
5
  "repository": {
6
6
  "type": "git",