@whisperr/wizard 0.7.2 → 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 +589 -407
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,13 +5,13 @@ 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
 
12
12
  // src/core/config.ts
13
13
  var DEFAULT_API_BASE = "https://api.whisperr.net";
14
- var DEFAULT_MODEL = "claude-sonnet-5";
14
+ var DEFAULT_MODEL = "claude-opus-5";
15
15
  var DEFAULT_PLANNER_MODEL = "claude-opus-5";
16
16
  var DEFAULT_EFFORT = "high";
17
17
  var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
@@ -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
@@ -2063,11 +2106,14 @@ var SURVEY_SYSTEM_PROMPT = [
2063
2106
  "",
2064
2107
  "Report, in plain markdown:",
2065
2108
  "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.",
2109
+ "2. Feature areas: name, root directory, and every relevant file containing",
2110
+ " committed end-user actions or meaningful lifecycle transitions.",
2067
2111
  "3. The end-user identify() anchor: the login/signup success and session-restore call sites for the",
2068
2112
  " PAYING end user \u2014 never admin, staff, operator, or seller paths.",
2069
2113
  "4. Existing analytics calls, if any.",
2070
- "5. Committed-action call sites worth instrumenting: file, function/symbol, and what the user just did.",
2114
+ "5. Exhaustive inventory of integratable call sites: file, function/symbol,",
2115
+ " confirmed outcome or state transition, available non-PII payload fields,",
2116
+ " and whether similar paths exist elsewhere.",
2071
2117
  "Cite concrete file paths for every claim. Do not propose event names yet."
2072
2118
  ].join("\n");
2073
2119
  function surveyUserPrompt(repoPath, onboardingContext) {
@@ -2090,9 +2136,20 @@ var SELECTION_SYSTEM_PROMPT = [
2090
2136
  " streak_broken) \u2014 the server derives those later.",
2091
2137
  "- Emit after confirmed outcomes, not intent: a purchase event fires on confirmed success, not on",
2092
2138
  " 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.",
2139
+ "- Aim for comprehensive coverage of the product\u2019s observable end-user lifecycle.",
2140
+ " Inspect every meaningful feature area and propose every distinct, concretely",
2141
+ " integratable event that could contribute to activation, engagement, retention,",
2142
+ " monetization, or churn analysis.",
2143
+ "- Prefer complete coverage over a short curated list. Include distinct successful",
2144
+ " outcomes, failures, cancellations, retries, state transitions, and meaningful",
2145
+ " feature usage when each has its own concrete call site and analytical value.",
2146
+ "- Let the product\u2019s actual complexity determine the count. A feature-rich product",
2147
+ " may legitimately produce roughly 100\u2013200 events, while a smaller product may",
2148
+ " produce fewer. Do not stop after finding only the most obvious events, and do",
2149
+ " not invent events merely to reach a number.",
2150
+ "- Before finishing, systematically revisit every surveyed feature area and confirm",
2151
+ " that all meaningful committed actions and lifecycle transitions have either",
2152
+ " been proposed or deliberately excluded with a reason.",
2096
2153
  "- Codes are lowercase snake_case, named for what happened (checkout_completed, not do_checkout).",
2097
2154
  "- payloadSchema lists the fields worth capturing with a short description of each \u2014 the important",
2098
2155
  " info only. NEVER include personal identifiers: no email, phone, names, addresses, birth dates,",
@@ -2179,9 +2236,14 @@ function validateProposedEvent(input, seen, rejected) {
2179
2236
  if (rejected.has(code)) {
2180
2237
  return { ok: false, reason: `event ${code} was rejected by the user \u2014 do not re-propose it` };
2181
2238
  }
2182
- 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) {
2183
2242
  return { ok: false, reason: "name, reasoning, and anchorFile are required" };
2184
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
+ }
2185
2247
  const schema = {};
2186
2248
  for (const [field, description] of Object.entries(input.payloadSchema ?? {})) {
2187
2249
  const normalizedField = normalizeEventCode(field);
@@ -2202,7 +2264,7 @@ function validateProposedEvent(input, seen, rejected) {
2202
2264
  code,
2203
2265
  name: input.name.trim(),
2204
2266
  reasoning: input.reasoning.trim(),
2205
- anchorFile: input.anchorFile.trim(),
2267
+ anchorFile,
2206
2268
  anchorSymbol: input.anchorSymbol?.trim() || void 0,
2207
2269
  payloadSchema: schema
2208
2270
  }
@@ -2225,7 +2287,7 @@ async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSess
2225
2287
  );
2226
2288
  return { report: outcome.kind === "completed" ? outcome.text : "", outcome };
2227
2289
  }
2228
- 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)) {
2229
2291
  const proposed = [];
2230
2292
  const seen = /* @__PURE__ */ new Set();
2231
2293
  const rejected = new Set(rejectedCodes);
@@ -2304,7 +2366,7 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
2304
2366
  userPrompt: selectionUserPrompt(
2305
2367
  surveyReport,
2306
2368
  renderOnboardingContext(bootstrap.onboardingContext),
2307
- bootstrap.snapshot.events.map((event) => event.code),
2369
+ existingEventCodes,
2308
2370
  rejectedCodes
2309
2371
  ),
2310
2372
  tools: [proposeTool, finishTool],
@@ -2326,12 +2388,27 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
2326
2388
  async function registerApprovedEvents(runs, runId, approved, sink) {
2327
2389
  const registered = [];
2328
2390
  for (const event of approved) {
2329
- const result = await runs.writeItem(runId, "event", {
2330
- code: event.code,
2331
- name: event.name,
2332
- reasoning: event.reasoning,
2333
- ...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
2334
- });
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
+ }
2335
2412
  registered.push({ ...event, eventId: result.id || result.item?.id || "" });
2336
2413
  sink.emit({ phase: "persisting", file: event.anchorFile, action: `registered ${event.code}` });
2337
2414
  }
@@ -2347,99 +2424,11 @@ function placementsFor(registered) {
2347
2424
  }));
