@whisperr/wizard 0.7.4 → 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 +652 -222
  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 resolve5 } 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,8 +1308,8 @@ function createProgressSink(options) {
1308
1308
  }
1309
1309
 
1310
1310
  // src/engine/orchestrator.ts
1311
- import { readFile as readFile2 } from "fs/promises";
1312
- import { basename, resolve as resolve2, sep as sep2 } from "path";
1311
+ import { readFile as readFile3 } from "fs/promises";
1312
+ import { basename, resolve as resolve3, sep as sep3 } from "path";
1313
1313
 
1314
1314
  // src/engine/bindings.ts
1315
1315
  function bindingsTargetFor(target9) {
@@ -1515,11 +1515,14 @@ function eventWrapperName(code) {
1515
1515
 
1516
1516
  // src/engine/clusters.ts
1517
1517
  import { createHash } from "crypto";
1518
- import { dirname } from "path";
1519
- var MIN_CLUSTER_EVENTS = 3;
1520
- var MAX_CLUSTER_EVENTS = 8;
1521
- function clusterIdFor(files) {
1522
- const digest = createHash("sha256").update([...files].sort().join("\n")).digest("hex");
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");
1523
1526
  return `cluster_${digest.slice(0, 12)}`;
1524
1527
  }
1525
1528
  function deriveClusters(placements) {
@@ -1529,46 +1532,58 @@ function deriveClusters(placements) {
1529
1532
  existing.push(placement);
1530
1533
  byFile.set(placement.file, existing);
1531
1534
  }
1532
- const byDirectory = /* @__PURE__ */ new Map();
1533
- for (const [file, events] of byFile) {
1534
- const directory = dirname(file);
1535
- const bucket = byDirectory.get(directory) ?? { files: /* @__PURE__ */ new Set(), events: [] };
1536
- bucket.files.add(file);
1537
- bucket.events.push(...events);
1538
- byDirectory.set(directory, bucket);
1539
- }
1540
- const clusters = [];
1541
- const pending = [];
1542
- for (const directory of [...byDirectory.keys()].sort()) {
1543
- const bucket = byDirectory.get(directory);
1544
- if (bucket.events.length >= MIN_CLUSTER_EVENTS) {
1545
- pending.push(bucket);
1546
- continue;
1547
- }
1548
- const last = pending[pending.length - 1];
1549
- if (last && last.events.length + bucket.events.length <= MAX_CLUSTER_EVENTS) {
1550
- for (const file of bucket.files) last.files.add(file);
1551
- last.events.push(...bucket.events);
1552
- } else {
1553
- pending.push(bucket);
1554
- }
1555
- }
1556
- for (const bucket of pending) {
1557
- const events = [...bucket.events].sort((a, b) => a.eventCode.localeCompare(b.eventCode));
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
+ );
1558
1540
  for (let start = 0; start < events.length; start += MAX_CLUSTER_EVENTS) {
1559
- const slice = events.slice(start, start + MAX_CLUSTER_EVENTS);
1560
- const files = [...new Set(slice.map((event) => event.file))].sort();
1561
- clusters.push({
1562
- id: clusterIdFor(files.length > 0 ? files : [`slice_${start}`]),
1563
- files,
1564
- events: slice,
1565
- status: "pending",
1566
- attempts: 0
1541
+ fileSlices.push({
1542
+ file,
1543
+ position: start / MAX_CLUSTER_EVENTS,
1544
+ events: events.slice(start, start + MAX_CLUSTER_EVENTS)
1567
1545
  });
1568
1546
  }
1569
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
+ }
1570
1573
  return clusters;
1571
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
+ }
1572
1587
  var TERMINAL_STATUSES = ["reviewed", "failed_resumable", "blocked"];
1573
1588
  function nextPendingCluster(clusters) {
1574
1589
  return clusters.find((cluster) => cluster.status === "pending" || cluster.status === "editing");
@@ -1587,8 +1602,8 @@ function transitionCluster(clusters, clusterId, status, note3) {
1587
1602
  }
1588
1603
 
1589
1604
  // src/engine/integrate.ts
1590
- import { writeFile } from "fs/promises";
1591
- import { join as join3 } from "path";
1605
+ import { readFile as readFile2, writeFile } from "fs/promises";
1606
+ import { join as join3, resolve, sep } from "path";
1592
1607
 
1593
1608
  // src/engine/budgets.ts
1594
1609
  var OVERFLOW_PATTERNS = [
@@ -1600,6 +1615,8 @@ var OVERFLOW_PATTERNS = [
1600
1615
  ];
1601
1616
  var TRANSIENT_PATTERNS = [
1602
1617
  /rate.?limit/i,
1618
+ /quota/i,
1619
+ /\b429\b/,
1603
1620
  /overloaded/i,
1604
1621
  /timeout/i,
1605
1622
  /timed out/i,
@@ -1629,6 +1646,11 @@ function nextClusterStatusAfterFailure(failure, attempts) {
1629
1646
  return "failed_resumable";
1630
1647
  }
1631
1648
 
1649
+ // src/engine/types.ts
1650
+ function modelConfigFor(models, task, role) {
1651
+ return models[task] ?? models[role];
1652
+ }
1653
+
1632
1654
  // src/engine/verify.ts
1633
1655
  import { spawn } from "child_process";
1634
1656
  import { readFileSync } from "fs";
@@ -1673,7 +1695,7 @@ function npmCommand() {
1673
1695
  return process.platform === "win32" ? "npm.cmd" : "npm";
1674
1696
  }
1675
1697
  function runVerifyStep(cwd, step, timeoutMs = 3e5) {
1676
- return new Promise((resolve6) => {
1698
+ return new Promise((resolve7) => {
1677
1699
  const child = spawn(step.command, step.args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
1678
1700
  let output = "";
1679
1701
  const capture = (chunk) => {
@@ -1688,11 +1710,11 @@ function runVerifyStep(cwd, step, timeoutMs = 3e5) {
1688
1710
  }, timeoutMs);
1689
1711
  child.on("error", (error) => {
1690
1712
  clearTimeout(timer);
1691
- resolve6({ ...step, ok: false, output: String(error) });
1713
+ resolve7({ ...step, ok: false, output: String(error) });
1692
1714
  });
1693
1715
  child.on("close", (exitCode) => {
1694
1716
  clearTimeout(timer);
1695
- resolve6({ ...step, ok: exitCode === 0, output });
1717
+ resolve7({ ...step, ok: exitCode === 0, output });
1696
1718
  });
1697
1719
  });
1698
1720
  }
@@ -1703,21 +1725,6 @@ async function runVerifySteps(cwd, steps) {
1703
1725
  }
1704
1726
  return results;
1705
1727
  }
1706
- function verifiedStatus(results) {
1707
- if (results.length === 0) {
1708
- return "applied";
1709
- }
1710
- if (results.some((result) => !result.ok)) {
1711
- return "applied";
1712
- }
1713
- if (results.some((result) => result.kind === "build")) {
1714
- return "build_verified";
1715
- }
1716
- if (results.some((result) => result.kind === "typecheck" || result.kind === "analyze")) {
1717
- return "type_verified";
1718
- }
1719
- return "syntax_verified";
1720
- }
1721
1728
  function runVerificationStatus(results) {
1722
1729
  if (results.length === 0) {
1723
1730
  return "unavailable";
@@ -1748,6 +1755,8 @@ async function runSdkSetupPass(deps, bindings) {
1748
1755
  const outcome = await deps.provider.invoke(
1749
1756
  {
1750
1757
  role: "edit",
1758
+ task: "sdk_setup",
1759
+ sessionKey: "sdk_setup",
1751
1760
  systemPrompt: deps.playbookSystemPrompt,
1752
1761
  userPrompt: [
1753
1762
  "CORE SETUP for the Whisperr SDK in this repository. Do exactly this, then stop:",
@@ -1764,7 +1773,7 @@ async function runSdkSetupPass(deps, bindings) {
1764
1773
  allowRepoWrite: true,
1765
1774
  cwd: deps.worktree.path
1766
1775
  },
1767
- deps.models.edit,
1776
+ modelConfigFor(deps.models, "sdk_setup", "edit"),
1768
1777
  (action) => deps.sink.emit({ phase: "binding", action })
1769
1778
  );
1770
1779
  return outcome.kind === "completed";
@@ -1798,6 +1807,8 @@ async function reviewCluster(deps, cluster, diff) {
1798
1807
  const outcome = await deps.provider.invoke(
1799
1808
  {
1800
1809
  role: "review",
1810
+ task: "review",
1811
+ sessionKey: `review:${cluster.id}`,
1801
1812
  systemPrompt: [
1802
1813
  "You review an instrumentation diff against a strict checklist. Answer with a verdict line",
1803
1814
  "`VERDICT: pass` or `VERDICT: fail`, then bullet notes for anything wrong."
@@ -1815,7 +1826,7 @@ async function reviewCluster(deps, cluster, diff) {
1815
1826
  allowRepoWrite: false,
1816
1827
  cwd: deps.worktree.path
1817
1828
  },
1818
- deps.models.review,
1829
+ modelConfigFor(deps.models, "review", "review"),
1819
1830
  (action) => deps.sink.emit({ phase: "reviewing", action })
1820
1831
  );
1821
1832
  if (outcome.kind !== "completed") {
@@ -1828,7 +1839,6 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
1828
1839
  let clusters = initialClusters;
1829
1840
  const eventStatuses = /* @__PURE__ */ new Map();
1830
1841
  const bindings = generateBindings(deps.target, deps.registered);
1831
- const steps = resolveVerifySteps(deps.worktree.path, deps.target);
1832
1842
  const total = clusters.length;
1833
1843
  for (; ; ) {
1834
1844
  const cluster = nextPendingCluster(clusters);
@@ -1844,22 +1854,47 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
1844
1854
  file: cluster.files[0],
1845
1855
  action: "placing events"
1846
1856
  });
1847
- const edit = await deps.provider.invoke(
1848
- {
1849
- role: "edit",
1850
- systemPrompt: deps.playbookSystemPrompt,
1851
- userPrompt: clusterPrompt(cluster, deps.registered, bindings),
1852
- tools: [],
1853
- allowRepoWrite: true,
1854
- cwd: deps.worktree.path
1855
- },
1856
- 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"),
1857
1870
  (action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
1858
1871
  );
1859
- 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) {
1860
1890
  const failure = edit.kind === "context_overflow" ? "context_overflow" : edit.kind === "budget_exhausted" ? "context_overflow" : classifyFailure(edit.kind === "failed" ? edit.error : "");
1861
1891
  const nextStatus = nextClusterStatusAfterFailure(failure, cluster.attempts);
1862
- 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
+ );
1863
1898
  deps.saveClusters(clusters);
1864
1899
  for (const placement of cluster.events) {
1865
1900
  eventStatuses.set(placement.eventCode, "failed");
@@ -1867,38 +1902,42 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
1867
1902
  await deps.discardChanges();
1868
1903
  continue;
1869
1904
  }
1870
- let statuses = "applied";
1871
- if (steps.length > 0) {
1872
- deps.sink.emit({ phase: "verifying", cluster: { index, total }, action: "running verifier" });
1873
- const results = await runVerifySteps(deps.worktree.path, steps);
1874
- statuses = verifiedStatus(results);
1875
- if (results.some((result) => !result.ok) && cluster.attempts < MAX_CLUSTER_ATTEMPTS) {
1876
- deps.sink.emit({ phase: "verifying", cluster: { index, total }, action: "repairing verification failure" });
1877
- await deps.provider.invoke(
1878
- {
1879
- role: "edit",
1880
- systemPrompt: deps.playbookSystemPrompt,
1881
- userPrompt: [
1882
- "The verification command failed after your edits. Fix ONLY the failure below, then stop.",
1883
- "",
1884
- results.map((result) => result.output).join("\n").slice(0, 2e4)
1885
- ].join("\n"),
1886
- tools: [],
1887
- allowRepoWrite: true,
1888
- cwd: deps.worktree.path
1889
- },
1890
- deps.models.edit,
1891
- (action) => deps.sink.emit({ phase: "verifying", cluster: { index, total }, action })
1892
- );
1893
- const retried = await runVerifySteps(deps.worktree.path, steps);
1894
- statuses = verifiedStatus(retried);
1895
- }
1896
- }
1897
1905
  clusters = transitionCluster(clusters, cluster.id, "verified");
1898
1906
  deps.saveClusters(clusters);
1899
1907
  const diff = await clusterDiff(cluster.files);
1900
- const review = await reviewCluster(deps, cluster, diff);
1901
- 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;
1902
1941
  clusters = transitionCluster(clusters, cluster.id, "reviewed", review.ok ? void 0 : review.notes.slice(0, 500));
1903
1942
  deps.saveClusters(clusters);
1904
1943
  for (const placement of cluster.events) {
@@ -1911,6 +1950,34 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
1911
1950
  }
1912
1951
  return { clusters, eventStatuses };
1913
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
+ }
1914
1981
  async function finalizeIntegration(deps) {
1915
1982
  const steps = resolveVerifySteps(deps.worktree.path, deps.target);
1916
1983
  deps.sink.emit({ phase: "verifying", action: "final verification pass" });
@@ -1921,6 +1988,33 @@ async function finalizeIntegration(deps) {
1921
1988
  command: verificationCommand(results)
1922
1989
  };
1923
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
+ }
1924
2018
 
1925
2019
  // src/engine/runs.ts
1926
2020
  import { createHash as createHash2 } from "crypto";
@@ -2275,6 +2369,8 @@ async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSess
2275
2369
  const outcome = await provider.invoke(
2276
2370
  {
2277
2371
  role: "survey",
2372
+ task: "survey",
2373
+ sessionKey: "survey",
2278
2374
  systemPrompt: SURVEY_SYSTEM_PROMPT,
2279
2375
  userPrompt: surveyUserPrompt(repoPath, renderOnboardingContext(bootstrap.onboardingContext)),
2280
2376
  tools: [],
@@ -2282,7 +2378,7 @@ async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSess
2282
2378
  cwd: repoPath,
2283
2379
  resumeSessionId
2284
2380
  },
2285
- models.survey,
2381
+ modelConfigFor(models, "survey", "survey"),
2286
2382
  (action) => sink.emit({ phase: "surveying", action })
2287
2383
  );
2288
2384
  return { report: outcome.kind === "completed" ? outcome.text : "", outcome };
@@ -2343,6 +2439,37 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
2343
2439
  return { ok: true, code: validated.event.code };
2344
2440
  }
2345
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
+ };
2346
2473
  const finishTool = {
2347
2474
  name: "finish_selection",
2348
2475
  description: "Signal that every important event has been proposed.",
@@ -2362,6 +2489,7 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
2362
2489
  const outcome = await provider.invoke(
2363
2490
  {
2364
2491
  role: "design",
2492
+ task: "design",
2365
2493
  systemPrompt: SELECTION_SYSTEM_PROMPT,
2366
2494
  userPrompt: selectionUserPrompt(
2367
2495
  surveyReport,
@@ -2369,11 +2497,11 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
2369
2497
  existingEventCodes,
2370
2498
  rejectedCodes
2371
2499
  ),
2372
- tools: [proposeTool, finishTool],
2500
+ tools: [proposeBatchTool, proposeTool, finishTool],
2373
2501
  allowRepoWrite: false,
2374
2502
  cwd: repoPath
2375
2503
  },
2376
- models.design,
2504
+ modelConfigFor(models, "design", "design"),
2377
2505
  (action) => sink.emit({ phase: "designing", action })
2378
2506
  );
2379
2507
  return {
@@ -2428,7 +2556,7 @@ function placementsFor(registered) {
2428
2556
  import { spawn as spawn2 } from "child_process";
2429
2557
  import { cp, lstat, mkdir, mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
2430
2558
  import { tmpdir } from "os";
2431
- import { dirname as dirname2, join as join4, relative, resolve, sep } from "path";
2559
+ import { dirname, join as join4, relative, resolve as resolve2, sep as sep2 } from "path";
2432
2560
  var WorktreeError = class extends Error {
2433
2561
  constructor(code, message) {
2434
2562
  super(message);
@@ -2438,7 +2566,7 @@ var WorktreeError = class extends Error {
2438
2566
  code;
2439
2567
  };
2440
2568
  function git(cwd, args) {
2441
- return new Promise((resolve6) => {
2569
+ return new Promise((resolve7) => {
2442
2570
  const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
2443
2571
  let stdout = "";
2444
2572
  let stderr = "";
@@ -2448,8 +2576,8 @@ function git(cwd, args) {
2448
2576
  child.stderr.on("data", (chunk) => {
2449
2577
  stderr += String(chunk);
2450
2578
  });
2451
- child.on("error", () => resolve6({ ok: false, stdout, stderr: "git is not available" }));
2452
- child.on("close", (exitCode) => resolve6({ 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 }));
2453
2581
  });
2454
2582
  }
2455
2583
  async function must(cwd, args) {
@@ -2460,8 +2588,8 @@ async function must(cwd, args) {
2460
2588
  return result.stdout;
2461
2589
  }
2462
2590
  function safeRepoFile(repoPath, file) {
2463
- const path = resolve(repoPath, file);
2464
- if (path === repoPath || !path.startsWith(repoPath + sep) || relative(repoPath, path).startsWith("..")) {
2591
+ const path = resolve2(repoPath, file);
2592
+ if (path === repoPath || !path.startsWith(repoPath + sep2) || relative(repoPath, path).startsWith("..")) {
2465
2593
  throw new WorktreeError("git_failed", `unsafe progress file path: ${file}`);
2466
2594
  }
2467
2595
  return path;
@@ -2477,7 +2605,7 @@ async function seedWorktree(repoPath, worktreePath, files) {
2477
2605
  const destination = safeRepoFile(worktreePath, file);
2478
2606
  try {
2479
2607
  await lstat(source);
2480
- await mkdir(dirname2(destination), { recursive: true });
2608
+ await mkdir(dirname(destination), { recursive: true });
2481
2609
  await cp(source, destination, { recursive: true, force: true });
2482
2610
  } catch {
2483
2611
  await rm(destination, { recursive: true, force: true });
@@ -2527,7 +2655,7 @@ async function applyPatchToRepo(handle, patch) {
2527
2655
  if (patch.trim() === "") {
2528
2656
  return;
2529
2657
  }
2530
- await new Promise((resolve6, reject) => {
2658
+ await new Promise((resolve7, reject) => {
2531
2659
  const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
2532
2660
  cwd: handle.repoPath,
2533
2661
  stdio: ["pipe", "ignore", "pipe"]
@@ -2539,7 +2667,7 @@ async function applyPatchToRepo(handle, patch) {
2539
2667
  child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
2540
2668
  child.on("close", (exitCode) => {
2541
2669
  if (exitCode === 0) {
2542
- resolve6();
2670
+ resolve7();
2543
2671
  } else {
2544
2672
  reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
2545
2673
  }
@@ -2686,6 +2814,7 @@ async function runEngine(input) {
2686
2814
  const prior = bootstrap.run.integrationEvidence;
2687
2815
  const changedFiles2 = new Set(prior?.changedFiles ?? []);
2688
2816
  const eventStatuses = await reconcileProgress(input.repoPath, registered, prior, input.sink);
2817
+ const eventFailureReasons = /* @__PURE__ */ new Map();
2689
2818
  for (const event of registered) {
2690
2819
  const status = eventStatuses.get(event.code) ?? "planned";
2691
2820
  if (status !== "planned" && status !== "failed" && status !== "unsupported") {
@@ -2758,7 +2887,10 @@ async function runEngine(input) {
2758
2887
  throw new Error("SDK setup did not complete");
2759
2888
  }
2760
2889
  await checkpoint();
2761
- identifyWired = true;
2890
+ identifyWired = await setupLooksPresent(input.repoPath, changedFiles2);
2891
+ if (!identifyWired) {
2892
+ throw new Error("SDK initialization, bindings, or identify() could not be verified");
2893
+ }
2762
2894
  await saveProgress();
2763
2895
  } else {
2764
2896
  await checkpoint();
@@ -2767,53 +2899,128 @@ async function runEngine(input) {
2767
2899
  const status = eventStatuses.get(event.code) ?? "planned";
2768
2900
  return status === "planned" || status === "failed";
2769
2901
  });
2770
- const clusters = deriveClusters(placementsFor(pending));
2771
- const deps = {
2772
- provider: input.provider,
2773
- models: input.models,
2774
- worktree,
2775
- target: input.target,
2776
- playbookSystemPrompt: input.playbookSystemPrompt,
2777
- registered,
2778
- ingestion: bootstrap.ingestion,
2779
- sink: input.sink,
2780
- saveClusters: () => {
2781
- },
2782
- discardChanges: async () => discardWorktreeChanges(worktree),
2783
- onClusterIntegrated: async (cluster, status) => {
2784
- const verified = /* @__PURE__ */ new Map();
2785
- for (const placement of cluster.events) {
2786
- const present = await fileContainsWrapper(
2787
- worktree.path,
2788
- placement.file,
2789
- placement.eventCode
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)
2790
2949
  );
2791
- verified.set(placement.eventCode, present ? status : "failed");
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
+ };
2792
2962
  }
2793
- await checkpoint();
2794
- for (const placement of cluster.events) {
2795
- eventStatuses.set(
2796
- placement.eventCode,
2797
- verified.get(placement.eventCode) ?? "failed"
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"
2798
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
+ }
2799
3013
  await saveProgress();
2800
3014
  input.sink.emit({
2801
3015
  phase: "integrating",
2802
3016
  file: placement.file,
2803
- action: `saved ${placement.eventCode} progress`
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(() => {
2804
3022
  });
2805
3023
  }
2806
- }
2807
- };
2808
- await patchPhase("integrating");
2809
- const integration = await runClusterIntegration(
2810
- deps,
2811
- clusters,
2812
- async () => worktreePatch(worktree)
2813
- );
2814
- for (const [code, status] of integration.eventStatuses) {
2815
- if ((eventStatuses.get(code) ?? "planned") === "planned") {
2816
- eventStatuses.set(code, status);
2817
3024
  }
2818
3025
  }
2819
3026
  await saveProgress();
@@ -2827,7 +3034,62 @@ async function runEngine(input) {
2827
3034
  );
2828
3035
  }
2829
3036
  await patchPhase("verifying");
2830
- const finalVerify = await finalizeIntegration(deps);
3037
+ const verificationWorktree = await createWorktree(
3038
+ input.repoPath,
3039
+ input.force ?? false,
3040
+ [...changedFiles2]
3041
+ );
3042
+ const verificationDeps = {
3043
+ provider: input.provider,
3044
+ models: input.models,
3045
+ worktree: verificationWorktree,
3046
+ target: input.target,
3047
+ playbookSystemPrompt: input.playbookSystemPrompt,
3048
+ registered,
3049
+ ingestion: bootstrap.ingestion,
3050
+ sink: input.sink,
3051
+ saveClusters: () => {
3052
+ },
3053
+ discardChanges: async () => discardWorktreeChanges(verificationWorktree),
3054
+ onClusterIntegrated: async () => {
3055
+ }
3056
+ };
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
+ }
2831
3093
  await patchPhase("reporting");
2832
3094
  await runs.completeRun(runId, {
2833
3095
  changedFiles: [...changedFiles2].sort(),
@@ -2845,7 +3107,7 @@ async function runEngine(input) {
2845
3107
  runId,
2846
3108
  registered,
2847
3109
  eventStatuses,
2848
- reportEvents: buildReportEvents(registered, eventStatuses),
3110
+ reportEvents: buildReportEvents(registered, eventStatuses, eventFailureReasons),
2849
3111
  changedFiles: [...changedFiles2].sort(),
2850
3112
  identifyWired,
2851
3113
  applied: changedFiles2.size > 0,
@@ -2853,6 +3115,13 @@ async function runEngine(input) {
2853
3115
  };
2854
3116
  } catch (error) {
2855
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
+ }
2856
3125
  await failRun(reason);
2857
3126
  await removeWorktree(worktree).catch(() => {
2858
3127
  });
@@ -2861,7 +3130,7 @@ async function runEngine(input) {
2861
3130
  runId,
2862
3131
  registered,
2863
3132
  eventStatuses,
2864
- reportEvents: buildReportEvents(registered, eventStatuses),
3133
+ reportEvents: buildReportEvents(registered, eventStatuses, eventFailureReasons),
2865
3134
  changedFiles: [...changedFiles2].sort(),
2866
3135
  identifyWired,
2867
3136
  applied: changedFiles2.size > 0,
@@ -2913,7 +3182,7 @@ async function reconcileProgress(repoPath, registered, prior, sink) {
2913
3182
  async function fileContainsWrapper(repoPath, file, eventCode) {
2914
3183
  try {
2915
3184
  const path = safeRepoPath(repoPath, file);
2916
- const content = await readFile2(path, "utf8");
3185
+ const content = await readFile3(path, "utf8");
2917
3186
  return content.includes(eventWrapperName(eventCode));
2918
3187
  } catch {
2919
3188
  return false;
@@ -2922,23 +3191,27 @@ async function fileContainsWrapper(repoPath, file, eventCode) {
2922
3191
  async function setupLooksPresent(repoPath, changedFiles2) {
2923
3192
  let bindingsFound = false;
2924
3193
  let bindingCallFound = false;
3194
+ let identifyFound = false;
3195
+ let sdkInitializationFound = false;
2925
3196
  for (const file of changedFiles2) {
2926
3197
  try {
2927
- const content = await readFile2(safeRepoPath(repoPath, file), "utf8");
3198
+ const content = await readFile3(safeRepoPath(repoPath, file), "utf8");
2928
3199
  const generated = content.includes("Generated by @whisperr/wizard");
2929
3200
  bindingsFound ||= generated;
2930
3201
  if (!generated) {
2931
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);
2932
3205
  }
2933
3206
  } catch {
2934
3207
  }
2935
3208
  }
2936
- return bindingsFound && bindingCallFound;
3209
+ return bindingsFound && bindingCallFound && identifyFound && sdkInitializationFound;
2937
3210
  }
2938
3211
  function safeRepoPath(repoPath, file) {
2939
- const root = resolve2(repoPath);
2940
- const path = resolve2(root, file);
2941
- if (path === root || !path.startsWith(root + sep2)) {
3212
+ const root = resolve3(repoPath);
3213
+ const path = resolve3(root, file);
3214
+ if (path === root || !path.startsWith(root + sep3)) {
2942
3215
  throw new Error(`unsafe progress file path: ${file}`);
2943
3216
  }
2944
3217
  return path;
@@ -2951,7 +3224,7 @@ function buildEvidence(registered, statuses) {
2951
3224
  status: statuses.get(event.code) ?? "planned"
2952
3225
  }));
2953
3226
  }
2954
- function buildReportEvents(registered, statuses) {
3227
+ function buildReportEvents(registered, statuses, failureReasons = /* @__PURE__ */ new Map()) {
2955
3228
  return registered.map((event) => {
2956
3229
  const status = statuses.get(event.code);
2957
3230
  const wired = status !== void 0 && status !== "failed" && status !== "unsupported" && status !== "planned";
@@ -2959,7 +3232,7 @@ function buildReportEvents(registered, statuses) {
2959
3232
  event_type: event.code,
2960
3233
  status: wired ? "wired" : "skipped",
2961
3234
  ...wired ? { file: event.anchorFile } : {},
2962
- ...wired ? {} : { reason: status ?? "not integrated" }
3235
+ ...wired ? {} : { reason: failureReasons.get(event.code) ?? status ?? "not integrated" }
2963
3236
  };
2964
3237
  });
2965
3238
  }
@@ -2991,7 +3264,7 @@ import {
2991
3264
  // src/core/git.ts
2992
3265
  import { spawn as spawn3 } from "child_process";
2993
3266
  import { createHash as createHash3 } from "crypto";
2994
- import { readFile as readFile3 } from "fs/promises";
3267
+ import { readFile as readFile4 } from "fs/promises";
2995
3268
  import { basename as basename2, join as join5 } from "path";
2996
3269
  async function takeCheckpoint(repoPath) {
2997
3270
  const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
@@ -3058,7 +3331,7 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
3058
3331
  const contents = [];
3059
3332
  for (const file of files) {
3060
3333
  try {
3061
- contents.push({ file, content: await readFile3(join5(repoPath, file), "utf8") });
3334
+ contents.push({ file, content: await readFile4(join5(repoPath, file), "utf8") });
3062
3335
  } catch {
3063
3336
  continue;
3064
3337
  }
@@ -3088,14 +3361,14 @@ function escapeRegExp(s) {
3088
3361
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3089
3362
  }
3090
3363
  function run(cwd, args) {
3091
- return new Promise((resolve6) => {
3364
+ return new Promise((resolve7) => {
3092
3365
  const child = spawn3("git", args, { cwd });
3093
3366
  let stdout = "";
3094
3367
  let stderr = "";
3095
3368
  child.stdout.on("data", (d) => stdout += d.toString());
3096
3369
  child.stderr.on("data", (d) => stderr += d.toString());
3097
- child.on("close", (code) => resolve6({ ok: code === 0, stdout, stderr }));
3098
- child.on("error", () => resolve6({ ok: false, stdout, stderr }));
3370
+ child.on("close", (code) => resolve7({ ok: code === 0, stdout, stderr }));
3371
+ child.on("error", () => resolve7({ ok: false, stdout, stderr }));
3099
3372
  });
3100
3373
  }
3101
3374
 
@@ -3334,7 +3607,7 @@ function coverageNote(coverage) {
3334
3607
  }
3335
3608
 
3336
3609
  // src/core/toolPolicy.ts
3337
- import { isAbsolute, relative as relative2, resolve as resolve3 } from "path";
3610
+ import { isAbsolute, relative as relative2, resolve as resolve4 } from "path";
3338
3611
  var SECRET_MATERIAL_DENIAL = "blocked: secret material must not be accessed";
3339
3612
  var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
3340
3613
  var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
@@ -3430,8 +3703,8 @@ function isSecretPathLike(value) {
3430
3703
  }
3431
3704
  function isPathInsideRepo(pathValue, repoPath) {
3432
3705
  if (pathValue.includes("..")) return false;
3433
- const repoRoot = resolve3(repoPath);
3434
- const candidate = isAbsolute(pathValue) ? resolve3(pathValue) : resolve3(repoRoot, pathValue);
3706
+ const repoRoot = resolve4(repoPath);
3707
+ const candidate = isAbsolute(pathValue) ? resolve4(pathValue) : resolve4(repoRoot, pathValue);
3435
3708
  const rel = relative2(repoRoot, candidate);
3436
3709
  return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
3437
3710
  }
@@ -3555,8 +3828,8 @@ function normalizePathLike(value) {
3555
3828
  return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
3556
3829
  }
3557
3830
  function repoRelativePath(pathValue, repoPath) {
3558
- const repoRoot = resolve3(repoPath);
3559
- const candidate = isAbsolute(pathValue) ? resolve3(pathValue) : resolve3(repoRoot, pathValue);
3831
+ const repoRoot = resolve4(repoPath);
3832
+ const candidate = isAbsolute(pathValue) ? resolve4(pathValue) : resolve4(repoRoot, pathValue);
3560
3833
  return normalizePathLike(relative2(repoRoot, candidate));
3561
3834
  }
3562
3835
  function isSensitiveWritePath(pathValue, repoPath) {
@@ -4525,8 +4798,12 @@ function supportsClaudeFastMode(model) {
4525
4798
  return FAST_MODE_MODELS.has(model);
4526
4799
  }
4527
4800
  function createClaudeProvider(config, session) {
4801
+ let runId = "";
4528
4802
  return {
4529
4803
  name: "claude",
4804
+ onRunReady(readyRunId) {
4805
+ runId = readyRunId;
4806
+ },
4530
4807
  async invoke(invocation, roleConfig, onProgress) {
4531
4808
  applyModelAuthEnv(config, session);
4532
4809
  const hostTools = invocation.tools.map(
@@ -4546,6 +4823,17 @@ function createClaudeProvider(config, session) {
4546
4823
  const deadline = Date.now() + roleConfig.maxMs;
4547
4824
  const outcome = (kind, text = "", error = "") => kind === "completed" ? { kind, text, sessionId, costUsd, turns } : kind === "failed" ? { kind, error, sessionId, costUsd, turns } : { kind, sessionId, costUsd, turns };
4548
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");
4549
4837
  const response = query2({
4550
4838
  prompt: invocation.userPrompt,
4551
4839
  options: {
@@ -4559,9 +4847,13 @@ function createClaudeProvider(config, session) {
4559
4847
  hooks: buildToolPolicyHooks(invocation.cwd, trustedTools),
4560
4848
  canUseTool: createToolPermissionCallback(invocation.cwd, trustedTools),
4561
4849
  maxTurns: roleConfig.maxTurns,
4562
- ...supportsClaudeFastMode(roleConfig.model) ? { settings: { fastMode: true } } : {},
4850
+ ...roleConfig.fastMode ?? supportsClaudeFastMode(roleConfig.model) ? { settings: { fastMode: true } } : {},
4563
4851
  ...invocation.resumeSessionId ? { resume: invocation.resumeSessionId } : {},
4564
- settingSources: []
4852
+ settingSources: [],
4853
+ env: {
4854
+ ...process.env,
4855
+ ANTHROPIC_CUSTOM_HEADERS: customHeaders
4856
+ }
4565
4857
  }
4566
4858
  });
4567
4859
  for await (const raw of response) {
@@ -4609,13 +4901,16 @@ function createClaudeProvider(config, session) {
4609
4901
  }
4610
4902
  };
4611
4903
  }
4904
+ function headerValue(value) {
4905
+ return value.replace(/[\r\n]/g, "").slice(0, 200);
4906
+ }
4612
4907
 
4613
4908
  // src/engine/providers/openai.ts
4614
4909
  import { readdirSync, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
4615
- import { join as join6, resolve as resolve4, sep as sep3 } from "path";
4910
+ import { join as join6, resolve as resolve5, sep as sep4 } from "path";
4616
4911
  function jailedPath(cwd, candidate) {
4617
- const resolved = resolve4(cwd, candidate);
4618
- if (resolved !== cwd && !resolved.startsWith(cwd + sep3)) {
4912
+ const resolved = resolve5(cwd, candidate);
4913
+ if (resolved !== cwd && !resolved.startsWith(cwd + sep4)) {
4619
4914
  throw new Error(`path ${candidate} escapes the repository`);
4620
4915
  }
4621
4916
  return resolved;
@@ -4715,8 +5010,9 @@ function fileTools(cwd, allowWrite) {
4715
5010
  }
4716
5011
  function createOpenAIProvider(config, session) {
4717
5012
  let runId;
4718
- let conversationId;
4719
- const gatewayFetch = async (path, body) => {
5013
+ const conversationIds = /* @__PURE__ */ new Map();
5014
+ let legacyConversationId;
5015
+ const gatewayFetch = async (path, body, sessionKey, task) => {
4720
5016
  if (!runId) {
4721
5017
  throw new Error("OpenAI provider used before the run was created");
4722
5018
  }
@@ -4726,7 +5022,11 @@ function createOpenAIProvider(config, session) {
4726
5022
  method: "POST",
4727
5023
  headers: {
4728
5024
  "Content-Type": "application/json",
4729
- 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
+ }
4730
5030
  },
4731
5031
  body: JSON.stringify(body)
4732
5032
  }
@@ -4739,44 +5039,61 @@ function createOpenAIProvider(config, session) {
4739
5039
  payload = {};
4740
5040
  }
4741
5041
  if (!response.ok) {
4742
- 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;
4743
5045
  const error = new Error(detail || `gateway request failed (${response.status})`);
4744
- error.code = payload.code;
5046
+ error.code = top.code ?? nested?.code;
4745
5047
  throw error;
4746
5048
  }
4747
5049
  return payload;
4748
5050
  };
4749
- const ensureConversation = async (resumeId) => {
4750
- if (conversationId) {
4751
- return conversationId;
5051
+ const ensureConversation = async (sessionKey, task, resumeId) => {
5052
+ const existing = sessionKey === "legacy" ? legacyConversationId : conversationIds.get(sessionKey);
5053
+ if (existing) {
5054
+ return existing;
4752
5055
  }
4753
5056
  if (resumeId) {
4754
- conversationId = resumeId;
5057
+ if (sessionKey === "legacy") {
5058
+ legacyConversationId = resumeId;
5059
+ } else {
5060
+ conversationIds.set(sessionKey, resumeId);
5061
+ }
4755
5062
  return resumeId;
4756
5063
  }
4757
5064
  try {
4758
- const created = await gatewayFetch("/v1/conversations", {});
4759
- 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
+ }
4760
5073
  } catch (error) {
4761
5074
  if (error.code !== "conversation_already_bound") {
4762
5075
  throw error;
4763
5076
  }
4764
5077
  }
4765
- if (!conversationId) {
5078
+ const createdId = sessionKey === "legacy" ? legacyConversationId : conversationIds.get(sessionKey);
5079
+ if (!createdId) {
4766
5080
  throw new Error("wizard run already has a bound conversation; rerun with its resume state");
4767
5081
  }
4768
- return conversationId;
5082
+ return createdId;
4769
5083
  };
4770
5084
  return {
4771
5085
  name: "openai-gateway",
4772
5086
  onRunReady(readyRunId, resumeSessionId) {
4773
5087
  runId = readyRunId;
4774
- conversationId = resumeSessionId;
5088
+ legacyConversationId = resumeSessionId;
4775
5089
  },
4776
5090
  async invoke(invocation, roleConfig, onProgress) {
4777
5091
  let costUsd = 0;
4778
5092
  let turns = 0;
4779
- 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 };
4780
5097
  const allTools = [...invocation.tools, ...fileTools(invocation.cwd, invocation.allowRepoWrite)];
4781
5098
  const toolsByName = new Map(allTools.map((tool2) => [tool2.name, tool2]));
4782
5099
  const toolDefs = allTools.map((tool2) => ({
@@ -4786,7 +5103,12 @@ function createOpenAIProvider(config, session) {
4786
5103
  parameters: tool2.jsonSchema
4787
5104
  }));
4788
5105
  try {
4789
- const conversation = await ensureConversation(invocation.resumeSessionId);
5106
+ const conversation = await ensureConversation(
5107
+ sessionKey,
5108
+ task,
5109
+ invocation.resumeSessionId
5110
+ );
5111
+ activeConversationId = conversation;
4790
5112
  const deadline = Date.now() + roleConfig.maxMs;
4791
5113
  let input = [
4792
5114
  { role: "user", content: invocation.userPrompt }
@@ -4802,8 +5124,9 @@ function createOpenAIProvider(config, session) {
4802
5124
  conversation,
4803
5125
  input,
4804
5126
  tools: toolDefs,
4805
- ...roleConfig.effort ? { reasoning: { effort: roleConfig.effort } } : {}
4806
- });
5127
+ ...roleConfig.effort ? { reasoning: { effort: roleConfig.effort } } : {},
5128
+ ...roleConfig.serviceTier ? { service_tier: roleConfig.serviceTier } : {}
5129
+ }, sessionKey, task);
4807
5130
  const items = response.output ?? [];
4808
5131
  const calls = items.filter(
4809
5132
  (item) => item.type === "function_call"
@@ -4849,18 +5172,102 @@ function createOpenAIProvider(config, session) {
4849
5172
  }
4850
5173
 
4851
5174
  // src/engine/providers/router.ts
4852
- function createRoutedProvider(routes, fallback) {
4853
- 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));
4854
5228
  return {
4855
5229
  name: "routed",
4856
5230
  onRunReady(runId, resumeSessionId) {
5231
+ disabledProviders.clear();
4857
5232
  for (const provider of providers) {
4858
5233
  provider.onRunReady?.(runId, resumeSessionId);
4859
5234
  }
4860
5235
  },
4861
5236
  invoke(invocation, config, onProgress) {
4862
- const provider = routes[invocation.role] ?? fallback;
4863
- 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);
4864
5271
  }
4865
5272
  };
4866
5273
  }
@@ -4868,22 +5275,45 @@ function createRoutedProvider(routes, fallback) {
4868
5275
  // src/engine/cli.ts
4869
5276
  var MINUTE = 6e4;
4870
5277
  function engineModelConfig(config) {
4871
- 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 };
4872
5288
  return {
4873
- survey: { model: model("survey", config.model), effort: "medium", maxTurns: 40, maxMs: 12 * MINUTE },
4874
- design: { model: model("design", config.plannerModel), effort: "high", maxTurns: 40, maxMs: 15 * MINUTE },
4875
- edit: { model: model("edit", config.model), effort: config.effort, maxTurns: 50, maxMs: 15 * MINUTE },
4876
- 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
4877
5296
  };
4878
5297
  }
4879
5298
  function engineProvider(config, session) {
4880
5299
  const claude = createClaudeProvider(config, session);
4881
- 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);
4882
5306
  if (preset === "sol-design") {
4883
- const openai = createOpenAIProvider(config, session);
4884
- 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);
4885
5312
  }
4886
- 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);
4887
5317
  }
4888
5318
  async function tryEngineFlow(options) {
4889
5319
  const { repoPath, fingerprint, config, session, playbook, theme: theme2 } = options;
@@ -5200,7 +5630,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5200
5630
  return Promise.resolve({ ran: false, ok: true, output: "" });
5201
5631
  }
5202
5632
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
5203
- return new Promise((resolve6) => {
5633
+ return new Promise((resolve7) => {
5204
5634
  const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
5205
5635
  let out = "";
5206
5636
  const append = (d) => {
@@ -5211,11 +5641,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5211
5641
  child.stderr?.on("data", append);
5212
5642
  const timer = setTimeout(() => {
5213
5643
  child.kill("SIGKILL");
5214
- resolve6({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
5644
+ resolve7({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
5215
5645
  }, timeoutMs);
5216
5646
  child.on("close", (code) => {
5217
5647
  clearTimeout(timer);
5218
- resolve6({
5648
+ resolve7({
5219
5649
  ran: true,
5220
5650
  ok: code === 0,
5221
5651
  command,
@@ -5227,7 +5657,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
5227
5657
  });
5228
5658
  child.on("error", () => {
5229
5659
  clearTimeout(timer);
5230
- resolve6({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
5660
+ resolve7({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
5231
5661
  });
5232
5662
  });
5233
5663
  }
@@ -5238,11 +5668,11 @@ function tail2(s) {
5238
5668
 
5239
5669
  // src/core/version.ts
5240
5670
  import { readFileSync as readFileSync3 } from "fs";
5241
- import { dirname as dirname4, join as join7, parse } from "path";
5671
+ import { dirname as dirname3, join as join7, parse } from "path";
5242
5672
  import { fileURLToPath } from "url";
5243
5673
  var PACKAGE_NAME = "@whisperr/wizard";
5244
5674
  function packageVersion() {
5245
- let dir = dirname4(fileURLToPath(import.meta.url));
5675
+ let dir = dirname3(fileURLToPath(import.meta.url));
5246
5676
  const { root } = parse(dir);
5247
5677
  while (true) {
5248
5678
  try {
@@ -5251,7 +5681,7 @@ function packageVersion() {
5251
5681
  } catch {
5252
5682
  }
5253
5683
  if (dir === root) return "0.0.0";
5254
- dir = dirname4(dir);
5684
+ dir = dirname3(dir);
5255
5685
  }
5256
5686
  }
5257
5687
 
@@ -5384,7 +5814,7 @@ function banner() {
5384
5814
 
5385
5815
  // src/cli.ts
5386
5816
  async function run2(options) {
5387
- const repoPath = resolve5(options.path ?? process.cwd());
5817
+ const repoPath = resolve6(options.path ?? process.cwd());
5388
5818
  const config = resolveConfig(options);
5389
5819
  console.log(banner());
5390
5820
  p2.intro(theme.signal("Let's wire Whisperr into your app."));