@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.
- package/README.md +13 -7
- package/dist/index.js +652 -222
- 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
|
|
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
|
|
1312
|
-
import { basename, resolve as
|
|
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
|
-
|
|
1519
|
-
var
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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
|
|
1533
|
-
for (const
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
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
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
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((
|
|
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
|
-
|
|
1713
|
+
resolve7({ ...step, ok: false, output: String(error) });
|
|
1692
1714
|
});
|
|
1693
1715
|
child.on("close", (exitCode) => {
|
|
1694
1716
|
clearTimeout(timer);
|
|
1695
|
-
|
|
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
|
|
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
|
|
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
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1901
|
-
|
|
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
|
|
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
|
|
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
|
|
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((
|
|
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", () =>
|
|
2452
|
-
child.on("close", (exitCode) =>
|
|
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 =
|
|
2464
|
-
if (path === repoPath || !path.startsWith(repoPath +
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
2940
|
-
const path =
|
|
2941
|
-
if (path === root || !path.startsWith(root +
|
|
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
|
|
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
|
|
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((
|
|
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) =>
|
|
3098
|
-
child.on("error", () =>
|
|
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
|
|
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 =
|
|
3434
|
-
const candidate = isAbsolute(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 =
|
|
3559
|
-
const candidate = isAbsolute(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
|
|
4910
|
+
import { join as join6, resolve as resolve5, sep as sep4 } from "path";
|
|
4616
4911
|
function jailedPath(cwd, candidate) {
|
|
4617
|
-
const resolved =
|
|
4618
|
-
if (resolved !== cwd && !resolved.startsWith(cwd +
|
|
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
|
-
|
|
4719
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
4751
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5082
|
+
return createdId;
|
|
4769
5083
|
};
|
|
4770
5084
|
return {
|
|
4771
5085
|
name: "openai-gateway",
|
|
4772
5086
|
onRunReady(readyRunId, resumeSessionId) {
|
|
4773
5087
|
runId = readyRunId;
|
|
4774
|
-
|
|
5088
|
+
legacyConversationId = resumeSessionId;
|
|
4775
5089
|
},
|
|
4776
5090
|
async invoke(invocation, roleConfig, onProgress) {
|
|
4777
5091
|
let costUsd = 0;
|
|
4778
5092
|
let turns = 0;
|
|
4779
|
-
const
|
|
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(
|
|
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
|
-
|
|
4853
|
-
|
|
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
|
|
4863
|
-
|
|
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 = (
|
|
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
|
|
4874
|
-
design
|
|
4875
|
-
edit:
|
|
4876
|
-
review
|
|
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
|
|
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
|
-
|
|
4884
|
-
|
|
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
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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."));
|