@whisperr/wizard 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +13 -7
  2. package/dist/index.js +1054 -494
  3. 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 resolve6 } 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 readFile3 } from "fs/promises";
1312
+ import { basename, resolve as resolve3, sep as sep3 } from "path";
1389
1313
 
1390
1314
  // src/engine/bindings.ts
1391
1315
  function bindingsTargetFor(target9) {
@@ -1582,6 +1506,104 @@ 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
+ var MAX_CLUSTER_EVENTS = 4;
1519
+ var MAX_CONCURRENT_CLUSTERS = 4;
1520
+ function clusterIdFor(files, eventIds = [], slicePosition = 0) {
1521
+ const digest = createHash("sha256").update(JSON.stringify({
1522
+ files: [...files].sort(),
1523
+ events: [...eventIds].sort(),
1524
+ slicePosition
1525
+ })).digest("hex");
1526
+ return `cluster_${digest.slice(0, 12)}`;
1527
+ }
1528
+ function deriveClusters(placements) {
1529
+ const byFile = /* @__PURE__ */ new Map();
1530
+ for (const placement of placements) {
1531
+ const existing = byFile.get(placement.file) ?? [];
1532
+ existing.push(placement);
1533
+ byFile.set(placement.file, existing);
1534
+ }
1535
+ const fileSlices = [];
1536
+ for (const file of [...byFile.keys()].sort()) {
1537
+ const events = [...byFile.get(file)].sort(
1538
+ (a, b) => a.eventCode.localeCompare(b.eventCode) || a.eventId.localeCompare(b.eventId)
1539
+ );
1540
+ for (let start = 0; start < events.length; start += MAX_CLUSTER_EVENTS) {
1541
+ fileSlices.push({
1542
+ file,
1543
+ position: start / MAX_CLUSTER_EVENTS,
1544
+ events: events.slice(start, start + MAX_CLUSTER_EVENTS)
1545
+ });
1546
+ }
1547
+ }
1548
+ const clusters = [];
1549
+ const positions = /* @__PURE__ */ new Map();
1550
+ for (const slice of fileSlices) {
1551
+ let target9 = clusters.find(
1552
+ (cluster) => !cluster.files.includes(slice.file) && cluster.events.length + slice.events.length <= MAX_CLUSTER_EVENTS
1553
+ );
1554
+ if (!target9) {
1555
+ target9 = { id: "", files: [], events: [], status: "pending", attempts: 0 };
1556
+ clusters.push(target9);
1557
+ }
1558
+ target9.files.push(slice.file);
1559
+ target9.events.push(...slice.events);
1560
+ const existing = positions.get(String(clusters.indexOf(target9))) ?? [];
1561
+ existing.push(slice.position);
1562
+ positions.set(String(clusters.indexOf(target9)), existing);
1563
+ }
1564
+ for (const [index, cluster] of clusters.entries()) {
1565
+ cluster.files.sort();
1566
+ cluster.events.sort((a, b) => a.eventCode.localeCompare(b.eventCode));
1567
+ cluster.id = clusterIdFor(
1568
+ cluster.files,
1569
+ cluster.events.map((event) => event.eventId || event.eventCode),
1570
+ Number((positions.get(String(index)) ?? [0]).join(""))
1571
+ );
1572
+ }
1573
+ return clusters;
1574
+ }
1575
+ function nextConcurrentClusters(clusters, limit = MAX_CONCURRENT_CLUSTERS) {
1576
+ const selected = [];
1577
+ const lockedFiles = /* @__PURE__ */ new Set();
1578
+ for (const cluster of clusters) {
1579
+ if (cluster.status !== "pending" && cluster.status !== "editing") continue;
1580
+ if (cluster.files.some((file) => lockedFiles.has(file))) continue;
1581
+ selected.push(cluster);
1582
+ for (const file of cluster.files) lockedFiles.add(file);
1583
+ if (selected.length === limit) break;
1584
+ }
1585
+ return selected;
1586
+ }
1587
+ var TERMINAL_STATUSES = ["reviewed", "failed_resumable", "blocked"];
1588
+ function nextPendingCluster(clusters) {
1589
+ return clusters.find((cluster) => cluster.status === "pending" || cluster.status === "editing");
1590
+ }
1591
+ function clustersComplete(clusters) {
1592
+ return clusters.every((cluster) => TERMINAL_STATUSES.includes(cluster.status));
1593
+ }
1594
+ function transitionCluster(clusters, clusterId, status, note3) {
1595
+ return clusters.map((cluster) => {
1596
+ if (cluster.id !== clusterId) {
1597
+ return cluster;
1598
+ }
1599
+ const attempts = status === "editing" ? cluster.attempts + 1 : cluster.attempts;
1600
+ return { ...cluster, status, attempts, note: note3 ?? cluster.note };
1601
+ });
1602
+ }
1603
+
1604
+ // src/engine/integrate.ts
1605
+ import { readFile as readFile2, writeFile } from "fs/promises";
1606
+ import { join as join3, resolve, sep } from "path";
1585
1607
 
1586
1608
  // src/engine/budgets.ts
1587
1609
  var OVERFLOW_PATTERNS = [
@@ -1593,6 +1615,8 @@ var OVERFLOW_PATTERNS = [
1593
1615
  ];
1594
1616
  var TRANSIENT_PATTERNS = [
1595
1617
  /rate.?limit/i,
1618
+ /quota/i,
1619
+ /\b429\b/,
1596
1620
  /overloaded/i,
1597
1621
  /timeout/i,
1598
1622
  /timed out/i,
@@ -1622,6 +1646,11 @@ function nextClusterStatusAfterFailure(failure, attempts) {
1622
1646
  return "failed_resumable";
1623
1647
  }
1624
1648
 
1649
+ // src/engine/types.ts
1650
+ function modelConfigFor(models, task, role) {
1651
+ return models[task] ?? models[role];
1652
+ }
1653
+
1625
1654
  // src/engine/verify.ts
1626
1655
  import { spawn } from "child_process";
1627
1656
  import { readFileSync } from "fs";
@@ -1666,7 +1695,7 @@ function npmCommand() {
1666
1695
  return process.platform === "win32" ? "npm.cmd" : "npm";
1667
1696
  }
1668
1697
  function runVerifyStep(cwd, step, timeoutMs = 3e5) {
1669
- return new Promise((resolve4) => {
1698
+ return new Promise((resolve7) => {
1670
1699
  const child = spawn(step.command, step.args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
1671
1700
  let output = "";
1672
1701
  const capture = (chunk) => {
@@ -1681,11 +1710,11 @@ function runVerifyStep(cwd, step, timeoutMs = 3e5) {
1681
1710
  }, timeoutMs);
1682
1711
  child.on("error", (error) => {
1683
1712
  clearTimeout(timer);
1684
- resolve4({ ...step, ok: false, output: String(error) });
1713
+ resolve7({ ...step, ok: false, output: String(error) });
1685
1714
  });
1686
1715
  child.on("close", (exitCode) => {
1687
1716
  clearTimeout(timer);
1688
- resolve4({ ...step, ok: exitCode === 0, output });
1717
+ resolve7({ ...step, ok: exitCode === 0, output });
1689
1718
  });
1690
1719
  });
1691
1720
  }
@@ -1696,21 +1725,6 @@ async function runVerifySteps(cwd, steps) {
1696
1725
  }
1697
1726
  return results;
1698
1727
  }
1699
- function verifiedStatus(results) {
1700
- if (results.length === 0) {
1701
- return "applied";
1702
- }
1703
- if (results.some((result) => !result.ok)) {
1704
- return "applied";
1705
- }
1706
- if (results.some((result) => result.kind === "build")) {
1707
- return "build_verified";
1708
- }
1709
- if (results.some((result) => result.kind === "typecheck" || result.kind === "analyze")) {
1710
- return "type_verified";
1711
- }
1712
- return "syntax_verified";
1713
- }
1714
1728
  function runVerificationStatus(results) {
1715
1729
  if (results.length === 0) {
1716
1730
  return "unavailable";
@@ -1741,6 +1755,8 @@ async function runSdkSetupPass(deps, bindings) {
1741
1755
  const outcome = await deps.provider.invoke(
1742
1756
  {
1743
1757
  role: "edit",
1758
+ task: "sdk_setup",
1759
+ sessionKey: "sdk_setup",
1744
1760
  systemPrompt: deps.playbookSystemPrompt,
1745
1761
  userPrompt: [
1746
1762
  "CORE SETUP for the Whisperr SDK in this repository. Do exactly this, then stop:",
@@ -1757,7 +1773,7 @@ async function runSdkSetupPass(deps, bindings) {
1757
1773
  allowRepoWrite: true,
1758
1774
  cwd: deps.worktree.path
1759
1775
  },
1760
- deps.models.edit,
1776
+ modelConfigFor(deps.models, "sdk_setup", "edit"),
1761
1777
  (action) => deps.sink.emit({ phase: "binding", action })
1762
1778
  );
1763
1779
  return outcome.kind === "completed";
@@ -1791,6 +1807,8 @@ async function reviewCluster(deps, cluster, diff) {
1791
1807
  const outcome = await deps.provider.invoke(
1792
1808
  {
1793
1809
  role: "review",
1810
+ task: "review",
1811
+ sessionKey: `review:${cluster.id}`,
1794
1812
  systemPrompt: [
1795
1813
  "You review an instrumentation diff against a strict checklist. Answer with a verdict line",
1796
1814
  "`VERDICT: pass` or `VERDICT: fail`, then bullet notes for anything wrong."
@@ -1808,7 +1826,7 @@ async function reviewCluster(deps, cluster, diff) {
1808
1826
  allowRepoWrite: false,
1809
1827
  cwd: deps.worktree.path
1810
1828
  },
1811
- deps.models.review,
1829
+ modelConfigFor(deps.models, "review", "review"),
1812
1830
  (action) => deps.sink.emit({ phase: "reviewing", action })
1813
1831
  );
1814
1832
  if (outcome.kind !== "completed") {
@@ -1821,7 +1839,6 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
1821
1839
  let clusters = initialClusters;
1822
1840
  const eventStatuses = /* @__PURE__ */ new Map();
1823
1841
  const bindings = generateBindings(deps.target, deps.registered);
1824
- const steps = resolveVerifySteps(deps.worktree.path, deps.target);
1825
1842
  const total = clusters.length;
1826
1843
  for (; ; ) {
1827
1844
  const cluster = nextPendingCluster(clusters);
@@ -1837,71 +1854,130 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
1837
1854
  file: cluster.files[0],
1838
1855
  action: "placing events"
1839
1856
  });
1840
- const edit = await deps.provider.invoke(
1841
- {
1842
- role: "edit",
1843
- systemPrompt: deps.playbookSystemPrompt,
1844
- userPrompt: clusterPrompt(cluster, deps.registered, bindings),
1845
- tools: [],
1846
- allowRepoWrite: true,
1847
- cwd: deps.worktree.path
1848
- },
1849
- deps.models.edit,
1857
+ const invocation = {
1858
+ role: "edit",
1859
+ task: "cluster_edit",
1860
+ sessionKey: cluster.id,
1861
+ systemPrompt: deps.playbookSystemPrompt,
1862
+ userPrompt: clusterPrompt(cluster, deps.registered, bindings),
1863
+ tools: [],
1864
+ allowRepoWrite: true,
1865
+ cwd: deps.worktree.path
1866
+ };
1867
+ let edit = await deps.provider.invoke(
1868
+ invocation,
1869
+ modelConfigFor(deps.models, "cluster_edit", "edit"),
1850
1870
  (action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
1851
1871
  );
1852
- if (edit.kind !== "completed") {
1872
+ let escalated = false;
1873
+ let structural = edit.kind === "completed" ? await structuralClusterCheck(deps.worktree.path, cluster) : { ok: false, notes: `edit ${edit.kind}` };
1874
+ if ((!structural.ok || edit.kind !== "completed") && deps.provider.invokeFallback) {
1875
+ await deps.discardChanges();
1876
+ escalated = true;
1877
+ deps.sink.emit({
1878
+ phase: "integrating",
1879
+ cluster: { index, total },
1880
+ action: "Terra attempt rolled back; retrying with Opus"
1881
+ });
1882
+ edit = await deps.provider.invokeFallback(
1883
+ { ...invocation, task: "repair", sessionKey: `escalation:${cluster.id}` },
1884
+ modelConfigFor(deps.models, "sdk_setup", "edit"),
1885
+ (action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
1886
+ );
1887
+ structural = edit.kind === "completed" ? await structuralClusterCheck(deps.worktree.path, cluster) : { ok: false, notes: `Opus escalation ${edit.kind}` };
1888
+ }
1889
+ if (edit.kind !== "completed" || !structural.ok) {
1853
1890
  const failure = edit.kind === "context_overflow" ? "context_overflow" : edit.kind === "budget_exhausted" ? "context_overflow" : classifyFailure(edit.kind === "failed" ? edit.error : "");
1854
1891
  const nextStatus = nextClusterStatusAfterFailure(failure, cluster.attempts);
1855
- clusters = transitionCluster(clusters, cluster.id, nextStatus, `edit ${edit.kind}`);
1892
+ clusters = transitionCluster(
1893
+ clusters,
1894
+ cluster.id,
1895
+ nextStatus,
1896
+ structural.notes || `edit ${edit.kind}`
1897
+ );
1856
1898
  deps.saveClusters(clusters);
1857
1899
  for (const placement of cluster.events) {
1858
1900
  eventStatuses.set(placement.eventCode, "failed");
1859
1901
  }
1902
+ await deps.discardChanges();
1860
1903
  continue;
1861
1904
  }
1862
- let statuses = "applied";
1863
- if (steps.length > 0) {
1864
- deps.sink.emit({ phase: "verifying", cluster: { index, total }, action: "running verifier" });
1865
- const results = await runVerifySteps(deps.worktree.path, steps);
1866
- statuses = verifiedStatus(results);
1867
- if (results.some((result) => !result.ok) && cluster.attempts < MAX_CLUSTER_ATTEMPTS) {
1868
- deps.sink.emit({ phase: "verifying", cluster: { index, total }, action: "repairing verification failure" });
1869
- await deps.provider.invoke(
1870
- {
1871
- role: "edit",
1872
- systemPrompt: deps.playbookSystemPrompt,
1873
- userPrompt: [
1874
- "The verification command failed after your edits. Fix ONLY the failure below, then stop.",
1875
- "",
1876
- results.map((result) => result.output).join("\n").slice(0, 2e4)
1877
- ].join("\n"),
1878
- tools: [],
1879
- allowRepoWrite: true,
1880
- cwd: deps.worktree.path
1881
- },
1882
- deps.models.edit,
1883
- (action) => deps.sink.emit({ phase: "verifying", cluster: { index, total }, action })
1884
- );
1885
- const retried = await runVerifySteps(deps.worktree.path, steps);
1886
- statuses = verifiedStatus(retried);
1887
- }
1888
- }
1889
1905
  clusters = transitionCluster(clusters, cluster.id, "verified");
1890
1906
  deps.saveClusters(clusters);
1891
1907
  const diff = await clusterDiff(cluster.files);
1892
- const review = await reviewCluster(deps, cluster, diff);
1893
- const finalStatus = review.ok ? statuses === "applied" ? "applied" : "reviewed" : statuses;
1908
+ let review = await reviewCluster(deps, cluster, diff);
1909
+ if (!review.ok && !escalated && deps.provider.invokeFallback) {
1910
+ await deps.discardChanges();
1911
+ deps.sink.emit({
1912
+ phase: "reviewing",
1913
+ cluster: { index, total },
1914
+ action: "Terra diff rejected; retrying the clean cluster with Opus"
1915
+ });
1916
+ edit = await deps.provider.invokeFallback(
1917
+ { ...invocation, task: "repair", sessionKey: `review-retry:${cluster.id}` },
1918
+ modelConfigFor(deps.models, "sdk_setup", "edit"),
1919
+ (action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
1920
+ );
1921
+ structural = edit.kind === "completed" ? await structuralClusterCheck(deps.worktree.path, cluster) : { ok: false, notes: `Opus review retry ${edit.kind}` };
1922
+ if (edit.kind === "completed" && structural.ok) {
1923
+ review = await reviewCluster(deps, cluster, await clusterDiff(cluster.files));
1924
+ }
1925
+ }
1926
+ if (!structural.ok || !review.ok) {
1927
+ clusters = transitionCluster(
1928
+ clusters,
1929
+ cluster.id,
1930
+ "blocked",
1931
+ (!structural.ok ? structural.notes : review.notes).slice(0, 500)
1932
+ );
1933
+ for (const placement of cluster.events) {
1934
+ eventStatuses.set(placement.eventCode, "failed");
1935
+ }
1936
+ await deps.discardChanges();
1937
+ continue;
1938
+ }
1939
+ const statuses = "reviewed";
1940
+ const finalStatus = review.ok ? statuses : statuses;
1894
1941
  clusters = transitionCluster(clusters, cluster.id, "reviewed", review.ok ? void 0 : review.notes.slice(0, 500));
1895
1942
  deps.saveClusters(clusters);
1896
1943
  for (const placement of cluster.events) {
1897
1944
  eventStatuses.set(placement.eventCode, finalStatus);
1898
1945
  }
1946
+ await deps.onClusterIntegrated(cluster, finalStatus);
1899
1947
  }
1900
1948
  if (!clustersComplete(clusters)) {
1901
1949
  throw new Error("cluster queue did not reach a terminal state");
1902
1950
  }
1903
1951
  return { clusters, eventStatuses };
1904
1952
  }
1953
+ async function structuralClusterCheck(repoPath, cluster) {
1954
+ const allowed = new Set(cluster.files);
1955
+ const missing = [];
1956
+ for (const placement of cluster.events) {
1957
+ if (!allowed.has(placement.file)) {
1958
+ missing.push(`${placement.eventCode}: placement escaped cluster`);
1959
+ continue;
1960
+ }
1961
+ try {
1962
+ const root = resolve(repoPath);
1963
+ const path = resolve(root, placement.file);
1964
+ if (!path.startsWith(root + sep)) {
1965
+ missing.push(`${placement.eventCode}: unsafe file`);
1966
+ continue;
1967
+ }
1968
+ const content = await readFile2(path, "utf8");
1969
+ if (!content.includes(eventWrapperName(placement.eventCode))) {
1970
+ missing.push(`${placement.eventCode}: wrapper call missing from ${placement.file}`);
1971
+ }
1972
+ } catch {
1973
+ missing.push(`${placement.eventCode}: cannot read ${placement.file}`);
1974
+ }
1975
+ }
1976
+ return {
1977
+ ok: missing.length === 0,
1978
+ notes: missing.join("; ")
1979
+ };
1980
+ }
1905
1981
  async function finalizeIntegration(deps) {
1906
1982
  const steps = resolveVerifySteps(deps.worktree.path, deps.target);
1907
1983
  deps.sink.emit({ phase: "verifying", action: "final verification pass" });
@@ -1912,6 +1988,33 @@ async function finalizeIntegration(deps) {
1912
1988
  command: verificationCommand(results)
1913
1989
  };
1914
1990
  }
1991
+ async function repairVerification(deps, results) {
1992
+ const failed = results.filter((result) => !result.ok);
1993
+ if (failed.length === 0) return true;
1994
+ deps.sink.emit({ phase: "verifying", action: "Opus is repairing final verification failures" });
1995
+ const outcome = await deps.provider.invoke(
1996
+ {
1997
+ role: "edit",
1998
+ task: "repair",
1999
+ sessionKey: "final_repair",
2000
+ systemPrompt: deps.playbookSystemPrompt,
2001
+ userPrompt: [
2002
+ "The one final repository verification failed after all instrumentation patches were merged.",
2003
+ "Repair only the affected integration files and their imports/configuration. Preserve every generated event wrapper call.",
2004
+ "Do not redesign events. Stop after the repair; the wizard will rerun verification.",
2005
+ "",
2006
+ failed.map((result) => `$ ${result.command} ${result.args.join(" ")}
2007
+ ${result.output}`).join("\n\n").slice(0, 3e4)
2008
+ ].join("\n"),
2009
+ tools: [],
2010
+ allowRepoWrite: true,
2011
+ cwd: deps.worktree.path
2012
+ },
2013
+ modelConfigFor(deps.models, "repair", "edit"),
2014
+ (action) => deps.sink.emit({ phase: "verifying", action })
2015
+ );
2016
+ return outcome.kind === "completed";
2017
+ }
1915
2018
 
1916
2019
  // src/engine/runs.ts
1917
2020
  import { createHash as createHash2 } from "crypto";
@@ -1977,6 +2080,9 @@ var RunsClient = class {
1977
2080
  patchRun(runId, patch) {
1978
2081
  return this.request("PATCH", `/wizard/runs/${runId}`, { ...patch });
1979
2082
  }
2083
+ saveProgress(runId, progress) {
2084
+ return this.patchRun(runId, { integrationEvidence: progress });
2085
+ }
1980
2086
  writeItem(runId, kind, payload, idempotencyExtra = "") {
1981
2087
  const code = typeof payload.code === "string" ? payload.code : JSON.stringify(payload);
1982
2088
  return this.request("POST", `/wizard/runs/${runId}/items`, {
@@ -2029,7 +2135,13 @@ function normalizeRunBootstrap(payload) {
2029
2135
  target: stringValue(project.target),
2030
2136
  kind: stringValue(project.kind)
2031
2137
  },
2032
- run: { id: run3.id, status: stringValue(run3.status), phase: stringValue(run3.phase) },
2138
+ run: {
2139
+ id: run3.id,
2140
+ status: stringValue(run3.status),
2141
+ phase: stringValue(run3.phase),
2142
+ ...stringValue(run3.modelConversationId) ? { modelConversationId: stringValue(run3.modelConversationId) } : {},
2143
+ ...normalizeIntegrationProgress(run3.integrationEvidence) ? { integrationEvidence: normalizeIntegrationProgress(run3.integrationEvidence) } : {}
2144
+ },
2033
2145
  resumed: value.resumed === true,
2034
2146
  universeMode: stringValue(value.universeMode),
2035
2147
  ...record(value.onboardingContext) ? { onboardingContext: record(value.onboardingContext) } : {},
@@ -2041,6 +2153,31 @@ function normalizeRunBootstrap(payload) {
2041
2153
  ingestion: { apiKey: ingestion.apiKey, baseUrl: ingestion.baseUrl }
2042
2154
  };
2043
2155
  }
2156
+ function normalizeIntegrationProgress(payload) {
2157
+ const value = record(payload);
2158
+ if (!value || !Array.isArray(value.changedFiles) || !Array.isArray(value.events)) {
2159
+ return void 0;
2160
+ }
2161
+ const events = value.events.map((candidate) => {
2162
+ const event = record(candidate);
2163
+ if (!event || !stringValue(event.eventId) || !stringValue(event.eventCode)) {
2164
+ return void 0;
2165
+ }
2166
+ return {
2167
+ eventId: stringValue(event.eventId),
2168
+ eventCode: stringValue(event.eventCode),
2169
+ ...stringValue(event.file) ? { file: stringValue(event.file) } : {},
2170
+ status: stringValue(event.status)
2171
+ };
2172
+ }).filter((event) => event !== void 0);
2173
+ return {
2174
+ changedFiles: value.changedFiles.filter((file) => typeof file === "string"),
2175
+ identifyWired: value.identifyWired === true,
2176
+ verificationStatus: stringValue(value.verificationStatus) || "pending",
2177
+ ...stringValue(value.verificationCommand) ? { verificationCommand: stringValue(value.verificationCommand) } : {},
2178
+ events
2179
+ };
2180
+ }
2044
2181
  function record(value) {
2045
2182
  return typeof value === "object" && value !== null ? value : void 0;
2046
2183
  }
@@ -2049,7 +2186,7 @@ function stringValue(value) {
2049
2186
  }
2050
2187
  function isSnapshotEvent(value) {
2051
2188
  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);
2189
+ 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
2190
  }
2054
2191
 
2055
2192
  // src/engine/selection.ts
@@ -2193,9 +2330,14 @@ function validateProposedEvent(input, seen, rejected) {
2193
2330
  if (rejected.has(code)) {
2194
2331
  return { ok: false, reason: `event ${code} was rejected by the user \u2014 do not re-propose it` };
2195
2332
  }
2196
- if (!input.name.trim() || !input.reasoning.trim() || !input.anchorFile.trim()) {
2333
+ const anchorFile = input.anchorFile.trim().replaceAll("\\", "/");
2334
+ const anchorParts = anchorFile.split("/");
2335
+ if (!input.name.trim() || !input.reasoning.trim() || !anchorFile) {
2197
2336
  return { ok: false, reason: "name, reasoning, and anchorFile are required" };
2198
2337
  }
2338
+ if (anchorFile.startsWith("/") || /^[a-z]:/i.test(anchorFile) || anchorParts.some((part) => part === "" || part === "." || part === "..")) {
2339
+ return { ok: false, reason: "anchorFile must be a safe repository-relative path" };
2340
+ }
2199
2341
  const schema = {};
2200
2342
  for (const [field, description] of Object.entries(input.payloadSchema ?? {})) {
2201
2343
  const normalizedField = normalizeEventCode(field);
@@ -2216,7 +2358,7 @@ function validateProposedEvent(input, seen, rejected) {
2216
2358
  code,
2217
2359
  name: input.name.trim(),
2218
2360
  reasoning: input.reasoning.trim(),
2219
- anchorFile: input.anchorFile.trim(),
2361
+ anchorFile,
2220
2362
  anchorSymbol: input.anchorSymbol?.trim() || void 0,
2221
2363
  payloadSchema: schema
2222
2364
  }
@@ -2227,6 +2369,8 @@ async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSess
2227
2369
  const outcome = await provider.invoke(
2228
2370
  {
2229
2371
  role: "survey",
2372
+ task: "survey",
2373
+ sessionKey: "survey",
2230
2374
  systemPrompt: SURVEY_SYSTEM_PROMPT,
2231
2375
  userPrompt: surveyUserPrompt(repoPath, renderOnboardingContext(bootstrap.onboardingContext)),
2232
2376
  tools: [],
@@ -2234,12 +2378,12 @@ async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSess
2234
2378
  cwd: repoPath,
2235
2379
  resumeSessionId
2236
2380
  },
2237
- models.survey,
2381
+ modelConfigFor(models, "survey", "survey"),
2238
2382
  (action) => sink.emit({ phase: "surveying", action })
2239
2383
  );
2240
2384
  return { report: outcome.kind === "completed" ? outcome.text : "", outcome };
2241
2385
  }
2242
- async function runSelection(provider, models, repoPath, bootstrap, surveyReport, rejectedCodes, sink) {
2386
+ async function runSelection(provider, models, repoPath, bootstrap, surveyReport, rejectedCodes, sink, existingEventCodes = bootstrap.snapshot.events.map((event) => event.code)) {
2243
2387
  const proposed = [];
2244
2388
  const seen = /* @__PURE__ */ new Set();
2245
2389
  const rejected = new Set(rejectedCodes);
@@ -2295,6 +2439,37 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
2295
2439
  return { ok: true, code: validated.event.code };
2296
2440
  }
2297
2441
  };
2442
+ const proposeBatchTool = {
2443
+ name: "propose_event_batch",
2444
+ description: "Propose a batch of important, concretely-anchored product events in one tool call.",
2445
+ inputSchema: {
2446
+ events: z.array(z.object(proposeTool.inputSchema)).min(1).max(200)
2447
+ },
2448
+ jsonSchema: {
2449
+ type: "object",
2450
+ properties: {
2451
+ events: {
2452
+ type: "array",
2453
+ minItems: 1,
2454
+ maxItems: 200,
2455
+ items: proposeTool.jsonSchema
2456
+ }
2457
+ },
2458
+ required: ["events"]
2459
+ },
2460
+ handler: async (input) => {
2461
+ const events = Array.isArray(input.events) ? input.events : [];
2462
+ const results = [];
2463
+ for (const event of events) {
2464
+ results.push(await proposeTool.handler(event ?? {}));
2465
+ }
2466
+ return {
2467
+ ok: results.every((result) => result.ok === true),
2468
+ accepted: results.filter((result) => result.ok === true).length,
2469
+ results
2470
+ };
2471
+ }
2472
+ };
2298
2473
  const finishTool = {
2299
2474
  name: "finish_selection",
2300
2475
  description: "Signal that every important event has been proposed.",
@@ -2314,18 +2489,19 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
2314
2489
  const outcome = await provider.invoke(
2315
2490
  {
2316
2491
  role: "design",
2492
+ task: "design",
2317
2493
  systemPrompt: SELECTION_SYSTEM_PROMPT,
2318
2494
  userPrompt: selectionUserPrompt(
2319
2495
  surveyReport,
2320
2496
  renderOnboardingContext(bootstrap.onboardingContext),
2321
- bootstrap.snapshot.events.map((event) => event.code),
2497
+ existingEventCodes,
2322
2498
  rejectedCodes
2323
2499
  ),
2324
- tools: [proposeTool, finishTool],
2500
+ tools: [proposeBatchTool, proposeTool, finishTool],
2325
2501
  allowRepoWrite: false,
2326
2502
  cwd: repoPath
2327
2503
  },
2328
- models.design,
2504
+ modelConfigFor(models, "design", "design"),
2329
2505
  (action) => sink.emit({ phase: "designing", action })
2330
2506
  );
2331
2507
  return {
@@ -2340,12 +2516,27 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
2340
2516
  async function registerApprovedEvents(runs, runId, approved, sink) {
2341
2517
  const registered = [];
2342
2518
  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
- });
2519
+ let result;
2520
+ try {
2521
+ result = await runs.writeItem(runId, "event", {
2522
+ code: event.code,
2523
+ name: event.name,
2524
+ reasoning: event.reasoning,
2525
+ anchorFile: event.anchorFile,
2526
+ ...event.anchorSymbol ? { anchorSymbol: event.anchorSymbol } : {},
2527
+ ...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
2528
+ }, "placement-v2");
2529
+ } catch (error) {
2530
+ if (error instanceof RunsApiError && error.code === "wizard_project_conflict") {
2531
+ sink.emit({
2532
+ phase: "persisting",
2533
+ file: event.anchorFile,
2534
+ action: `skipped ${event.code} \u2014 already registered by another project`
2535
+ });
2536
+ continue;
2537
+ }
2538
+ throw error;
2539
+ }
2349
2540
  registered.push({ ...event, eventId: result.id || result.item?.id || "" });
2350
2541
  sink.emit({ phase: "persisting", file: event.anchorFile, action: `registered ${event.code}` });
2351
2542
  }
@@ -2361,99 +2552,11 @@ function placementsFor(registered) {
2361
2552
  }));
2362
2553
  }
2363
2554
 
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
2555
  // src/engine/worktree.ts
2453
2556
  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";
2557
+ import { cp, lstat, mkdir, mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
2558
+ import { tmpdir } from "os";
2559
+ import { dirname, join as join4, relative, resolve as resolve2, sep as sep2 } from "path";
2457
2560
  var WorktreeError = class extends Error {
2458
2561
  constructor(code, message) {
2459
2562
  super(message);
@@ -2463,7 +2566,7 @@ var WorktreeError = class extends Error {
2463
2566
  code;
2464
2567
  };
2465
2568
  function git(cwd, args) {
2466
- return new Promise((resolve4) => {
2569
+ return new Promise((resolve7) => {
2467
2570
  const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
2468
2571
  let stdout = "";
2469
2572
  let stderr = "";
@@ -2473,8 +2576,8 @@ function git(cwd, args) {
2473
2576
  child.stderr.on("data", (chunk) => {
2474
2577
  stderr += String(chunk);
2475
2578
  });
2476
- child.on("error", () => resolve4({ ok: false, stdout, stderr: "git is not available" }));
2477
- child.on("close", (exitCode) => resolve4({ ok: exitCode === 0, stdout, stderr }));
2579
+ child.on("error", () => resolve7({ ok: false, stdout, stderr: "git is not available" }));
2580
+ child.on("close", (exitCode) => resolve7({ ok: exitCode === 0, stdout, stderr }));
2478
2581
  });
2479
2582
  }
2480
2583
  async function must(cwd, args) {
@@ -2484,21 +2587,64 @@ async function must(cwd, args) {
2484
2587
  }
2485
2588
  return result.stdout;
2486
2589
  }
2487
- async function createWorktree(repoPath, force = false) {
2590
+ function safeRepoFile(repoPath, file) {
2591
+ const path = resolve2(repoPath, file);
2592
+ if (path === repoPath || !path.startsWith(repoPath + sep2) || relative(repoPath, path).startsWith("..")) {
2593
+ throw new WorktreeError("git_failed", `unsafe progress file path: ${file}`);
2594
+ }
2595
+ return path;
2596
+ }
2597
+ async function dirtyFiles(repoPath) {
2598
+ const tracked = await must(repoPath, ["diff", "--name-only", "-z", "HEAD"]);
2599
+ const untracked = await must(repoPath, ["ls-files", "--others", "--exclude-standard", "-z"]);
2600
+ return [...new Set((tracked + untracked).split("\0").filter(Boolean))];
2601
+ }
2602
+ async function seedWorktree(repoPath, worktreePath, files) {
2603
+ for (const file of files) {
2604
+ const source = safeRepoFile(repoPath, file);
2605
+ const destination = safeRepoFile(worktreePath, file);
2606
+ try {
2607
+ await lstat(source);
2608
+ await mkdir(dirname(destination), { recursive: true });
2609
+ await cp(source, destination, { recursive: true, force: true });
2610
+ } catch {
2611
+ await rm(destination, { recursive: true, force: true });
2612
+ }
2613
+ }
2614
+ }
2615
+ async function createWorktree(repoPath, force = false, allowedDirtyFiles = []) {
2488
2616
  const inside = await git(repoPath, ["rev-parse", "--is-inside-work-tree"]);
2489
2617
  if (!inside.ok) {
2490
2618
  throw new WorktreeError("not_a_repo", "the target directory is not a git repository");
2491
2619
  }
2492
- const status = await must(repoPath, ["status", "--porcelain"]);
2493
- if (status.trim() !== "" && !force) {
2620
+ const dirty = await dirtyFiles(repoPath);
2621
+ const allowed = new Set(allowedDirtyFiles);
2622
+ const unexpected = dirty.filter((file) => !allowed.has(file));
2623
+ if (unexpected.length > 0 && !force) {
2494
2624
  throw new WorktreeError(
2495
2625
  "dirty_repo",
2496
- "the repository has uncommitted changes \u2014 commit or stash them, or rerun with --force"
2626
+ `the repository has unrelated uncommitted changes (${unexpected.slice(0, 3).join(", ")}) \u2014 commit or stash them, or rerun with --force`
2497
2627
  );
2498
2628
  }
2499
- const baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
2500
- const path = await mkdtemp(join5(tmpdir2(), "whisperr-worktree-"));
2629
+ let baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
2630
+ const path = await mkdtemp(join4(tmpdir(), "whisperr-worktree-"));
2501
2631
  await must(repoPath, ["worktree", "add", "--detach", "--force", path, "HEAD"]);
2632
+ const seedFiles = force ? dirty : dirty.filter((file) => allowed.has(file));
2633
+ if (seedFiles.length > 0) {
2634
+ await seedWorktree(repoPath, path, seedFiles);
2635
+ await must(path, ["add", "-A"]);
2636
+ await must(path, [
2637
+ "-c",
2638
+ "user.name=Whisperr Wizard",
2639
+ "-c",
2640
+ "user.email=wizard@whisperr.net",
2641
+ "commit",
2642
+ "--no-gpg-sign",
2643
+ "-m",
2644
+ "Whisperr resume checkpoint"
2645
+ ]);
2646
+ baseRef = (await must(path, ["rev-parse", "HEAD"])).trim();
2647
+ }
2502
2648
  return { path, baseRef, repoPath };
2503
2649
  }
2504
2650
  async function worktreePatch(handle) {
@@ -2509,7 +2655,7 @@ async function applyPatchToRepo(handle, patch) {
2509
2655
  if (patch.trim() === "") {
2510
2656
  return;
2511
2657
  }
2512
- await new Promise((resolve4, reject) => {
2658
+ await new Promise((resolve7, reject) => {
2513
2659
  const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
2514
2660
  cwd: handle.repoPath,
2515
2661
  stdio: ["pipe", "ignore", "pipe"]
@@ -2521,7 +2667,7 @@ async function applyPatchToRepo(handle, patch) {
2521
2667
  child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
2522
2668
  child.on("close", (exitCode) => {
2523
2669
  if (exitCode === 0) {
2524
- resolve4();
2670
+ resolve7();
2525
2671
  } else {
2526
2672
  reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
2527
2673
  }
@@ -2529,13 +2675,27 @@ async function applyPatchToRepo(handle, patch) {
2529
2675
  child.stdin.end(patch);
2530
2676
  });
2531
2677
  }
2532
- async function savePartialPatch(handle, destination) {
2533
- const patch = await worktreePatch(handle);
2678
+ async function commitWorktreeCheckpoint(handle) {
2679
+ await must(handle.path, ["add", "-A"]);
2680
+ const patch = await must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
2534
2681
  if (patch.trim() === "") {
2535
- return void 0;
2682
+ return;
2536
2683
  }
2537
- await writeFile2(destination, patch, { mode: 384 });
2538
- return destination;
2684
+ await must(handle.path, [
2685
+ "-c",
2686
+ "user.name=Whisperr Wizard",
2687
+ "-c",
2688
+ "user.email=wizard@whisperr.net",
2689
+ "commit",
2690
+ "--no-gpg-sign",
2691
+ "-m",
2692
+ "Whisperr integration checkpoint"
2693
+ ]);
2694
+ handle.baseRef = (await must(handle.path, ["rev-parse", "HEAD"])).trim();
2695
+ }
2696
+ async function discardWorktreeChanges(handle) {
2697
+ await must(handle.path, ["reset", "--hard", handle.baseRef]);
2698
+ await must(handle.path, ["clean", "-fd"]);
2539
2699
  }
2540
2700
  async function removeWorktree(handle) {
2541
2701
  await git(handle.repoPath, ["worktree", "remove", "--force", handle.path]);
@@ -2560,7 +2720,7 @@ async function runEngine(input) {
2560
2720
  throw error;
2561
2721
  }
2562
2722
  const runId = bootstrap.run.id;
2563
- input.provider.onRunReady?.(runId);
2723
+ input.provider.onRunReady?.(runId, bootstrap.run.modelConversationId);
2564
2724
  const patchPhase = async (phase, message) => {
2565
2725
  try {
2566
2726
  await runs.patchRun(runId, { phase, ...message ? { message } : {} });
@@ -2573,45 +2733,42 @@ async function runEngine(input) {
2573
2733
  } catch {
2574
2734
  }
2575
2735
  };
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
2736
  let registered;
2589
- if (!state.designComplete) {
2590
- try {
2737
+ try {
2738
+ const projectEvents = bootstrap.snapshot.events.filter(
2739
+ (event) => event.projectId === bootstrap.project.id
2740
+ );
2741
+ const placed = registeredFromSnapshot(projectEvents);
2742
+ const missingPlacement = projectEvents.filter((event) => !event.anchorFile);
2743
+ if (placed.length === 0 || missingPlacement.length > 0) {
2744
+ input.sink.emit({
2745
+ phase: "surveying",
2746
+ action: projectEvents.length === 0 ? "no saved progress found; starting a fresh survey" : "saved events need placement recovery; verifying the repository"
2747
+ });
2591
2748
  await patchPhase("surveying");
2592
2749
  const survey = await runSurvey(
2593
2750
  input.provider,
2594
2751
  input.models,
2595
2752
  input.repoPath,
2596
2753
  bootstrap,
2597
- input.sink,
2598
- state.providerSessions.survey
2754
+ input.sink
2599
2755
  );
2600
2756
  if (survey.outcome.kind !== "completed") {
2601
2757
  await failRun(`survey ${survey.outcome.kind}`);
2602
2758
  return { kind: "aborted", reason: `survey ${survey.outcome.kind}` };
2603
2759
  }
2604
- state.providerSessions.survey = survey.outcome.sessionId;
2605
- persist();
2606
2760
  await patchPhase("designing");
2761
+ const recoverableCodes = new Set(missingPlacement.map((event) => event.code));
2762
+ const existingCodes = bootstrap.snapshot.events.filter((event) => !recoverableCodes.has(event.code)).map((event) => event.code);
2607
2763
  const selection = await runSelection(
2608
2764
  input.provider,
2609
2765
  input.models,
2610
2766
  input.repoPath,
2611
2767
  bootstrap,
2612
2768
  survey.report,
2613
- state.tombstonedCodes,
2614
- input.sink
2769
+ [],
2770
+ input.sink,
2771
+ existingCodes
2615
2772
  );
2616
2773
  if (!selection.finished) {
2617
2774
  const reason = selection.outcome === "completed" ? "selection did not call finish_selection" : `selection ${selection.outcome}`;
@@ -2631,189 +2788,443 @@ async function runEngine(input) {
2631
2788
  await failRun("no events accepted for instrumentation");
2632
2789
  return { kind: "aborted", reason: "no events accepted for instrumentation" };
2633
2790
  }
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
2791
  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}` };
2792
+ const newlyRegistered = await registerApprovedEvents(runs, runId, approved, input.sink);
2793
+ registered = mergeRegistered(placed, newlyRegistered);
2794
+ const unresolved = projectEvents.filter((event) => !registered.some((candidate) => candidate.code === event.code)).map((event) => event.code);
2795
+ if (unresolved.length > 0) {
2796
+ throw new Error(`could not recover placements for: ${unresolved.join(", ")}`);
2797
+ }
2798
+ } else {
2799
+ registered = placed;
2800
+ input.sink.emit({
2801
+ phase: "integrating",
2802
+ action: `found ${registered.length} saved events; verifying current files`
2803
+ });
2650
2804
  }
2651
- } else {
2652
- registered = rebuildRegistered(state, bootstrap);
2653
- input.sink.emit({ phase: "designing", action: `resumed with ${registered.length} registered events` });
2805
+ } catch (error) {
2806
+ const reason = error instanceof Error ? error.message : String(error);
2807
+ await failRun(`design failed: ${reason}`);
2808
+ return { kind: "aborted", reason: `design failed: ${reason}` };
2654
2809
  }
2655
2810
  if (registered.length === 0) {
2656
2811
  await failRun("no events registered for this project");
2657
2812
  return { kind: "aborted", reason: "no events registered for this project" };
2658
2813
  }
2814
+ const prior = bootstrap.run.integrationEvidence;
2815
+ const changedFiles2 = new Set(prior?.changedFiles ?? []);
2816
+ const eventStatuses = await reconcileProgress(input.repoPath, registered, prior, input.sink);
2817
+ const eventFailureReasons = /* @__PURE__ */ new Map();
2818
+ for (const event of registered) {
2819
+ const status = eventStatuses.get(event.code) ?? "planned";
2820
+ if (status !== "planned" && status !== "failed" && status !== "unsupported") {
2821
+ changedFiles2.add(event.anchorFile);
2822
+ }
2823
+ }
2824
+ let identifyWired = prior?.identifyWired === true && await setupLooksPresent(input.repoPath, changedFiles2);
2825
+ const progress = () => ({
2826
+ changedFiles: [...changedFiles2].sort(),
2827
+ identifyWired,
2828
+ verificationStatus: "pending",
2829
+ events: buildEvidence(registered, eventStatuses)
2830
+ });
2831
+ const saveProgress = async () => {
2832
+ await runs.saveProgress(runId, progress());
2833
+ };
2834
+ try {
2835
+ await saveProgress();
2836
+ } catch (error) {
2837
+ const reason = error instanceof Error ? error.message : String(error);
2838
+ await failRun(`progress initialization failed: ${reason}`);
2839
+ return { kind: "aborted", reason: `progress initialization failed: ${reason}` };
2840
+ }
2659
2841
  await patchPhase("binding");
2660
2842
  let worktree;
2661
2843
  try {
2662
- worktree = await createWorktree(input.repoPath, input.force ?? false);
2844
+ worktree = await createWorktree(
2845
+ input.repoPath,
2846
+ input.force ?? false,
2847
+ [...changedFiles2]
2848
+ );
2663
2849
  } catch (error) {
2664
2850
  const reason = error instanceof Error ? error.message : String(error);
2665
2851
  await failRun(reason);
2666
2852
  return { kind: "aborted", reason };
2667
2853
  }
2668
- state.worktreePath = worktree.path;
2669
- persist();
2854
+ const checkpoint = async () => {
2855
+ const patch = await worktreePatch(worktree);
2856
+ if (patch.trim() === "") {
2857
+ return [];
2858
+ }
2859
+ const files = patchFiles(patch);
2860
+ for (const file of files) {
2861
+ changedFiles2.add(file);
2862
+ }
2863
+ await saveProgress();
2864
+ await applyPatchToRepo(worktree, patch);
2865
+ await commitWorktreeCheckpoint(worktree);
2866
+ return files;
2867
+ };
2670
2868
  try {
2671
2869
  const bindings = await writeBindingsModule(worktree, input.target, registered);
2672
- const deps = {
2870
+ if (!identifyWired) {
2871
+ const setupCompleted = await runSdkSetupPass({
2872
+ provider: input.provider,
2873
+ models: input.models,
2874
+ worktree,
2875
+ target: input.target,
2876
+ playbookSystemPrompt: input.playbookSystemPrompt,
2877
+ registered,
2878
+ ingestion: bootstrap.ingestion,
2879
+ sink: input.sink,
2880
+ saveClusters: () => {
2881
+ },
2882
+ discardChanges: async () => discardWorktreeChanges(worktree),
2883
+ onClusterIntegrated: async () => {
2884
+ }
2885
+ }, bindings);
2886
+ if (!setupCompleted) {
2887
+ throw new Error("SDK setup did not complete");
2888
+ }
2889
+ await checkpoint();
2890
+ identifyWired = await setupLooksPresent(input.repoPath, changedFiles2);
2891
+ if (!identifyWired) {
2892
+ throw new Error("SDK initialization, bindings, or identify() could not be verified");
2893
+ }
2894
+ await saveProgress();
2895
+ } else {
2896
+ await checkpoint();
2897
+ }
2898
+ const pending = registered.filter((event) => {
2899
+ const status = eventStatuses.get(event.code) ?? "planned";
2900
+ return status === "planned" || status === "failed";
2901
+ });
2902
+ let clusters = deriveClusters(placementsFor(pending));
2903
+ await patchPhase("integrating");
2904
+ await removeWorktree(worktree).catch(() => {
2905
+ });
2906
+ while (!clustersComplete(clusters)) {
2907
+ const wave = nextConcurrentClusters(clusters);
2908
+ if (wave.length === 0) {
2909
+ break;
2910
+ }
2911
+ const prepared = [];
2912
+ for (const cluster of wave) {
2913
+ try {
2914
+ prepared.push({
2915
+ cluster,
2916
+ handle: await createWorktree(input.repoPath, input.force ?? false, [...changedFiles2])
2917
+ });
2918
+ } catch (error) {
2919
+ prepared.push({
2920
+ cluster,
2921
+ error: error instanceof Error ? error.message : String(error)
2922
+ });
2923
+ }
2924
+ }
2925
+ const completed = await Promise.all(prepared.map(async (worker) => {
2926
+ if (!worker.handle) {
2927
+ return { ...worker, integration: void 0, patch: "" };
2928
+ }
2929
+ const workerDeps = {
2930
+ provider: input.provider,
2931
+ models: input.models,
2932
+ worktree: worker.handle,
2933
+ target: input.target,
2934
+ playbookSystemPrompt: input.playbookSystemPrompt,
2935
+ registered,
2936
+ ingestion: bootstrap.ingestion,
2937
+ sink: input.sink,
2938
+ saveClusters: () => {
2939
+ },
2940
+ discardChanges: async () => discardWorktreeChanges(worker.handle),
2941
+ onClusterIntegrated: async () => {
2942
+ }
2943
+ };
2944
+ try {
2945
+ const integration = await runClusterIntegration(
2946
+ workerDeps,
2947
+ [worker.cluster],
2948
+ async () => worktreePatch(worker.handle)
2949
+ );
2950
+ return {
2951
+ ...worker,
2952
+ integration,
2953
+ patch: await worktreePatch(worker.handle)
2954
+ };
2955
+ } catch (error) {
2956
+ return {
2957
+ ...worker,
2958
+ integration: void 0,
2959
+ patch: "",
2960
+ error: error instanceof Error ? error.message : String(error)
2961
+ };
2962
+ }
2963
+ }));
2964
+ for (const worker of completed) {
2965
+ const returned = worker.integration?.clusters[0];
2966
+ if (returned) {
2967
+ clusters = clusters.map((cluster) => cluster.id === returned.id ? returned : cluster);
2968
+ } else {
2969
+ clusters = transitionCluster(
2970
+ clusters,
2971
+ worker.cluster.id,
2972
+ "blocked",
2973
+ worker.error ?? "cluster worker failed"
2974
+ );
2975
+ }
2976
+ let applied = false;
2977
+ if (returned?.status === "reviewed") {
2978
+ const files = patchFiles(worker.patch);
2979
+ const unexpected = files.filter((file) => !worker.cluster.files.includes(file));
2980
+ if (unexpected.length > 0) {
2981
+ clusters = transitionCluster(
2982
+ clusters,
2983
+ worker.cluster.id,
2984
+ "blocked",
2985
+ `cluster edited files outside its isolation boundary: ${unexpected.join(", ")}`
2986
+ );
2987
+ } else {
2988
+ try {
2989
+ for (const file of files) changedFiles2.add(file);
2990
+ await saveProgress();
2991
+ await applyPatchToRepo(worker.handle, worker.patch);
2992
+ applied = true;
2993
+ } catch (error) {
2994
+ clusters = transitionCluster(
2995
+ clusters,
2996
+ worker.cluster.id,
2997
+ "blocked",
2998
+ `serial patch merge failed: ${error instanceof Error ? error.message : String(error)}`
2999
+ );
3000
+ }
3001
+ }
3002
+ }
3003
+ for (const placement of worker.cluster.events) {
3004
+ const status = applied ? worker.integration?.eventStatuses.get(placement.eventCode) ?? "failed" : "failed";
3005
+ eventStatuses.set(placement.eventCode, status);
3006
+ if (status === "failed") {
3007
+ const current = clusters.find((candidate) => candidate.id === worker.cluster.id);
3008
+ eventFailureReasons.set(
3009
+ placement.eventCode,
3010
+ current?.note ?? worker.error ?? "cluster patch could not be merged"
3011
+ );
3012
+ }
3013
+ await saveProgress();
3014
+ input.sink.emit({
3015
+ phase: "integrating",
3016
+ file: placement.file,
3017
+ action: applied ? `merged and saved ${placement.eventCode}` : `${placement.eventCode} needs manual attention`
3018
+ });
3019
+ }
3020
+ if (worker.handle) {
3021
+ await removeWorktree(worker.handle).catch(() => {
3022
+ });
3023
+ }
3024
+ }
3025
+ }
3026
+ await saveProgress();
3027
+ const unfinished = registered.filter((event) => {
3028
+ const status = eventStatuses.get(event.code) ?? "planned";
3029
+ return status === "planned" || status === "failed";
3030
+ });
3031
+ if (unfinished.length > 0) {
3032
+ throw new Error(
3033
+ `${unfinished.length} event${unfinished.length === 1 ? "" : "s"} still need integration`
3034
+ );
3035
+ }
3036
+ await patchPhase("verifying");
3037
+ const verificationWorktree = await createWorktree(
3038
+ input.repoPath,
3039
+ input.force ?? false,
3040
+ [...changedFiles2]
3041
+ );
3042
+ const verificationDeps = {
2673
3043
  provider: input.provider,
2674
3044
  models: input.models,
2675
- worktree,
3045
+ worktree: verificationWorktree,
2676
3046
  target: input.target,
2677
3047
  playbookSystemPrompt: input.playbookSystemPrompt,
2678
3048
  registered,
2679
3049
  ingestion: bootstrap.ingestion,
2680
3050
  sink: input.sink,
2681
- saveClusters: (clusters) => {
2682
- state.clusters = clusters;
2683
- persist();
3051
+ saveClusters: () => {
3052
+ },
3053
+ discardChanges: async () => discardWorktreeChanges(verificationWorktree),
3054
+ onClusterIntegrated: async () => {
2684
3055
  }
2685
3056
  };
2686
- const identifyWired = await runSdkSetupPass(deps, bindings);
2687
- await patchPhase("integrating");
2688
- const integration = await runClusterIntegration(
2689
- deps,
2690
- state.clusters,
2691
- async () => worktreePatch(worktree)
2692
- );
2693
- state.clusters = integration.clusters;
2694
- persist();
2695
- const finalVerify = await finalizeIntegration(deps);
2696
- const patch = await worktreePatch(worktree);
2697
- const changedFiles2 = patchFiles(patch);
3057
+ let finalVerify = await finalizeIntegration(verificationDeps);
3058
+ if (finalVerify.status === "failed") {
3059
+ const repaired = await repairVerification(verificationDeps, finalVerify.results);
3060
+ if (repaired) {
3061
+ const repairPatch = await worktreePatch(verificationWorktree);
3062
+ const repairFiles = patchFiles(repairPatch);
3063
+ finalVerify = await finalizeIntegration(verificationDeps);
3064
+ if (finalVerify.status !== "failed") {
3065
+ for (const file of repairFiles) changedFiles2.add(file);
3066
+ await saveProgress();
3067
+ await applyPatchToRepo(verificationWorktree, repairPatch);
3068
+ }
3069
+ }
3070
+ }
3071
+ await removeWorktree(verificationWorktree).catch(() => {
3072
+ });
3073
+ if (finalVerify.status === "failed") {
3074
+ for (const event of registered) {
3075
+ eventStatuses.set(event.code, "failed");
3076
+ eventFailureReasons.set(
3077
+ event.code,
3078
+ `final verification failed${finalVerify.command ? `: ${finalVerify.command}` : ""}`
3079
+ );
3080
+ }
3081
+ await saveProgress();
3082
+ throw new Error("final verification still fails after Opus repair");
3083
+ }
3084
+ identifyWired = await setupLooksPresent(input.repoPath, changedFiles2);
3085
+ if (!identifyWired) {
3086
+ for (const event of registered) {
3087
+ eventStatuses.set(event.code, "failed");
3088
+ eventFailureReasons.set(event.code, "SDK initialization or identify() is missing after final repair");
3089
+ }
3090
+ await saveProgress();
3091
+ throw new Error("SDK initialization and identify() must be present before completion");
3092
+ }
2698
3093
  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,
3094
+ await runs.completeRun(runId, {
3095
+ changedFiles: [...changedFiles2].sort(),
2706
3096
  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
- );
3097
+ verificationStatus: finalVerify.status,
3098
+ ...finalVerify.command ? { verificationCommand: finalVerify.command } : {},
3099
+ events: buildEvidence(registered, eventStatuses)
3100
+ });
3101
+ await removeWorktree(worktree).catch(() => {
3102
+ });
3103
+ const summaryLines = summarize(registered, eventStatuses);
3104
+ const hasFailures = [...eventStatuses.values()].some((status) => status === "failed");
2722
3105
  return {
2723
3106
  kind: hasFailures ? "partial" : "completed",
2724
3107
  runId,
2725
3108
  registered,
2726
- eventStatuses: integration.eventStatuses,
2727
- reportEvents: buildReportEvents(registered, integration.eventStatuses),
2728
- changedFiles: changedFiles2,
3109
+ eventStatuses,
3110
+ reportEvents: buildReportEvents(registered, eventStatuses, eventFailureReasons),
3111
+ changedFiles: [...changedFiles2].sort(),
2729
3112
  identifyWired,
2730
- applied,
2731
- partialPatchPath: partialPatchPath ?? void 0,
3113
+ applied: changedFiles2.size > 0,
2732
3114
  summary: summaryLines.join("\n")
2733
3115
  };
2734
3116
  } catch (error) {
2735
3117
  const reason = error instanceof Error ? error.message : String(error);
3118
+ for (const event of registered) {
3119
+ const status = eventStatuses.get(event.code) ?? "planned";
3120
+ if (status === "planned") {
3121
+ eventStatuses.set(event.code, "failed");
3122
+ eventFailureReasons.set(event.code, reason);
3123
+ }
3124
+ }
2736
3125
  await failRun(reason);
2737
- const partialPatchPath = await savePartialPatch(
2738
- worktree,
2739
- join6(input.repoPath, ".whisperr-wizard.patch")
2740
- ).catch(() => void 0);
2741
- persist();
3126
+ await removeWorktree(worktree).catch(() => {
3127
+ });
2742
3128
  return {
2743
3129
  kind: "partial",
2744
3130
  runId,
2745
3131
  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.`
3132
+ eventStatuses,
3133
+ reportEvents: buildReportEvents(registered, eventStatuses, eventFailureReasons),
3134
+ changedFiles: [...changedFiles2].sort(),
3135
+ identifyWired,
3136
+ applied: changedFiles2.size > 0,
3137
+ summary: `Integration stopped early: ${reason}. Saved event progress will be verified on rerun.`
2753
3138
  };
2754
3139
  }
2755
3140
  }
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
- };
3141
+ function registeredFromSnapshot(events) {
3142
+ return events.filter((event) => Boolean(event.anchorFile)).map((event) => ({
3143
+ code: event.code,
3144
+ name: event.name,
3145
+ reasoning: event.reasoning,
3146
+ eventId: event.id,
3147
+ anchorFile: event.anchorFile.replaceAll("\\", "/"),
3148
+ ...event.anchorSymbol ? { anchorSymbol: event.anchorSymbol } : {},
3149
+ payloadSchema: event.payloadSchema ?? {}
3150
+ }));
2770
3151
  }
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 ?? {}
3152
+ function mergeRegistered(existing, added) {
3153
+ const merged = new Map(existing.map((event) => [event.code, event]));
3154
+ for (const event of added) {
3155
+ merged.set(event.code, event);
3156
+ }
3157
+ return [...merged.values()].sort((a, b) => a.code.localeCompare(b.code));
3158
+ }
3159
+ async function reconcileProgress(repoPath, registered, prior, sink) {
3160
+ const priorById = new Map((prior?.events ?? []).map((event) => [event.eventId, event]));
3161
+ const statuses = /* @__PURE__ */ new Map();
3162
+ for (const event of registered) {
3163
+ const saved = priorById.get(event.eventId);
3164
+ if (saved?.status === "unsupported") {
3165
+ statuses.set(event.code, "unsupported");
3166
+ continue;
3167
+ }
3168
+ const present = await fileContainsWrapper(repoPath, event.anchorFile, event.code);
3169
+ const savedIntegrated = saved && saved.status !== "planned" && saved.status !== "failed";
3170
+ const status = present ? savedIntegrated ? saved.status : "applied" : "planned";
3171
+ statuses.set(event.code, status);
3172
+ if (saved && saved.status !== status) {
3173
+ sink.emit({
3174
+ phase: "verifying",
3175
+ file: event.anchorFile,
3176
+ action: present ? `recovered ${event.code} from the current code` : `${event.code} is missing and will be integrated again`
2788
3177
  });
2789
3178
  }
2790
3179
  }
2791
- return registered;
3180
+ return statuses;
2792
3181
  }
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
- });
3182
+ async function fileContainsWrapper(repoPath, file, eventCode) {
3183
+ try {
3184
+ const path = safeRepoPath(repoPath, file);
3185
+ const content = await readFile3(path, "utf8");
3186
+ return content.includes(eventWrapperName(eventCode));
3187
+ } catch {
3188
+ return false;
3189
+ }
2806
3190
  }
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
- });
3191
+ async function setupLooksPresent(repoPath, changedFiles2) {
3192
+ let bindingsFound = false;
3193
+ let bindingCallFound = false;
3194
+ let identifyFound = false;
3195
+ let sdkInitializationFound = false;
3196
+ for (const file of changedFiles2) {
3197
+ try {
3198
+ const content = await readFile3(safeRepoPath(repoPath, file), "utf8");
3199
+ const generated = content.includes("Generated by @whisperr/wizard");
3200
+ bindingsFound ||= generated;
3201
+ if (!generated) {
3202
+ bindingCallFound ||= content.includes("bindWhisperr(") || content.includes("WhisperrEvents.bind(");
3203
+ identifyFound ||= /\bidentify\s*\(/.test(content);
3204
+ sdkInitializationFound ||= /whisperr/i.test(content) && /(initiali[sz]e|configure|client|setup)/i.test(content);
3205
+ }
3206
+ } catch {
3207
+ }
3208
+ }
3209
+ return bindingsFound && bindingCallFound && identifyFound && sdkInitializationFound;
3210
+ }
3211
+ function safeRepoPath(repoPath, file) {
3212
+ const root = resolve3(repoPath);
3213
+ const path = resolve3(root, file);
3214
+ if (path === root || !path.startsWith(root + sep3)) {
3215
+ throw new Error(`unsafe progress file path: ${file}`);
3216
+ }
3217
+ return path;
2815
3218
  }
2816
- function buildReportEvents(registered, statuses) {
3219
+ function buildEvidence(registered, statuses) {
3220
+ return registered.map((event) => ({
3221
+ eventId: event.eventId,
3222
+ eventCode: event.code,
3223
+ file: event.anchorFile,
3224
+ status: statuses.get(event.code) ?? "planned"
3225
+ }));
3226
+ }
3227
+ function buildReportEvents(registered, statuses, failureReasons = /* @__PURE__ */ new Map()) {
2817
3228
  return registered.map((event) => {
2818
3229
  const status = statuses.get(event.code);
2819
3230
  const wired = status !== void 0 && status !== "failed" && status !== "unsupported" && status !== "planned";
@@ -2821,7 +3232,7 @@ function buildReportEvents(registered, statuses) {
2821
3232
  event_type: event.code,
2822
3233
  status: wired ? "wired" : "skipped",
2823
3234
  ...wired ? { file: event.anchorFile } : {},
2824
- ...wired ? {} : { reason: status ?? "not integrated" }
3235
+ ...wired ? {} : { reason: failureReasons.get(event.code) ?? status ?? "not integrated" }
2825
3236
  };
2826
3237
  });
2827
3238
  }
@@ -2852,9 +3263,9 @@ import {
2852
3263
 
2853
3264
  // src/core/git.ts
2854
3265
  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";
3266
+ import { createHash as createHash3 } from "crypto";
3267
+ import { readFile as readFile4 } from "fs/promises";
3268
+ import { basename as basename2, join as join5 } from "path";
2858
3269
  async function takeCheckpoint(repoPath) {
2859
3270
  const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
2860
3271
  if (!isRepo) return { isRepo: false };
@@ -2903,7 +3314,7 @@ async function revertToCheckpoint(repoPath, checkpoint) {
2903
3314
  async function repoFingerprint(repoPath) {
2904
3315
  const remote = await run(repoPath, ["remote", "get-url", "origin"]);
2905
3316
  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);
3317
+ return createHash3("sha256").update(seed).digest("hex").slice(0, 16);
2907
3318
  }
2908
3319
  function normalizeRemote(url) {
2909
3320
  return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
@@ -2920,7 +3331,7 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
2920
3331
  const contents = [];
2921
3332
  for (const file of files) {
2922
3333
  try {
2923
- contents.push({ file, content: await readFile2(join7(repoPath, file), "utf8") });
3334
+ contents.push({ file, content: await readFile4(join5(repoPath, file), "utf8") });
2924
3335
  } catch {
2925
3336
  continue;
2926
3337
  }
@@ -2950,14 +3361,14 @@ function escapeRegExp(s) {
2950
3361
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2951
3362
  }
2952
3363
  function run(cwd, args) {
2953
- return new Promise((resolve4) => {
3364
+ return new Promise((resolve7) => {
2954
3365
  const child = spawn3("git", args, { cwd });
2955
3366
  let stdout = "";
2956
3367
  let stderr = "";
2957
3368
  child.stdout.on("data", (d) => stdout += d.toString());
2958
3369
  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 }));
3370
+ child.on("close", (code) => resolve7({ ok: code === 0, stdout, stderr }));
3371
+ child.on("error", () => resolve7({ ok: false, stdout, stderr }));
2961
3372
  });
2962
3373
  }
2963
3374
 
@@ -3196,7 +3607,7 @@ function coverageNote(coverage) {
3196
3607
  }
3197
3608
 
3198
3609
  // src/core/toolPolicy.ts
3199
- import { isAbsolute, relative, resolve } from "path";
3610
+ import { isAbsolute, relative as relative2, resolve as resolve4 } from "path";
3200
3611
  var SECRET_MATERIAL_DENIAL = "blocked: secret material must not be accessed";
3201
3612
  var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
3202
3613
  var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
@@ -3292,9 +3703,9 @@ function isSecretPathLike(value) {
3292
3703
  }
3293
3704
  function isPathInsideRepo(pathValue, repoPath) {
3294
3705
  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);
3706
+ const repoRoot = resolve4(repoPath);
3707
+ const candidate = isAbsolute(pathValue) ? resolve4(pathValue) : resolve4(repoRoot, pathValue);
3708
+ const rel = relative2(repoRoot, candidate);
3298
3709
  return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
3299
3710
  }
3300
3711
  function evaluateReadLike(secretValues, repoPathValues, context) {
@@ -3417,9 +3828,9 @@ function normalizePathLike(value) {
3417
3828
  return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
3418
3829
  }
3419
3830
  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));
3831
+ const repoRoot = resolve4(repoPath);
3832
+ const candidate = isAbsolute(pathValue) ? resolve4(pathValue) : resolve4(repoRoot, pathValue);
3833
+ return normalizePathLike(relative2(repoRoot, candidate));
3423
3834
  }
3424
3835
  function isSensitiveWritePath(pathValue, repoPath) {
3425
3836
  const parts = repoRelativePath(pathValue, repoPath).split("/").map((part) => stripOuterQuotes(part.trim().toLowerCase())).filter(Boolean);
@@ -4387,8 +4798,12 @@ function supportsClaudeFastMode(model) {
4387
4798
  return FAST_MODE_MODELS.has(model);
4388
4799
  }
4389
4800
  function createClaudeProvider(config, session) {
4801
+ let runId = "";
4390
4802
  return {
4391
4803
  name: "claude",
4804
+ onRunReady(readyRunId) {
4805
+ runId = readyRunId;
4806
+ },
4392
4807
  async invoke(invocation, roleConfig, onProgress) {
4393
4808
  applyModelAuthEnv(config, session);
4394
4809
  const hostTools = invocation.tools.map(
@@ -4408,6 +4823,17 @@ function createClaudeProvider(config, session) {
4408
4823
  const deadline = Date.now() + roleConfig.maxMs;
4409
4824
  const outcome = (kind, text = "", error = "") => kind === "completed" ? { kind, text, sessionId, costUsd, turns } : kind === "failed" ? { kind, error, sessionId, costUsd, turns } : { kind, sessionId, costUsd, turns };
4410
4825
  try {
4826
+ const task = invocation.task ?? invocation.role;
4827
+ const sessionKey = invocation.sessionKey ?? task;
4828
+ const customHeaders = [
4829
+ process.env.ANTHROPIC_CUSTOM_HEADERS,
4830
+ runId ? `X-Whisperr-Wizard-Run-ID: ${headerValue(runId)}` : "",
4831
+ `X-Whisperr-Wizard-Task: ${headerValue(task)}`,
4832
+ `X-Whisperr-Wizard-Session-Key: ${headerValue(sessionKey)}`,
4833
+ `X-Whisperr-Wizard-Model: ${headerValue(roleConfig.model)}`,
4834
+ `X-Whisperr-Wizard-Effort: ${headerValue(roleConfig.effort ?? "")}`,
4835
+ `X-Whisperr-Wizard-Service-Tier: ${roleConfig.fastMode ? "fast" : "default"}`
4836
+ ].filter(Boolean).join("\n");
4411
4837
  const response = query2({
4412
4838
  prompt: invocation.userPrompt,
4413
4839
  options: {
@@ -4421,9 +4847,13 @@ function createClaudeProvider(config, session) {
4421
4847
  hooks: buildToolPolicyHooks(invocation.cwd, trustedTools),
4422
4848
  canUseTool: createToolPermissionCallback(invocation.cwd, trustedTools),
4423
4849
  maxTurns: roleConfig.maxTurns,
4424
- ...supportsClaudeFastMode(roleConfig.model) ? { settings: { fastMode: true } } : {},
4850
+ ...roleConfig.fastMode ?? supportsClaudeFastMode(roleConfig.model) ? { settings: { fastMode: true } } : {},
4425
4851
  ...invocation.resumeSessionId ? { resume: invocation.resumeSessionId } : {},
4426
- settingSources: []
4852
+ settingSources: [],
4853
+ env: {
4854
+ ...process.env,
4855
+ ANTHROPIC_CUSTOM_HEADERS: customHeaders
4856
+ }
4427
4857
  }
4428
4858
  });
4429
4859
  for await (const raw of response) {
@@ -4471,13 +4901,16 @@ function createClaudeProvider(config, session) {
4471
4901
  }
4472
4902
  };
4473
4903
  }
4904
+ function headerValue(value) {
4905
+ return value.replace(/[\r\n]/g, "").slice(0, 200);
4906
+ }
4474
4907
 
4475
4908
  // 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";
4909
+ import { readdirSync, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
4910
+ import { join as join6, resolve as resolve5, sep as sep4 } from "path";
4478
4911
  function jailedPath(cwd, candidate) {
4479
- const resolved = resolve2(cwd, candidate);
4480
- if (resolved !== cwd && !resolved.startsWith(cwd + sep)) {
4912
+ const resolved = resolve5(cwd, candidate);
4913
+ if (resolved !== cwd && !resolved.startsWith(cwd + sep4)) {
4481
4914
  throw new Error(`path ${candidate} escapes the repository`);
4482
4915
  }
4483
4916
  return resolved;
@@ -4507,7 +4940,7 @@ function fileTools(cwd, allowWrite) {
4507
4940
  },
4508
4941
  handler: async (input) => {
4509
4942
  const path = guardedPath(cwd, "Read", String(input.path ?? ""));
4510
- const content = readFileSync3(path, "utf8");
4943
+ const content = readFileSync2(path, "utf8");
4511
4944
  return { content: content.slice(0, 4e4) };
4512
4945
  }
4513
4946
  },
@@ -4523,7 +4956,7 @@ function fileTools(cwd, allowWrite) {
4523
4956
  handler: async (input) => {
4524
4957
  const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."));
4525
4958
  const entries = readdirSync(target9).filter((entry) => !isSecretPathLike(entry)).map((entry) => {
4526
- const kind = statSync(join8(target9, entry)).isDirectory() ? "dir" : "file";
4959
+ const kind = statSync(join6(target9, entry)).isDirectory() ? "dir" : "file";
4527
4960
  return `${kind}:${entry}`;
4528
4961
  });
4529
4962
  return { entries: entries.slice(0, 500) };
@@ -4543,7 +4976,7 @@ function fileTools(cwd, allowWrite) {
4543
4976
  },
4544
4977
  handler: async (input) => {
4545
4978
  const path = guardedPath(cwd, "Write", String(input.path ?? ""));
4546
- writeFileSync2(path, String(input.content ?? ""));
4979
+ writeFileSync(path, String(input.content ?? ""));
4547
4980
  return { ok: true };
4548
4981
  }
4549
4982
  },
@@ -4562,12 +4995,12 @@ function fileTools(cwd, allowWrite) {
4562
4995
  },
4563
4996
  handler: async (input) => {
4564
4997
  const path = guardedPath(cwd, "Edit", String(input.path ?? ""));
4565
- const content = readFileSync3(path, "utf8");
4998
+ const content = readFileSync2(path, "utf8");
4566
4999
  const oldText = String(input.oldText ?? "");
4567
5000
  if (!content.includes(oldText)) {
4568
5001
  return { ok: false, reason: "oldText not found" };
4569
5002
  }
4570
- writeFileSync2(path, content.replace(oldText, String(input.newText ?? "")));
5003
+ writeFileSync(path, content.replace(oldText, String(input.newText ?? "")));
4571
5004
  return { ok: true };
4572
5005
  }
4573
5006
  }
@@ -4577,8 +5010,9 @@ function fileTools(cwd, allowWrite) {
4577
5010
  }
4578
5011
  function createOpenAIProvider(config, session) {
4579
5012
  let runId;
4580
- let conversationId;
4581
- const gatewayFetch = async (path, body) => {
5013
+ const conversationIds = /* @__PURE__ */ new Map();
5014
+ let legacyConversationId;
5015
+ const gatewayFetch = async (path, body, sessionKey, task) => {
4582
5016
  if (!runId) {
4583
5017
  throw new Error("OpenAI provider used before the run was created");
4584
5018
  }
@@ -4588,7 +5022,11 @@ function createOpenAIProvider(config, session) {
4588
5022
  method: "POST",
4589
5023
  headers: {
4590
5024
  "Content-Type": "application/json",
4591
- Authorization: `Bearer ${session.token}`
5025
+ Authorization: `Bearer ${session.token}`,
5026
+ ...sessionKey === "legacy" ? {} : {
5027
+ "X-Whisperr-Wizard-Session-Key": sessionKey,
5028
+ "X-Whisperr-Wizard-Task": task
5029
+ }
4592
5030
  },
4593
5031
  body: JSON.stringify(body)
4594
5032
  }
@@ -4601,43 +5039,61 @@ function createOpenAIProvider(config, session) {
4601
5039
  payload = {};
4602
5040
  }
4603
5041
  if (!response.ok) {
4604
- const detail = payload.message ?? text;
5042
+ const top = payload;
5043
+ const nested = top.error?.error ?? top.error;
5044
+ const detail = top.message ?? nested?.message ?? text;
4605
5045
  const error = new Error(detail || `gateway request failed (${response.status})`);
4606
- error.code = payload.code;
5046
+ error.code = top.code ?? nested?.code;
4607
5047
  throw error;
4608
5048
  }
4609
5049
  return payload;
4610
5050
  };
4611
- const ensureConversation = async (resumeId) => {
4612
- if (conversationId) {
4613
- return conversationId;
5051
+ const ensureConversation = async (sessionKey, task, resumeId) => {
5052
+ const existing = sessionKey === "legacy" ? legacyConversationId : conversationIds.get(sessionKey);
5053
+ if (existing) {
5054
+ return existing;
4614
5055
  }
4615
5056
  if (resumeId) {
4616
- conversationId = resumeId;
5057
+ if (sessionKey === "legacy") {
5058
+ legacyConversationId = resumeId;
5059
+ } else {
5060
+ conversationIds.set(sessionKey, resumeId);
5061
+ }
4617
5062
  return resumeId;
4618
5063
  }
4619
5064
  try {
4620
- const created = await gatewayFetch("/v1/conversations", {});
4621
- conversationId = created.id;
5065
+ const created = await gatewayFetch("/v1/conversations", {}, sessionKey, task);
5066
+ if (created.id) {
5067
+ if (sessionKey === "legacy") {
5068
+ legacyConversationId = created.id;
5069
+ } else {
5070
+ conversationIds.set(sessionKey, created.id);
5071
+ }
5072
+ }
4622
5073
  } catch (error) {
4623
5074
  if (error.code !== "conversation_already_bound") {
4624
5075
  throw error;
4625
5076
  }
4626
5077
  }
4627
- if (!conversationId) {
5078
+ const createdId = sessionKey === "legacy" ? legacyConversationId : conversationIds.get(sessionKey);
5079
+ if (!createdId) {
4628
5080
  throw new Error("wizard run already has a bound conversation; rerun with its resume state");
4629
5081
  }
4630
- return conversationId;
5082
+ return createdId;
4631
5083
  };
4632
5084
  return {
4633
5085
  name: "openai-gateway",
4634
- onRunReady(readyRunId) {
5086
+ onRunReady(readyRunId, resumeSessionId) {
4635
5087
  runId = readyRunId;
5088
+ legacyConversationId = resumeSessionId;
4636
5089
  },
4637
5090
  async invoke(invocation, roleConfig, onProgress) {
4638
5091
  let costUsd = 0;
4639
5092
  let turns = 0;
4640
- const outcome = (kind, text = "", error = "") => kind === "completed" ? { kind, text, sessionId: conversationId, costUsd, turns } : kind === "failed" ? { kind, error, sessionId: conversationId, costUsd, turns } : { kind, sessionId: conversationId, costUsd, turns };
5093
+ const task = invocation.task ?? invocation.role;
5094
+ const sessionKey = invocation.sessionKey?.trim() || "legacy";
5095
+ let activeConversationId = sessionKey === "legacy" ? legacyConversationId : conversationIds.get(sessionKey);
5096
+ const outcome = (kind, text = "", error = "") => kind === "completed" ? { kind, text, sessionId: activeConversationId, costUsd, turns } : kind === "failed" ? { kind, error, sessionId: activeConversationId, costUsd, turns } : { kind, sessionId: activeConversationId, costUsd, turns };
4641
5097
  const allTools = [...invocation.tools, ...fileTools(invocation.cwd, invocation.allowRepoWrite)];
4642
5098
  const toolsByName = new Map(allTools.map((tool2) => [tool2.name, tool2]));
4643
5099
  const toolDefs = allTools.map((tool2) => ({
@@ -4647,7 +5103,12 @@ function createOpenAIProvider(config, session) {
4647
5103
  parameters: tool2.jsonSchema
4648
5104
  }));
4649
5105
  try {
4650
- const conversation = await ensureConversation(invocation.resumeSessionId);
5106
+ const conversation = await ensureConversation(
5107
+ sessionKey,
5108
+ task,
5109
+ invocation.resumeSessionId
5110
+ );
5111
+ activeConversationId = conversation;
4651
5112
  const deadline = Date.now() + roleConfig.maxMs;
4652
5113
  let input = [
4653
5114
  { role: "user", content: invocation.userPrompt }
@@ -4663,8 +5124,9 @@ function createOpenAIProvider(config, session) {
4663
5124
  conversation,
4664
5125
  input,
4665
5126
  tools: toolDefs,
4666
- ...roleConfig.effort ? { reasoning: { effort: roleConfig.effort } } : {}
4667
- });
5127
+ ...roleConfig.effort ? { reasoning: { effort: roleConfig.effort } } : {},
5128
+ ...roleConfig.serviceTier ? { service_tier: roleConfig.serviceTier } : {}
5129
+ }, sessionKey, task);
4668
5130
  const items = response.output ?? [];
4669
5131
  const calls = items.filter(
4670
5132
  (item) => item.type === "function_call"
@@ -4710,18 +5172,102 @@ function createOpenAIProvider(config, session) {
4710
5172
  }
4711
5173
 
4712
5174
  // src/engine/providers/router.ts
4713
- function createRoutedProvider(routes, fallback) {
4714
- const providers = /* @__PURE__ */ new Set([fallback, ...Object.values(routes)]);
5175
+ var Semaphore = class {
5176
+ constructor(limit) {
5177
+ this.limit = limit;
5178
+ }
5179
+ limit;
5180
+ active = 0;
5181
+ waiting = [];
5182
+ async run(operation) {
5183
+ if (this.active >= this.limit) {
5184
+ await new Promise((resolve7) => this.waiting.push(resolve7));
5185
+ }
5186
+ this.active += 1;
5187
+ try {
5188
+ return await operation();
5189
+ } finally {
5190
+ this.active -= 1;
5191
+ this.waiting.shift()?.();
5192
+ }
5193
+ }
5194
+ };
5195
+ function shouldFallback(outcome) {
5196
+ if (outcome.kind === "context_overflow" || outcome.kind === "budget_exhausted") {
5197
+ return true;
5198
+ }
5199
+ return outcome.kind === "failed" && classifyFailure(outcome.error) !== "fatal";
5200
+ }
5201
+ function shouldDisableProvider(outcome) {
5202
+ return outcome.kind === "failed" && classifyFailure(outcome.error) === "transient";
5203
+ }
5204
+ function createRoutedProvider(routes, fallback, fallbackConcurrency = 2) {
5205
+ const normalized = /* @__PURE__ */ new Map();
5206
+ for (const [key, value] of Object.entries(routes)) {
5207
+ normalized.set(
5208
+ key,
5209
+ "provider" in value ? value : { provider: value }
5210
+ );
5211
+ }
5212
+ const providers = /* @__PURE__ */ new Set([
5213
+ fallback,
5214
+ ...[...normalized.values()].map((route) => route.provider)
5215
+ ]);
5216
+ const semaphores = /* @__PURE__ */ new Map();
5217
+ const disabledProviders = /* @__PURE__ */ new Set();
5218
+ const semaphoreFor = (provider, concurrency) => {
5219
+ let semaphore = semaphores.get(provider);
5220
+ if (!semaphore) {
5221
+ semaphore = new Semaphore(concurrency ?? (provider === fallback ? fallbackConcurrency : 4));
5222
+ semaphores.set(provider, semaphore);
5223
+ }
5224
+ return semaphore;
5225
+ };
5226
+ const routeFor = (invocation) => normalized.get(invocation.task ?? invocation.role) ?? normalized.get(invocation.role);
5227
+ const call = (provider, concurrency, invocation, config, onProgress) => semaphoreFor(provider, concurrency).run(() => provider.invoke(invocation, config, onProgress));
4715
5228
  return {
4716
5229
  name: "routed",
4717
- onRunReady(runId) {
5230
+ onRunReady(runId, resumeSessionId) {
5231
+ disabledProviders.clear();
4718
5232
  for (const provider of providers) {
4719
- provider.onRunReady?.(runId);
5233
+ provider.onRunReady?.(runId, resumeSessionId);
4720
5234
  }
4721
5235
  },
4722
5236
  invoke(invocation, config, onProgress) {
4723
- const provider = routes[invocation.role] ?? fallback;
4724
- return provider.invoke(invocation, config, onProgress);
5237
+ const route = routeFor(invocation);
5238
+ if (!route) {
5239
+ return call(fallback, fallbackConcurrency, invocation, config, onProgress);
5240
+ }
5241
+ if (disabledProviders.has(route.provider)) {
5242
+ return call(
5243
+ fallback,
5244
+ fallbackConcurrency,
5245
+ invocation,
5246
+ route.fallbackConfig ?? config,
5247
+ onProgress
5248
+ );
5249
+ }
5250
+ return call(route.provider, route.concurrency, invocation, config, onProgress).then(
5251
+ async (outcome) => {
5252
+ if (shouldDisableProvider(outcome)) {
5253
+ disabledProviders.add(route.provider);
5254
+ }
5255
+ if (!invocation.allowRepoWrite && shouldFallback(outcome)) {
5256
+ onProgress(`${route.provider.name} unavailable; continuing with ${fallback.name}`);
5257
+ return call(
5258
+ fallback,
5259
+ fallbackConcurrency,
5260
+ invocation,
5261
+ route.fallbackConfig ?? config,
5262
+ onProgress
5263
+ );
5264
+ }
5265
+ return outcome;
5266
+ }
5267
+ );
5268
+ },
5269
+ invokeFallback(invocation, config, onProgress) {
5270
+ return call(fallback, fallbackConcurrency, invocation, config, onProgress);
4725
5271
  }
4726
5272
  };
4727
5273
  }
@@ -4729,22 +5275,45 @@ function createRoutedProvider(routes, fallback) {
4729
5275
  // src/engine/cli.ts
4730
5276
  var MINUTE = 6e4;
4731
5277
  function engineModelConfig(config) {
4732
- const model = (role, fallback) => process.env[`WHISPERR_WIZARD_ENGINE_${role.toUpperCase()}_MODEL`]?.trim() || fallback;
5278
+ const model = (task, fallback) => process.env[`WHISPERR_WIZARD_ENGINE_${task.toUpperCase()}_MODEL`]?.trim() || fallback;
5279
+ const terra = "gpt-5.6-terra";
5280
+ const opus = "claude-opus-5";
5281
+ const opusOnly = process.env.WHISPERR_WIZARD_PRESET?.trim() === "claude-default" || config.offline === true;
5282
+ const survey = opusOnly ? { model: model("survey", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 5 * MINUTE } : { model: model("survey", terra), effort: "medium", serviceTier: "priority", maxTurns: 30, maxMs: 5 * MINUTE };
5283
+ const design = { model: model("design", opus), effort: "medium", fastMode: true, maxTurns: 80, maxMs: 20 * MINUTE };
5284
+ const sdkSetup = { model: model("sdk_setup", opus), effort: "medium", fastMode: true, maxTurns: 60, maxMs: 12 * MINUTE };
5285
+ const clusterEdit = opusOnly ? { model: model("cluster_edit", opus), effort: "medium", fastMode: true, maxTurns: 60, maxMs: 12 * MINUTE } : { model: model("cluster_edit", terra), effort: "medium", serviceTier: "priority", maxTurns: 30, maxMs: 5 * MINUTE };
5286
+ const repair = { model: model("repair", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 8 * MINUTE };
5287
+ const review = { model: model("review", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 8 * MINUTE };
4733
5288
  return {
4734
- survey: { model: model("survey", config.model), effort: "medium", maxTurns: 40, maxMs: 12 * MINUTE },
4735
- design: { model: model("design", config.plannerModel), effort: "high", maxTurns: 40, maxMs: 15 * MINUTE },
4736
- edit: { model: model("edit", config.model), effort: config.effort, maxTurns: 50, maxMs: 15 * MINUTE },
4737
- review: { model: model("review", config.model), effort: "medium", maxTurns: 15, maxMs: 8 * MINUTE }
5289
+ survey,
5290
+ design,
5291
+ edit: sdkSetup,
5292
+ review,
5293
+ sdk_setup: sdkSetup,
5294
+ cluster_edit: clusterEdit,
5295
+ repair
4738
5296
  };
4739
5297
  }
4740
5298
  function engineProvider(config, session) {
4741
5299
  const claude = createClaudeProvider(config, session);
4742
- const preset = process.env.WHISPERR_WIZARD_PRESET?.trim() ?? "claude-default";
5300
+ const models = engineModelConfig(config);
5301
+ const preset = process.env.WHISPERR_WIZARD_PRESET?.trim() ?? "hybrid";
5302
+ if (preset === "claude-default" || config.offline) {
5303
+ return createRoutedProvider({}, claude, 2);
5304
+ }
5305
+ const openai = createOpenAIProvider(config, session);
4743
5306
  if (preset === "sol-design") {
4744
- const openai = createOpenAIProvider(config, session);
4745
- return createRoutedProvider({ survey: openai, design: openai, review: openai }, claude);
5307
+ return createRoutedProvider({
5308
+ survey: { provider: openai, fallbackConfig: models.sdk_setup },
5309
+ design: { provider: openai, fallbackConfig: models.design },
5310
+ review: { provider: openai, fallbackConfig: models.review }
5311
+ }, claude);
4746
5312
  }
4747
- return claude;
5313
+ return createRoutedProvider({
5314
+ survey: { provider: openai, concurrency: 4, fallbackConfig: models.sdk_setup },
5315
+ cluster_edit: { provider: openai, concurrency: 4, fallbackConfig: models.sdk_setup }
5316
+ }, claude, 2);
4748
5317
  }
4749
5318
  async function tryEngineFlow(options) {
4750
5319
  const { repoPath, fingerprint, config, session, playbook, theme: theme2 } = options;
@@ -4768,16 +5337,7 @@ async function tryEngineFlow(options) {
4768
5337
  playbookSystemPrompt: playbook.systemPrompt,
4769
5338
  sink,
4770
5339
  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
- }
5340
+ reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2)
4781
5341
  }
4782
5342
  });
4783
5343
  if (result.kind === "wrong_mode") {
@@ -4827,7 +5387,7 @@ async function reviewEventsInTerminal(proposed, theme2) {
4827
5387
  });
4828
5388
  p.note(lines.join("\n\n"), `Proposed events (${proposed.length})`);
4829
5389
  const selection = await p.multiselect({
4830
- message: "Register these events? Deselect any you don't want.",
5390
+ message: "Register and integrate these events? Deselect any you don't want.",
4831
5391
  options: proposed.map((event) => ({ value: event.code, label: event.code, hint: event.name })),
4832
5392
  initialValues: proposed.map((event) => event.code),
4833
5393
  required: false
@@ -5070,7 +5630,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5070
5630
  return Promise.resolve({ ran: false, ok: true, output: "" });
5071
5631
  }
5072
5632
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
5073
- return new Promise((resolve4) => {
5633
+ return new Promise((resolve7) => {
5074
5634
  const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
5075
5635
  let out = "";
5076
5636
  const append = (d) => {
@@ -5081,11 +5641,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5081
5641
  child.stderr?.on("data", append);
5082
5642
  const timer = setTimeout(() => {
5083
5643
  child.kill("SIGKILL");
5084
- resolve4({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
5644
+ resolve7({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
5085
5645
  }, timeoutMs);
5086
5646
  child.on("close", (code) => {
5087
5647
  clearTimeout(timer);
5088
- resolve4({
5648
+ resolve7({
5089
5649
  ran: true,
5090
5650
  ok: code === 0,
5091
5651
  command,
@@ -5097,7 +5657,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5097
5657
  });
5098
5658
  child.on("error", () => {
5099
5659
  clearTimeout(timer);
5100
- resolve4({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
5660
+ resolve7({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
5101
5661
  });
5102
5662
  });
5103
5663
  }
@@ -5107,8 +5667,8 @@ function tail2(s) {
5107
5667
  }
5108
5668
 
5109
5669
  // src/core/version.ts
5110
- import { readFileSync as readFileSync4 } from "fs";
5111
- import { dirname as dirname3, join as join9, parse } from "path";
5670
+ import { readFileSync as readFileSync3 } from "fs";
5671
+ import { dirname as dirname3, join as join7, parse } from "path";
5112
5672
  import { fileURLToPath } from "url";
5113
5673
  var PACKAGE_NAME = "@whisperr/wizard";
5114
5674
  function packageVersion() {
@@ -5116,7 +5676,7 @@ function packageVersion() {
5116
5676
  const { root } = parse(dir);
5117
5677
  while (true) {
5118
5678
  try {
5119
- const pkg = JSON.parse(readFileSync4(join9(dir, "package.json"), "utf8"));
5679
+ const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf8"));
5120
5680
  if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
5121
5681
  } catch {
5122
5682
  }
@@ -5254,7 +5814,7 @@ function banner() {
5254
5814
 
5255
5815
  // src/cli.ts
5256
5816
  async function run2(options) {
5257
- const repoPath = resolve3(options.path ?? process.cwd());
5817
+ const repoPath = resolve6(options.path ?? process.cwd());
5258
5818
  const config = resolveConfig(options);
5259
5819
  console.log(banner());
5260
5820
  p2.intro(theme.signal("Let's wire Whisperr into your app."));