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