2348
2425
  }
2349
2426
 
2350
- // src/engine/state.ts
2351
- import { createHash as createHash3 } from "crypto";
2352
- import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
2353
- import { homedir, tmpdir } from "os";
2354
- import { join as join4 } from "path";
2355
-
2356
- // src/engine/types.ts
2357
- var ENGINE_PHASES = [
2358
- "authorizing",
2359
- "surveying",
2360
- "designing",
2361
- "persisting",
2362
- "binding",
2363
- "integrating",
2364
- "reviewing",
2365
- "verifying",
2366
- "reporting",
2367
- "completed"
2368
- ];
2369
-
2370
- // src/engine/state.ts
2371
- function stateDirectory() {
2372
- const override = process.env.WHISPERR_WIZARD_STATE_DIR?.trim();
2373
- const preferred = override || join4(homedir(), ".whisperr", "wizard-runs");
2374
- if (canCreateStateDirectory(preferred)) {
2375
- return preferred;
2376
- }
2377
- const user = typeof process.getuid === "function" ? process.getuid() : "user";
2378
- const fallback = join4(tmpdir(), `whisperr-${user}`, "wizard-runs");
2379
- if (canCreateStateDirectory(fallback)) {
2380
- return fallback;
2381
- }
2382
- throw new Error("Whisperr Wizard could not create a directory for run state");
2383
- }
2384
- function stateKey(apiBaseUrl, appId, repoFingerprint2) {
2385
- return createHash3("sha256").update(`${apiBaseUrl}
2386
- ${appId}
2387
- ${repoFingerprint2}`).digest("hex").slice(0, 24);
2388
- }
2389
- function statePath(apiBaseUrl, appId, repoFingerprint2) {
2390
- return join4(stateDirectory(), `${stateKey(apiBaseUrl, appId, repoFingerprint2)}.json`);
2391
- }
2392
- function saveRunState(state) {
2393
- const directory = stateDirectory();
2394
- const path = join4(directory, `${stateKey(state.apiBaseUrl, state.appId, state.repoFingerprint)}.json`);
2395
- const payload = JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2);
2396
- const temporary = `${path}.tmp`;
2397
- writeFileSync(temporary, payload, { mode: 384 });
2398
- renameSync(temporary, path);
2399
- }
2400
- function canCreateStateDirectory(directory) {
2401
- try {
2402
- mkdirSync(directory, { recursive: true, mode: 448 });
2403
- return true;
2404
- } catch {
2405
- return false;
2406
- }
2407
- }
2408
- function loadRunState(apiBaseUrl, appId, repoFingerprint2) {
2409
- const path = statePath(apiBaseUrl, appId, repoFingerprint2);
2410
- let raw;
2411
- try {
2412
- raw = readFileSync2(path, "utf8");
2413
- } catch {
2414
- return void 0;
2415
- }
2416
- let parsed;
2417
- try {
2418
- parsed = JSON.parse(raw);
2419
- } catch {
2420
- return void 0;
2421
- }
2422
- if (!isEngineRunState(parsed)) {
2423
- return void 0;
2424
- }
2425
- if (parsed.apiBaseUrl !== apiBaseUrl || parsed.appId !== appId || parsed.repoFingerprint !== repoFingerprint2) {
2426
- return void 0;
2427
- }
2428
- return parsed;
2429
- }
2430
- function isEngineRunState(value) {
2431
- if (typeof value !== "object" || value === null) {
2432
- return false;
2433
- }
2434
- const candidate = value;
2435
- 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;
2436
- }
2437
-
2438
2427
  // src/engine/worktree.ts
2439
2428
  import { spawn as spawn2 } from "child_process";
2440
- import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
2441
- import { tmpdir as tmpdir2 } from "os";
2442
- 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";
2443
2432
  var WorktreeError = class extends Error {
2444
2433
  constructor(code, message) {
2445
2434
  super(message);
@@ -2449,7 +2438,7 @@ var WorktreeError = class extends Error {
2449
2438
  code;
2450
2439
  };
2451
2440
  function git(cwd, args) {
2452
- return new Promise((resolve4) => {
2441
+ return new Promise((resolve6) => {
2453
2442
  const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
2454
2443
  let stdout = "";
2455
2444
  let stderr = "";
@@ -2459,8 +2448,8 @@ function git(cwd, args) {
2459
2448
  child.stderr.on("data", (chunk) => {
2460
2449
  stderr += String(chunk);
2461
2450
  });
2462
- child.on("error", () => resolve4({ ok: false, stdout, stderr: "git is not available" }));
2463
- 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 }));
2464
2453
  });
2465
2454
  }
2466
2455
  async function must(cwd, args) {
@@ -2470,21 +2459,64 @@ async function must(cwd, args) {
2470
2459
  }
2471
2460
  return result.stdout;
2472
2461
  }
2473
- 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 = []) {
2474
2488
  const inside = await git(repoPath, ["rev-parse", "--is-inside-work-tree"]);
2475
2489
  if (!inside.ok) {
2476
2490
  throw new WorktreeError("not_a_repo", "the target directory is not a git repository");
2477
2491
  }
2478
- const status = await must(repoPath, ["status", "--porcelain"]);
2479
- 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) {
2480
2496
  throw new WorktreeError(
2481
2497
  "dirty_repo",
2482
- "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`
2483
2499
  );
2484
2500
  }
2485
- const baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
2486
- 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-"));
2487
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
+ }
2488
2520
  return { path, baseRef, repoPath };
2489
2521
  }
2490
2522
  async function worktreePatch(handle) {
@@ -2495,7 +2527,7 @@ async function applyPatchToRepo(handle, patch) {
2495
2527
  if (patch.trim() === "") {
2496
2528
  return;
2497
2529
  }
2498
- await new Promise((resolve4, reject) => {
2530
+ await new Promise((resolve6, reject) => {
2499
2531
  const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
2500
2532
  cwd: handle.repoPath,
2501
2533
  stdio: ["pipe", "ignore", "pipe"]
@@ -2507,7 +2539,7 @@ async function applyPatchToRepo(handle, patch) {
2507
2539
  child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
2508
2540
  child.on("close", (exitCode) => {
2509
2541
  if (exitCode === 0) {
2510
- resolve4();
2542
+ resolve6();
2511
2543
  } else {
2512
2544
  reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
2513
2545
  }
@@ -2515,13 +2547,27 @@ async function applyPatchToRepo(handle, patch) {
2515
2547
  child.stdin.end(patch);
2516
2548
  });
2517
2549
  }
2518
- async function savePartialPatch(handle, destination) {
2519
- 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]);
2520
2553
  if (patch.trim() === "") {
2521
- return void 0;
2554
+ return;
2522
2555
  }
2523
- await writeFile2(destination, patch, { mode: 384 });
2524
- 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"]);
2525
2571
  }
2526
2572
  async function removeWorktree(handle) {
2527
2573
  await git(handle.repoPath, ["worktree", "remove", "--force", handle.path]);
@@ -2546,7 +2592,7 @@ async function runEngine(input) {
2546
2592
  throw error;
2547
2593
  }
2548
2594
  const runId = bootstrap.run.id;
2549
- input.provider.onRunReady?.(runId);
2595
+ input.provider.onRunReady?.(runId, bootstrap.run.modelConversationId);
2550
2596
  const patchPhase = async (phase, message) => {
2551
2597
  try {
2552
2598
  await runs.patchRun(runId, { phase, ...message ? { message } : {} });
@@ -2559,45 +2605,42 @@ async function runEngine(input) {
2559
2605
  } catch {
2560
2606
  }
2561
2607
  };
2562
- let state;
2563
- try {
2564
- state = loadRunState(input.config.apiBaseUrl, input.session.appId, input.repoFingerprint) ?? freshState(input, runId);
2565
- if (state.runId !== runId) {
2566
- state = freshState(input, runId);
2567
- }
2568
- } catch (error) {
2569
- const reason = error instanceof Error ? error.message : String(error);
2570
- await failRun(`state initialization failed: ${reason}`);
2571
- return { kind: "aborted", reason: `state initialization failed: ${reason}` };
2572
- }
2573
- const persist = () => saveRunState(state);
2574
2608
  let registered;
2575
- if (!state.designComplete) {
2576
- 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
+ });
2577
2620
  await patchPhase("surveying");
2578
2621
  const survey = await runSurvey(
2579
2622
  input.provider,
2580
2623
  input.models,
2581
2624
  input.repoPath,
2582
2625
  bootstrap,
2583
- input.sink,
2584
- state.providerSessions.survey
2626
+ input.sink
2585
2627
  );
2586
2628
  if (survey.outcome.kind !== "completed") {
2587
2629
  await failRun(`survey ${survey.outcome.kind}`);
2588
2630
  return { kind: "aborted", reason: `survey ${survey.outcome.kind}` };
2589
2631
  }
2590
- state.providerSessions.survey = survey.outcome.sessionId;
2591
- persist();
2592
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);
2593
2635
  const selection = await runSelection(
2594
2636
  input.provider,
2595
2637
  input.models,
2596
2638
  input.repoPath,
2597
2639
  bootstrap,
2598
2640
  survey.report,
2599
- state.tombstonedCodes,
2600
- input.sink
2641
+ [],
2642
+ input.sink,
2643
+ existingCodes
2601
2644
  );
2602
2645
  if (!selection.finished) {
2603
2646
  const reason = selection.outcome === "completed" ? "selection did not call finish_selection" : `selection ${selection.outcome}`;
@@ -2617,44 +2660,114 @@ async function runEngine(input) {
2617
2660
  await failRun("no events accepted for instrumentation");
2618
2661
  return { kind: "aborted", reason: "no events accepted for instrumentation" };
2619
2662
  }
2620
- const approvedCodes = new Set(approved.map((event) => event.code));
2621
- for (const event of selection.proposed) {
2622
- if (!approvedCodes.has(event.code) && !state.tombstonedCodes.includes(event.code)) {
2623
- state.tombstonedCodes.push(event.code);
2624
- }
2625
- }
2626
- persist();
2627
2663
  await patchPhase("persisting", `registering ${approved.length} events`);
2628
- registered = await registerApprovedEvents(runs, runId, approved, input.sink);
2629
- state.designComplete = true;
2630
- state.clusters = deriveClusters(placementsFor(registered));
2631
- persist();
2632
- } catch (error) {
2633
- const reason = error instanceof Error ? error.message : String(error);
2634
- await failRun(`design failed: ${reason}`);
2635
- 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
+ });
2636
2676
  }
2637
- } else {
2638
- registered = rebuildRegistered(state, bootstrap);
2639
- 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}` };
2640
2681
  }
2641
2682
  if (registered.length === 0) {
2642
2683
  await failRun("no events registered for this project");
2643
2684
  return { kind: "aborted", reason: "no events registered for this project" };
2644
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
+ }
2645
2712
  await patchPhase("binding");
2646
2713
  let worktree;
2647
2714
  try {
2648
- worktree = await createWorktree(input.repoPath, input.force ?? false);
2715
+ worktree = await createWorktree(
2716
+ input.repoPath,
2717
+ input.force ?? false,
2718
+ [...changedFiles2]
2719
+ );
2649
2720
  } catch (error) {
2650
2721
  const reason = error instanceof Error ? error.message : String(error);
2651
2722
  await failRun(reason);
2652
2723
  return { kind: "aborted", reason };
2653
2724
  }
2654
- state.worktreePath = worktree.path;
2655
- 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
+ };
2656
2739
  try {
2657
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));
2658
2771
  const deps = {
2659
2772
  provider: input.provider,
2660
2773
  models: input.models,
@@ -2664,140 +2777,179 @@ async function runEngine(input) {
2664
2777
  registered,
2665
2778
  ingestion: bootstrap.ingestion,
2666
2779
  sink: input.sink,
2667
- saveClusters: (clusters) => {
2668
- state.clusters = clusters;
2669
- 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
+ }
2670
2806
  }
2671
2807
  };
2672
- const identifyWired = await runSdkSetupPass(deps, bindings);
2673
2808
  await patchPhase("integrating");
2674
2809
  const integration = await runClusterIntegration(
2675
2810
  deps,
2676
- state.clusters,
2811
+ clusters,
2677
2812
  async () => worktreePatch(worktree)
2678
2813
  );
2679
- state.clusters = integration.clusters;
2680
- 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");
2681
2830
  const finalVerify = await finalizeIntegration(deps);
2682
- const patch = await worktreePatch(worktree);
2683
- const changedFiles2 = patchFiles(patch);
2684
2831
  await patchPhase("reporting");
2685
- const evidence = buildEvidence(registered, integration.eventStatuses, changedFiles2);
2686
- await completeRun(
2687
- runs,
2688
- runId,
2689
- evidence,
2690
- integration.eventStatuses,
2691
- changedFiles2,
2832
+ await runs.completeRun(runId, {
2833
+ changedFiles: [...changedFiles2].sort(),
2692
2834
  identifyWired,
2693
- finalVerify.status,
2694
- finalVerify.command
2695
- );
2696
- const summaryLines = summarize(registered, integration.eventStatuses);
2697
- const applied = patch.trim() !== "" && await input.callbacks.confirmApply(patch, summaryLines);
2698
- if (applied) {
2699
- await applyPatchToRepo(worktree, patch);
2700
- }
2701
- const partialPatchPath = applied ? void 0 : await savePartialPatch(worktree, join6(input.repoPath, ".whisperr-wizard.patch"));
2702
- await removeWorktree(worktree);
2703
- state.worktreePath = void 0;
2704
- persist();
2705
- const hasFailures = [...integration.eventStatuses.values()].some(
2706
- (status) => status === "failed"
2707
- );
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");
2708
2843
  return {
2709
2844
  kind: hasFailures ? "partial" : "completed",
2710
2845
  runId,
2711
2846
  registered,
2712
- eventStatuses: integration.eventStatuses,
2713
- reportEvents: buildReportEvents(registered, integration.eventStatuses),
2714
- changedFiles: changedFiles2,
2847
+ eventStatuses,
2848
+ reportEvents: buildReportEvents(registered, eventStatuses),
2849
+ changedFiles: [...changedFiles2].sort(),
2715
2850
  identifyWired,
2716
- applied,
2717
- partialPatchPath: partialPatchPath ?? void 0,
2851
+ applied: changedFiles2.size > 0,
2718
2852
  summary: summaryLines.join("\n")
2719
2853
  };
2720
2854
  } catch (error) {
2721
2855
  const reason = error instanceof Error ? error.message : String(error);
2722
2856
  await failRun(reason);
2723
- const partialPatchPath = await savePartialPatch(
2724
- worktree,
2725
- join6(input.repoPath, ".whisperr-wizard.patch")
2726
- ).catch(() => void 0);
2727
- persist();
2857
+ await removeWorktree(worktree).catch(() => {
2858
+ });
2728
2859
  return {
2729
2860
  kind: "partial",
2730
2861
  runId,
2731
2862
  registered,
2732
- eventStatuses: /* @__PURE__ */ new Map(),
2733
- reportEvents: buildReportEvents(registered, /* @__PURE__ */ new Map()),
2734
- changedFiles: [],
2735
- identifyWired: false,
2736
- applied: false,
2737
- partialPatchPath: partialPatchPath ?? void 0,
2738
- 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.`
2739
2869
  };
2740
2870
  }
2741
2871
  }
2742
- function freshState(input, runId) {
2743
- return {
2744
- version: 1,
2745
- apiBaseUrl: input.config.apiBaseUrl,
2746
- appId: input.session.appId,
2747
- repoFingerprint: input.repoFingerprint,
2748
- runId,
2749
- phase: "authorizing",
2750
- providerSessions: {},
2751
- designComplete: false,
2752
- tombstonedCodes: [],
2753
- clusters: [],
2754
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2755
- };
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
+ }));
2756
2882
  }
2757
- function rebuildRegistered(state, bootstrap) {
2758
- const byCode = new Map(bootstrap.snapshot.events.map((event) => [event.code, event]));
2759
- const registered = [];
2760
- for (const cluster of state.clusters) {
2761
- for (const placement of cluster.events) {
2762
- const event = byCode.get(placement.eventCode);
2763
- if (!event) {
2764
- continue;
2765
- }
2766
- registered.push({
2767
- code: event.code,
2768
- name: event.name,
2769
- reasoning: event.reasoning,
2770
- eventId: event.id,
2771
- anchorFile: placement.file,
2772
- anchorSymbol: placement.symbol,
2773
- 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`
2774
2908
  });
2775
2909
  }
2776
2910
  }
2777
- return registered;
2911
+ return statuses;
2778
2912
  }
2779
- function buildEvidence(registered, statuses, changedFiles2) {
2780
- const changed = new Set(changedFiles2);
2781
- return registered.map((event) => {
2782
- const status = statuses.get(event.code) ?? "planned";
2783
- const needsFile = status !== "planned" && status !== "unsupported" && status !== "failed";
2784
- const file = changed.has(event.anchorFile) ? event.anchorFile : changedFiles2[0];
2785
- return {
2786
- eventId: event.eventId,
2787
- eventCode: event.code,
2788
- ...needsFile && file ? { file } : {},
2789
- status
2790
- };
2791
- });
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
+ }
2792
2921
  }
2793
- async function completeRun(runs, runId, evidence, _statuses, changedFiles2, identifyWired, verificationStatus, verificationCommand2) {
2794
- await runs.completeRun(runId, {
2795
- changedFiles: changedFiles2,
2796
- identifyWired,
2797
- verificationStatus,
2798
- ...verificationCommand2 ? { verificationCommand: verificationCommand2 } : {},
2799
- events: evidence
2800
- });
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
+ }));
2801
2953
  }
2802
2954
  function buildReportEvents(registered, statuses) {
2803
2955
  return registered.map((event) => {
@@ -2838,9 +2990,9 @@ import {
2838
2990
 
2839
2991
  // src/core/git.ts
2840
2992
  import { spawn as spawn3 } from "child_process";
2841
- import { createHash as createHash4 } from "crypto";
2842
- import { readFile as readFile2 } from "fs/promises";
2843
- 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";
2844
2996
  async function takeCheckpoint(repoPath) {
2845
2997
  const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
2846
2998
  if (!isRepo) return { isRepo: false };
@@ -2889,7 +3041,7 @@ async function revertToCheckpoint(repoPath, checkpoint) {
2889
3041
  async function repoFingerprint(repoPath) {
2890
3042
  const remote = await run(repoPath, ["remote", "get-url", "origin"]);
2891
3043
  const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename2(repoPath)}`;
2892
- return createHash4("sha256").update(seed).digest("hex").slice(0, 16);
3044
+ return createHash3("sha256").update(seed).digest("hex").slice(0, 16);
2893
3045
  }
2894
3046
  function normalizeRemote(url) {
2895
3047
  return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
@@ -2906,7 +3058,7 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
2906
3058
  const contents = [];
2907
3059
  for (const file of files) {
2908
3060
  try {
2909
- contents.push({ file, content: await readFile2(join7(repoPath, file), "utf8") });
3061
+ contents.push({ file, content: await readFile3(join5(repoPath, file), "utf8") });
2910
3062
  } catch {
2911
3063
  continue;
2912
3064
  }
@@ -2936,14 +3088,14 @@ function escapeRegExp(s) {
2936
3088
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2937
3089
  }
2938
3090
  function run(cwd, args) {
2939
- return new Promise((resolve4) => {
3091
+ return new Promise((resolve6) => {
2940
3092
  const child = spawn3("git", args, { cwd });
2941
3093
  let stdout = "";
2942
3094
  let stderr = "";
2943
3095
  child.stdout.on("data", (d) => stdout += d.toString());
2944
3096
  child.stderr.on("data", (d) => stderr += d.toString());
2945
- child.on("close", (code) => resolve4({ ok: code === 0, stdout, stderr }));
2946
- 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 }));
2947
3099
  });
2948
3100
  }
2949
3101
 
@@ -3182,8 +3334,8 @@ function coverageNote(coverage) {
3182
3334
  }
3183
3335
 
3184
3336
  // src/core/toolPolicy.ts
3185
- import { isAbsolute, relative, resolve } from "path";
3186
- var SECRET_MATERIAL_DENIAL = "blocked: secret material - reference the variable name in .env.example instead of reading the real file";
3337
+ import { isAbsolute, relative as relative2, resolve as resolve3 } from "path";
3338
+ var SECRET_MATERIAL_DENIAL = "blocked: secret material must not be accessed";
3187
3339
  var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
3188
3340
  var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
3189
3341
  var BASH_ALLOWLIST_DENIAL = "blocked: command is outside the Bash allowlist - use package install/add commands, mkdir, or git status/diff/log only";
@@ -3194,9 +3346,24 @@ function engineMcpToolName(toolName) {
3194
3346
  function trustedEngineMcpTools(tools) {
3195
3347
  return new Set(tools.map((tool2) => engineMcpToolName(tool2.name)));
3196
3348
  }
3197
- var ENV_EXAMPLES = /* @__PURE__ */ new Set([".env.example", ".env.sample", ".env.template"]);
3198
- var SECRET_DIRECTORIES = /* @__PURE__ */ new Set([".aws", ".ssh", ".gnupg"]);
3199
- var SECRET_EXACT_FILES = /* @__PURE__ */ new Set([".netrc", ".npmrc", ".pypirc"]);
3349
+ var SECRET_DIRECTORIES = /* @__PURE__ */ new Set([
3350
+ ".aws",
3351
+ ".ssh",
3352
+ ".gnupg",
3353
+ ".azure",
3354
+ ".docker",
3355
+ ".kube",
3356
+ ".secrets",
3357
+ "secrets"
3358
+ ]);
3359
+ var SECRET_EXACT_FILES = /* @__PURE__ */ new Set([
3360
+ ".netrc",
3361
+ ".npmrc",
3362
+ ".pypirc",
3363
+ "auth.json",
3364
+ "google-services.json",
3365
+ "googleservice-info.plist"
3366
+ ]);
3200
3367
  var SENSITIVE_WRITE_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".circleci", ".buildkite"]);
3201
3368
  var SENSITIVE_WRITE_EXACT_FILES = /* @__PURE__ */ new Set([
3202
3369
  ".gitlab-ci.yml",
@@ -3212,6 +3379,7 @@ var SECRET_SUFFIXES = [
3212
3379
  ".pfx",
3213
3380
  ".jks",
3214
3381
  ".keystore",
3382
+ ".kdbx",
3215
3383
  ".tfvars"
3216
3384
  ];
3217
3385
  var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
@@ -3245,7 +3413,6 @@ function isSecretPathLike(value) {
3245
3413
  for (const rawPart of parts) {
3246
3414
  const part = stripOuterQuotes(rawPart.trim().toLowerCase());
3247
3415
  if (!part || part === "." || part === "**") continue;
3248
- if (ENV_EXAMPLES.has(part)) continue;
3249
3416
  if (SECRET_DIRECTORIES.has(part)) return true;
3250
3417
  if (SECRET_EXACT_FILES.has(part)) return true;
3251
3418
  if (part === ".env" || part === ".envrc" || part.startsWith(".env.") || part.startsWith(".env*")) {
@@ -3256,15 +3423,16 @@ function isSecretPathLike(value) {
3256
3423
  }
3257
3424
  if (part.includes("credentials")) return true;
3258
3425
  if (part.startsWith("secrets.")) return true;
3426
+ if (part.startsWith("service-account") && part.endsWith(".json")) return true;
3259
3427
  if (SECRET_SUFFIXES.some((suffix) => part.endsWith(suffix))) return true;
3260
3428
  }
3261
3429
  return false;
3262
3430
  }
3263
3431
  function isPathInsideRepo(pathValue, repoPath) {
3264
3432
  if (pathValue.includes("..")) return false;
3265
- const repoRoot = resolve(repoPath);
3266
- const candidate = isAbsolute(pathValue) ? resolve(pathValue) : resolve(repoRoot, pathValue);
3267
- 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);
3268
3436
  return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
3269
3437
  }
3270
3438
  function evaluateReadLike(secretValues, repoPathValues, context) {
@@ -3291,6 +3459,9 @@ function evaluateWrite(input, context) {
3291
3459
  if (!isPathInsideRepo(pathValue, context.repoPath)) {
3292
3460
  return { behavior: "deny", message: WRITE_OUTSIDE_REPO_DENIAL };
3293
3461
  }
3462
+ if (isSecretPathLike(pathValue)) {
3463
+ return { behavior: "deny", message: SECRET_MATERIAL_DENIAL };
3464
+ }
3294
3465
  if (isSensitiveWritePath(pathValue, context.repoPath)) {
3295
3466
  return { behavior: "deny", message: SENSITIVE_WRITE_DENIAL };
3296
3467
  }
@@ -3384,9 +3555,9 @@ function normalizePathLike(value) {
3384
3555
  return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
3385
3556
  }
3386
3557
  function repoRelativePath(pathValue, repoPath) {
3387
- const repoRoot = resolve(repoPath);
3388
- const candidate = isAbsolute(pathValue) ? resolve(pathValue) : resolve(repoRoot, pathValue);
3389
- 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));
3390
3561
  }
3391
3562
  function isSensitiveWritePath(pathValue, repoPath) {
3392
3563
  const parts = repoRelativePath(pathValue, repoPath).split("/").map((part) => stripOuterQuotes(part.trim().toLowerCase())).filter(Boolean);
@@ -4349,6 +4520,10 @@ function short(s, max = 60) {
4349
4520
  // src/engine/providers/claude.ts
4350
4521
  var READ_ONLY_TOOLS2 = ["Read", "Glob", "Grep"];
4351
4522
  var EDIT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
4523
+ var FAST_MODE_MODELS = /* @__PURE__ */ new Set(["claude-opus-5", "claude-opus-4-8"]);
4524
+ function supportsClaudeFastMode(model) {
4525
+ return FAST_MODE_MODELS.has(model);
4526
+ }
4352
4527
  function createClaudeProvider(config, session) {
4353
4528
  return {
4354
4529
  name: "claude",
@@ -4384,6 +4559,7 @@ function createClaudeProvider(config, session) {
4384
4559
  hooks: buildToolPolicyHooks(invocation.cwd, trustedTools),
4385
4560
  canUseTool: createToolPermissionCallback(invocation.cwd, trustedTools),
4386
4561
  maxTurns: roleConfig.maxTurns,
4562
+ ...supportsClaudeFastMode(roleConfig.model) ? { settings: { fastMode: true } } : {},
4387
4563
  ...invocation.resumeSessionId ? { resume: invocation.resumeSessionId } : {},
4388
4564
  settingSources: []
4389
4565
  }
@@ -4435,15 +4611,27 @@ function createClaudeProvider(config, session) {
4435
4611
  }
4436
4612
 
4437
4613
  // src/engine/providers/openai.ts
4438
- import { readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
4439
- 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";
4440
4616
  function jailedPath(cwd, candidate) {
4441
- const resolved = resolve2(cwd, candidate);
4442
- if (resolved !== cwd && !resolved.startsWith(cwd + sep)) {
4617
+ const resolved = resolve4(cwd, candidate);
4618
+ if (resolved !== cwd && !resolved.startsWith(cwd + sep3)) {
4443
4619
  throw new Error(`path ${candidate} escapes the repository`);
4444
4620
  }
4445
4621
  return resolved;
4446
4622
  }
4623
+ function guardedPath(cwd, toolName, candidate) {
4624
+ const path = jailedPath(cwd, candidate);
4625
+ const decision = evaluateToolUse(
4626
+ toolName,
4627
+ toolName === "Glob" ? { path: candidate, pattern: "*" } : { file_path: candidate },
4628
+ { repoPath: cwd }
4629
+ );
4630
+ if (decision.behavior === "deny") {
4631
+ throw new Error(decision.message);
4632
+ }
4633
+ return path;
4634
+ }
4447
4635
  function fileTools(cwd, allowWrite) {
4448
4636
  const tools = [
4449
4637
  {
@@ -4456,7 +4644,8 @@ function fileTools(cwd, allowWrite) {
4456
4644
  required: ["path"]
4457
4645
  },
4458
4646
  handler: async (input) => {
4459
- const content = readFileSync3(jailedPath(cwd, String(input.path ?? "")), "utf8");
4647
+ const path = guardedPath(cwd, "Read", String(input.path ?? ""));
4648
+ const content = readFileSync2(path, "utf8");
4460
4649
  return { content: content.slice(0, 4e4) };
4461
4650
  }
4462
4651
  },
@@ -4470,9 +4659,9 @@ function fileTools(cwd, allowWrite) {
4470
4659
  required: ["path"]
4471
4660
  },
4472
4661
  handler: async (input) => {
4473
- const target9 = jailedPath(cwd, String(input.path ?? "."));
4474
- const entries = readdirSync(target9).map((entry) => {
4475
- const kind = statSync(join8(target9, entry)).isDirectory() ? "dir" : "file";
4662
+ const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."));
4663
+ const entries = readdirSync(target9).filter((entry) => !isSecretPathLike(entry)).map((entry) => {
4664
+ const kind = statSync(join6(target9, entry)).isDirectory() ? "dir" : "file";
4476
4665
  return `${kind}:${entry}`;
4477
4666
  });
4478
4667
  return { entries: entries.slice(0, 500) };
@@ -4491,7 +4680,8 @@ function fileTools(cwd, allowWrite) {
4491
4680
  required: ["path", "content"]
4492
4681
  },
4493
4682
  handler: async (input) => {
4494
- writeFileSync2(jailedPath(cwd, String(input.path ?? "")), String(input.content ?? ""));
4683
+ const path = guardedPath(cwd, "Write", String(input.path ?? ""));
4684
+ writeFileSync(path, String(input.content ?? ""));
4495
4685
  return { ok: true };
4496
4686
  }
4497
4687
  },
@@ -4509,13 +4699,13 @@ function fileTools(cwd, allowWrite) {
4509
4699
  required: ["path", "oldText", "newText"]
4510
4700
  },
4511
4701
  handler: async (input) => {
4512
- const path = jailedPath(cwd, String(input.path ?? ""));
4513
- const content = readFileSync3(path, "utf8");
4702
+ const path = guardedPath(cwd, "Edit", String(input.path ?? ""));
4703
+ const content = readFileSync2(path, "utf8");
4514
4704
  const oldText = String(input.oldText ?? "");
4515
4705
  if (!content.includes(oldText)) {
4516
4706
  return { ok: false, reason: "oldText not found" };
4517
4707
  }
4518
- writeFileSync2(path, content.replace(oldText, String(input.newText ?? "")));
4708
+ writeFileSync(path, content.replace(oldText, String(input.newText ?? "")));
4519
4709
  return { ok: true };
4520
4710
  }
4521
4711
  }
@@ -4579,8 +4769,9 @@ function createOpenAIProvider(config, session) {
4579
4769
  };
4580
4770
  return {
4581
4771
  name: "openai-gateway",
4582
- onRunReady(readyRunId) {
4772
+ onRunReady(readyRunId, resumeSessionId) {
4583
4773
  runId = readyRunId;
4774
+ conversationId = resumeSessionId;
4584
4775
  },
4585
4776
  async invoke(invocation, roleConfig, onProgress) {
4586
4777
  let costUsd = 0;
@@ -4662,9 +4853,9 @@ function createRoutedProvider(routes, fallback) {
4662
4853
  const providers = /* @__PURE__ */ new Set([fallback, ...Object.values(routes)]);
4663
4854
  return {
4664
4855
  name: "routed",
4665
- onRunReady(runId) {
4856
+ onRunReady(runId, resumeSessionId) {
4666
4857
  for (const provider of providers) {
4667
- provider.onRunReady?.(runId);
4858
+ provider.onRunReady?.(runId, resumeSessionId);
4668
4859
  }
4669
4860
  },
4670
4861
  invoke(invocation, config, onProgress) {
@@ -4679,7 +4870,7 @@ var MINUTE = 6e4;
4679
4870
  function engineModelConfig(config) {
4680
4871
  const model = (role, fallback) => process.env[`WHISPERR_WIZARD_ENGINE_${role.toUpperCase()}_MODEL`]?.trim() || fallback;
4681
4872
  return {
4682
- survey: { model: model("survey", "claude-sonnet-5"), effort: "medium", maxTurns: 40, maxMs: 12 * MINUTE },
4873
+ survey: { model: model("survey", config.model), effort: "medium", maxTurns: 40, maxMs: 12 * MINUTE },
4683
4874
  design: { model: model("design", config.plannerModel), effort: "high", maxTurns: 40, maxMs: 15 * MINUTE },
4684
4875
  edit: { model: model("edit", config.model), effort: config.effort, maxTurns: 50, maxMs: 15 * MINUTE },
4685
4876
  review: { model: model("review", config.model), effort: "medium", maxTurns: 15, maxMs: 8 * MINUTE }
@@ -4716,16 +4907,7 @@ async function tryEngineFlow(options) {
4716
4907
  playbookSystemPrompt: playbook.systemPrompt,
4717
4908
  sink,
4718
4909
  callbacks: {
4719
- reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2),
4720
- confirmApply: async (patch, summaryLines) => {
4721
- p.note(summaryLines.join("\n"), "Integration result");
4722
- const lineCount = patch.split("\n").length;
4723
- const answer = await p.confirm({
4724
- message: `Apply these changes to your working tree? (${lineCount} patch lines)`,
4725
- initialValue: true
4726
- });
4727
- return answer === true;
4728
- }
4910
+ reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2)
4729
4911
  }
4730
4912
  });
4731
4913
  if (result.kind === "wrong_mode") {
@@ -4775,7 +4957,7 @@ async function reviewEventsInTerminal(proposed, theme2) {
4775
4957
  });
4776
4958
  p.note(lines.join("\n\n"), `Proposed events (${proposed.length})`);
4777
4959
  const selection = await p.multiselect({
4778
- message: "Register these events? Deselect any you don't want.",
4960
+ message: "Register and integrate these events? Deselect any you don't want.",
4779
4961
  options: proposed.map((event) => ({ value: event.code, label: event.code, hint: event.name })),
4780
4962
  initialValues: proposed.map((event) => event.code),
4781
4963
  required: false
@@ -5018,7 +5200,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5018
5200
  return Promise.resolve({ ran: false, ok: true, output: "" });
5019
5201
  }
5020
5202
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
5021
- return new Promise((resolve4) => {
5203
+ return new Promise((resolve6) => {
5022
5204
  const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
5023
5205
  let out = "";
5024
5206
  const append = (d) => {
@@ -5029,11 +5211,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5029
5211
  child.stderr?.on("data", append);
5030
5212
  const timer = setTimeout(() => {
5031
5213
  child.kill("SIGKILL");
5032
- resolve4({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
5214
+ resolve6({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
5033
5215
  }, timeoutMs);
5034
5216
  child.on("close", (code) => {
5035
5217
  clearTimeout(timer);
5036
- resolve4({
5218
+ resolve6({
5037
5219
  ran: true,
5038
5220
  ok: code === 0,
5039
5221
  command,
@@ -5045,7 +5227,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5045
5227
  });
5046
5228
  child.on("error", () => {
5047
5229
  clearTimeout(timer);
5048
- resolve4({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
5230
+ resolve6({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
5049
5231
  });
5050
5232
  });
5051
5233
  }
@@ -5055,21 +5237,21 @@ function tail2(s) {
5055
5237
  }
5056
5238
 
5057
5239
  // src/core/version.ts
5058
- import { readFileSync as readFileSync4 } from "fs";
5059
- 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";
5060
5242
  import { fileURLToPath } from "url";
5061
5243
  var PACKAGE_NAME = "@whisperr/wizard";
5062
5244
  function packageVersion() {
5063
- let dir = dirname3(fileURLToPath(import.meta.url));
5245
+ let dir = dirname4(fileURLToPath(import.meta.url));
5064
5246
  const { root } = parse(dir);
5065
5247
  while (true) {
5066
5248
  try {
5067
- const pkg = JSON.parse(readFileSync4(join9(dir, "package.json"), "utf8"));
5249
+ const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf8"));
5068
5250
  if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
5069
5251
  } catch {
5070
5252
  }
5071
5253
  if (dir === root) return "0.0.0";
5072
- dir = dirname3(dir);
5254
+ dir = dirname4(dir);
5073
5255
  }
5074
5256
  }
5075
5257
 
@@ -5202,7 +5384,7 @@ function banner() {
5202
5384
 
5203
5385
  // src/cli.ts
5204
5386
  async function run2(options) {
5205
- const repoPath = resolve3(options.path ?? process.cwd());
5387
+ const repoPath = resolve5(options.path ?? process.cwd());
5206
5388
  const config = resolveConfig(options);
5207
5389
  console.log(banner());
5208
5390
  p2.intro(theme.signal("Let's wire Whisperr into your app."));