@whisperr/wizard 0.8.0 → 0.9.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 +17 -18
- package/dist/index.js +1465 -632
- 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 resolve7 } from "path";
|
|
9
9
|
import * as p2 from "@clack/prompts";
|
|
10
10
|
import open2 from "open";
|
|
11
11
|
|
|
@@ -1260,6 +1260,25 @@ async function pollFirstEvent(config, session, opts = {}) {
|
|
|
1260
1260
|
return { received: false };
|
|
1261
1261
|
}
|
|
1262
1262
|
|
|
1263
|
+
// src/core/version.ts
|
|
1264
|
+
import { readFileSync } from "fs";
|
|
1265
|
+
import { dirname, join as join2, parse } from "path";
|
|
1266
|
+
import { fileURLToPath } from "url";
|
|
1267
|
+
var PACKAGE_NAME = "@whisperr/wizard";
|
|
1268
|
+
function packageVersion() {
|
|
1269
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
1270
|
+
const { root } = parse(dir);
|
|
1271
|
+
while (true) {
|
|
1272
|
+
try {
|
|
1273
|
+
const pkg = JSON.parse(readFileSync(join2(dir, "package.json"), "utf8"));
|
|
1274
|
+
if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
|
|
1275
|
+
} catch {
|
|
1276
|
+
}
|
|
1277
|
+
if (dir === root) return "0.0.0";
|
|
1278
|
+
dir = dirname(dir);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1263
1282
|
// src/engine/output.ts
|
|
1264
1283
|
function formatProgressLine(event) {
|
|
1265
1284
|
const parts = [event.phase];
|
|
@@ -1308,8 +1327,8 @@ function createProgressSink(options) {
|
|
|
1308
1327
|
}
|
|
1309
1328
|
|
|
1310
1329
|
// src/engine/orchestrator.ts
|
|
1311
|
-
import { readFile as
|
|
1312
|
-
import { basename, resolve as
|
|
1330
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1331
|
+
import { basename, resolve as resolve4, sep as sep4 } from "path";
|
|
1313
1332
|
|
|
1314
1333
|
// src/engine/bindings.ts
|
|
1315
1334
|
function bindingsTargetFor(target9) {
|
|
@@ -1516,7 +1535,6 @@ function eventWrapperName(code) {
|
|
|
1516
1535
|
// src/engine/clusters.ts
|
|
1517
1536
|
import { createHash } from "crypto";
|
|
1518
1537
|
var MAX_CLUSTER_EVENTS = 4;
|
|
1519
|
-
var MAX_CONCURRENT_CLUSTERS = 4;
|
|
1520
1538
|
function clusterIdFor(files, eventIds = [], slicePosition = 0) {
|
|
1521
1539
|
const digest = createHash("sha256").update(JSON.stringify({
|
|
1522
1540
|
files: [...files].sort(),
|
|
@@ -1545,46 +1563,22 @@ function deriveClusters(placements) {
|
|
|
1545
1563
|
});
|
|
1546
1564
|
}
|
|
1547
1565
|
}
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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;
|
|
1566
|
+
return fileSlices.map((slice) => {
|
|
1567
|
+
const events = [...slice.events].sort((a, b) => a.eventCode.localeCompare(b.eventCode));
|
|
1568
|
+
return {
|
|
1569
|
+
id: clusterIdFor(
|
|
1570
|
+
[slice.file],
|
|
1571
|
+
events.map((event) => event.eventId || event.eventCode),
|
|
1572
|
+
slice.position
|
|
1573
|
+
),
|
|
1574
|
+
files: [slice.file],
|
|
1575
|
+
events,
|
|
1576
|
+
status: "pending",
|
|
1577
|
+
attempts: 0
|
|
1578
|
+
};
|
|
1579
|
+
});
|
|
1586
1580
|
}
|
|
1587
|
-
var TERMINAL_STATUSES = ["reviewed", "failed_resumable", "blocked"];
|
|
1581
|
+
var TERMINAL_STATUSES = ["verified", "reviewed", "failed_resumable", "blocked"];
|
|
1588
1582
|
function nextPendingCluster(clusters) {
|
|
1589
1583
|
return clusters.find((cluster) => cluster.status === "pending" || cluster.status === "editing");
|
|
1590
1584
|
}
|
|
@@ -1602,8 +1596,10 @@ function transitionCluster(clusters, clusterId, status, note3) {
|
|
|
1602
1596
|
}
|
|
1603
1597
|
|
|
1604
1598
|
// src/engine/integrate.ts
|
|
1605
|
-
import {
|
|
1606
|
-
import {
|
|
1599
|
+
import { createHash as createHash2 } from "crypto";
|
|
1600
|
+
import { readFile as readFile2, stat as stat2, writeFile } from "fs/promises";
|
|
1601
|
+
import { join as join4, resolve, sep } from "path";
|
|
1602
|
+
import { z } from "zod";
|
|
1607
1603
|
|
|
1608
1604
|
// src/engine/budgets.ts
|
|
1609
1605
|
var OVERFLOW_PATTERNS = [
|
|
@@ -1653,8 +1649,8 @@ function modelConfigFor(models, task, role) {
|
|
|
1653
1649
|
|
|
1654
1650
|
// src/engine/verify.ts
|
|
1655
1651
|
import { spawn } from "child_process";
|
|
1656
|
-
import { readFileSync } from "fs";
|
|
1657
|
-
import { join as
|
|
1652
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1653
|
+
import { join as join3 } from "path";
|
|
1658
1654
|
var JS_TARGETS = /* @__PURE__ */ new Set(["web-js", "nextjs", "node", "react-native"]);
|
|
1659
1655
|
function resolveVerifySteps(repoPath, target9) {
|
|
1660
1656
|
if (target9 === "flutter") {
|
|
@@ -1668,7 +1664,10 @@ function resolveVerifySteps(repoPath, target9) {
|
|
|
1668
1664
|
} else if (hasTsconfig(repoPath)) {
|
|
1669
1665
|
steps.push({ kind: "typecheck", command: "npx", args: ["--no-install", "tsc", "--noEmit"] });
|
|
1670
1666
|
}
|
|
1671
|
-
if (
|
|
1667
|
+
if (scripts.lint) {
|
|
1668
|
+
steps.push({ kind: "lint", command: npmCommand(), args: ["run", "lint"] });
|
|
1669
|
+
}
|
|
1670
|
+
if (scripts.build) {
|
|
1672
1671
|
steps.push({ kind: "build", command: npmCommand(), args: ["run", "build"] });
|
|
1673
1672
|
}
|
|
1674
1673
|
return steps;
|
|
@@ -1677,7 +1676,7 @@ function resolveVerifySteps(repoPath, target9) {
|
|
|
1677
1676
|
}
|
|
1678
1677
|
function packageScripts(repoPath) {
|
|
1679
1678
|
try {
|
|
1680
|
-
const parsed = JSON.parse(
|
|
1679
|
+
const parsed = JSON.parse(readFileSync2(join3(repoPath, "package.json"), "utf8"));
|
|
1681
1680
|
return parsed.scripts ?? {};
|
|
1682
1681
|
} catch {
|
|
1683
1682
|
return {};
|
|
@@ -1685,7 +1684,7 @@ function packageScripts(repoPath) {
|
|
|
1685
1684
|
}
|
|
1686
1685
|
function hasTsconfig(repoPath) {
|
|
1687
1686
|
try {
|
|
1688
|
-
|
|
1687
|
+
readFileSync2(join3(repoPath, "tsconfig.json"), "utf8");
|
|
1689
1688
|
return true;
|
|
1690
1689
|
} catch {
|
|
1691
1690
|
return false;
|
|
@@ -1695,7 +1694,7 @@ function npmCommand() {
|
|
|
1695
1694
|
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
1696
1695
|
}
|
|
1697
1696
|
function runVerifyStep(cwd, step, timeoutMs = 3e5) {
|
|
1698
|
-
return new Promise((
|
|
1697
|
+
return new Promise((resolve8) => {
|
|
1699
1698
|
const child = spawn(step.command, step.args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
1700
1699
|
let output = "";
|
|
1701
1700
|
const capture = (chunk) => {
|
|
@@ -1710,11 +1709,11 @@ function runVerifyStep(cwd, step, timeoutMs = 3e5) {
|
|
|
1710
1709
|
}, timeoutMs);
|
|
1711
1710
|
child.on("error", (error) => {
|
|
1712
1711
|
clearTimeout(timer);
|
|
1713
|
-
|
|
1712
|
+
resolve8({ ...step, ok: false, output: String(error) });
|
|
1714
1713
|
});
|
|
1715
1714
|
child.on("close", (exitCode) => {
|
|
1716
1715
|
clearTimeout(timer);
|
|
1717
|
-
|
|
1716
|
+
resolve8({ ...step, ok: exitCode === 0, output });
|
|
1718
1717
|
});
|
|
1719
1718
|
});
|
|
1720
1719
|
}
|
|
@@ -1747,36 +1746,116 @@ var REVIEW_CHECKLIST = [
|
|
|
1747
1746
|
].join("\n");
|
|
1748
1747
|
async function writeBindingsModule(worktree, target9, registered) {
|
|
1749
1748
|
const bindings = generateBindings(target9, registered);
|
|
1750
|
-
await writeFile(
|
|
1749
|
+
await writeFile(join4(worktree.path, bindings.relativePath), bindings.content);
|
|
1751
1750
|
return bindings;
|
|
1752
1751
|
}
|
|
1753
|
-
async function runSdkSetupPass(deps, bindings) {
|
|
1752
|
+
async function runSdkSetupPass(deps, bindings, validate) {
|
|
1754
1753
|
deps.sink.emit({ phase: "binding", action: "installing SDK and wiring identify()" });
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1754
|
+
const allowedFiles = await sdkSetupFiles(deps.worktree.path, deps.survey, bindings.relativePath);
|
|
1755
|
+
const invocation = {
|
|
1756
|
+
role: "edit",
|
|
1757
|
+
task: "sdk_setup",
|
|
1758
|
+
sessionKey: "sdk_setup",
|
|
1759
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
1760
|
+
userPrompt: [
|
|
1761
|
+
"CORE SETUP for the Whisperr SDK in this repository. Do exactly this, then stop:",
|
|
1762
|
+
"1. Add and install the Whisperr SDK dependency for this stack.",
|
|
1763
|
+
`2. Initialize it once at app startup with API key ${deps.ingestion.apiKey} and base URL ${deps.ingestion.baseUrl}.`,
|
|
1764
|
+
`3. The deterministic bindings module at ${bindings.relativePath} queues events until bound.`,
|
|
1765
|
+
` ${bindings.bindInstruction}`,
|
|
1766
|
+
" Never edit the generated bindings content.",
|
|
1767
|
+
"4. Wire identify() for the END USER (paying customer) at login success AND session restoration.",
|
|
1768
|
+
"5. Wire reset() at every end-user logout anchor.",
|
|
1769
|
+
"Never instrument admin/staff/operator paths. Do not insert any product event calls. Do not run builds.",
|
|
1770
|
+
"",
|
|
1771
|
+
`Saved stack and identity anchors:
|
|
1772
|
+
${JSON.stringify({
|
|
1773
|
+
stack: deps.survey?.stack,
|
|
1774
|
+
identifyAnchors: deps.survey?.identifyAnchors ?? [],
|
|
1775
|
+
logoutAnchors: deps.survey?.logoutAnchors ?? []
|
|
1776
|
+
})}`,
|
|
1777
|
+
"",
|
|
1778
|
+
`You may access only:
|
|
1779
|
+
${allowedFiles.map((file) => `- ${file}`).join("\n")}`
|
|
1780
|
+
].join("\n"),
|
|
1781
|
+
tools: [],
|
|
1782
|
+
repositoryAccess: { mode: "sdk_setup", allowedFiles },
|
|
1783
|
+
allowRepoWrite: true,
|
|
1784
|
+
cwd: deps.worktree.path
|
|
1785
|
+
};
|
|
1786
|
+
let outcome = await deps.provider.invoke(
|
|
1787
|
+
invocation,
|
|
1776
1788
|
modelConfigFor(deps.models, "sdk_setup", "edit"),
|
|
1777
1789
|
(action) => deps.sink.emit({ phase: "binding", action })
|
|
1778
1790
|
);
|
|
1779
|
-
|
|
1791
|
+
const passesValidation = async () => {
|
|
1792
|
+
if (!validate) return true;
|
|
1793
|
+
try {
|
|
1794
|
+
return await validate();
|
|
1795
|
+
} catch {
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
let valid = outcome.kind === "completed" && await passesValidation();
|
|
1800
|
+
if (!valid && deps.provider.invokeFallback) {
|
|
1801
|
+
await deps.discardChanges();
|
|
1802
|
+
await writeFile(join4(deps.worktree.path, bindings.relativePath), bindings.content);
|
|
1803
|
+
deps.sink.emit({ phase: "binding", action: "Opus setup rolled back; retrying with Sol" });
|
|
1804
|
+
outcome = await deps.provider.invokeFallback(
|
|
1805
|
+
{ ...invocation, sessionKey: "sdk_setup:sol" },
|
|
1806
|
+
modelConfigFor(deps.models, "repair", "edit"),
|
|
1807
|
+
(action) => deps.sink.emit({ phase: "binding", action })
|
|
1808
|
+
);
|
|
1809
|
+
valid = outcome.kind === "completed" && await passesValidation();
|
|
1810
|
+
}
|
|
1811
|
+
return valid;
|
|
1812
|
+
}
|
|
1813
|
+
async function sdkSetupFiles(repoPath, survey, bindingsFile) {
|
|
1814
|
+
const candidates = /* @__PURE__ */ new Set([
|
|
1815
|
+
bindingsFile,
|
|
1816
|
+
...survey?.stack.entryPoints ?? [],
|
|
1817
|
+
...survey?.identifyAnchors.map((anchor) => anchor.file) ?? [],
|
|
1818
|
+
...survey?.logoutAnchors.map((anchor) => anchor.file) ?? [],
|
|
1819
|
+
...survey?.featureAreas.flatMap(
|
|
1820
|
+
(area) => area.files.filter(isSdkConfigurationFile)
|
|
1821
|
+
) ?? [],
|
|
1822
|
+
"package.json",
|
|
1823
|
+
"package-lock.json",
|
|
1824
|
+
"pnpm-lock.yaml",
|
|
1825
|
+
"yarn.lock",
|
|
1826
|
+
"bun.lock",
|
|
1827
|
+
"bun.lockb",
|
|
1828
|
+
"pyproject.toml",
|
|
1829
|
+
"requirements.txt",
|
|
1830
|
+
"Pipfile",
|
|
1831
|
+
"Package.swift",
|
|
1832
|
+
"Podfile",
|
|
1833
|
+
"pubspec.yaml",
|
|
1834
|
+
"go.mod",
|
|
1835
|
+
"composer.json",
|
|
1836
|
+
"tsconfig.json",
|
|
1837
|
+
"next.config.js",
|
|
1838
|
+
"next.config.mjs",
|
|
1839
|
+
"next.config.ts",
|
|
1840
|
+
"vite.config.js",
|
|
1841
|
+
"vite.config.ts",
|
|
1842
|
+
"app.json",
|
|
1843
|
+
"Info.plist"
|
|
1844
|
+
]);
|
|
1845
|
+
const available = [];
|
|
1846
|
+
for (const file of candidates) {
|
|
1847
|
+
try {
|
|
1848
|
+
if ((await stat2(join4(repoPath, file))).isFile()) available.push(file.replaceAll("\\", "/"));
|
|
1849
|
+
} catch {
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return [...new Set(available)].sort();
|
|
1853
|
+
}
|
|
1854
|
+
function isSdkConfigurationFile(file) {
|
|
1855
|
+
const normalized = file.replaceAll("\\", "/");
|
|
1856
|
+
return /(^|\/)(?:[^/]*config[^/]*|settings[^/]*|Info\.plist|AndroidManifest\.xml|Podfile|pubspec\.yaml)$/.test(
|
|
1857
|
+
normalized
|
|
1858
|
+
);
|
|
1780
1859
|
}
|
|
1781
1860
|
function clusterPrompt(cluster, registered, bindings) {
|
|
1782
1861
|
const byCode = new Map(registered.map((event) => [event.code, event]));
|
|
@@ -1803,39 +1882,7 @@ function clusterPrompt(cluster, registered, bindings) {
|
|
|
1803
1882
|
"Only edit the listed files (plus imports of the bindings module). Then stop."
|
|
1804
1883
|
].join("\n");
|
|
1805
1884
|
}
|
|
1806
|
-
async function
|
|
1807
|
-
const outcome = await deps.provider.invoke(
|
|
1808
|
-
{
|
|
1809
|
-
role: "review",
|
|
1810
|
-
task: "review",
|
|
1811
|
-
sessionKey: `review:${cluster.id}`,
|
|
1812
|
-
systemPrompt: [
|
|
1813
|
-
"You review an instrumentation diff against a strict checklist. Answer with a verdict line",
|
|
1814
|
-
"`VERDICT: pass` or `VERDICT: fail`, then bullet notes for anything wrong."
|
|
1815
|
-
].join("\n"),
|
|
1816
|
-
userPrompt: [
|
|
1817
|
-
"Checklist:",
|
|
1818
|
-
REVIEW_CHECKLIST,
|
|
1819
|
-
"",
|
|
1820
|
-
`Events expected: ${cluster.events.map((event) => event.eventCode).join(", ")}`,
|
|
1821
|
-
"",
|
|
1822
|
-
"Diff:",
|
|
1823
|
-
diff.length > 6e4 ? diff.slice(0, 6e4) + "\n\u2026(truncated)" : diff
|
|
1824
|
-
].join("\n"),
|
|
1825
|
-
tools: [],
|
|
1826
|
-
allowRepoWrite: false,
|
|
1827
|
-
cwd: deps.worktree.path
|
|
1828
|
-
},
|
|
1829
|
-
modelConfigFor(deps.models, "review", "review"),
|
|
1830
|
-
(action) => deps.sink.emit({ phase: "reviewing", action })
|
|
1831
|
-
);
|
|
1832
|
-
if (outcome.kind !== "completed") {
|
|
1833
|
-
return { ok: false, notes: `review pass did not complete (${outcome.kind})` };
|
|
1834
|
-
}
|
|
1835
|
-
const ok = /verdict:\s*pass/i.test(outcome.text);
|
|
1836
|
-
return { ok, notes: outcome.text.trim() };
|
|
1837
|
-
}
|
|
1838
|
-
async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
1885
|
+
async function runClusterIntegration(deps, initialClusters, _clusterDiff) {
|
|
1839
1886
|
let clusters = initialClusters;
|
|
1840
1887
|
const eventStatuses = /* @__PURE__ */ new Map();
|
|
1841
1888
|
const bindings = generateBindings(deps.target, deps.registered);
|
|
@@ -1861,6 +1908,7 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
|
1861
1908
|
systemPrompt: deps.playbookSystemPrompt,
|
|
1862
1909
|
userPrompt: clusterPrompt(cluster, deps.registered, bindings),
|
|
1863
1910
|
tools: [],
|
|
1911
|
+
repositoryAccess: { mode: "cluster", allowedFiles: cluster.files },
|
|
1864
1912
|
allowRepoWrite: true,
|
|
1865
1913
|
cwd: deps.worktree.path
|
|
1866
1914
|
};
|
|
@@ -1869,22 +1917,20 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
|
1869
1917
|
modelConfigFor(deps.models, "cluster_edit", "edit"),
|
|
1870
1918
|
(action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
|
|
1871
1919
|
);
|
|
1872
|
-
let escalated = false;
|
|
1873
1920
|
let structural = edit.kind === "completed" ? await structuralClusterCheck(deps.worktree.path, cluster) : { ok: false, notes: `edit ${edit.kind}` };
|
|
1874
1921
|
if ((!structural.ok || edit.kind !== "completed") && deps.provider.invokeFallback) {
|
|
1875
1922
|
await deps.discardChanges();
|
|
1876
|
-
escalated = true;
|
|
1877
1923
|
deps.sink.emit({
|
|
1878
1924
|
phase: "integrating",
|
|
1879
1925
|
cluster: { index, total },
|
|
1880
|
-
action: "Terra attempt rolled back; retrying with
|
|
1926
|
+
action: "Terra attempt rolled back; retrying the clean cluster with Sol"
|
|
1881
1927
|
});
|
|
1882
1928
|
edit = await deps.provider.invokeFallback(
|
|
1883
|
-
{ ...invocation, task: "repair", sessionKey: `
|
|
1884
|
-
modelConfigFor(deps.models, "
|
|
1929
|
+
{ ...invocation, task: "repair", sessionKey: `cluster_sol:${cluster.id}` },
|
|
1930
|
+
modelConfigFor(deps.models, "repair", "edit"),
|
|
1885
1931
|
(action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
|
|
1886
1932
|
);
|
|
1887
|
-
structural = edit.kind === "completed" ? await structuralClusterCheck(deps.worktree.path, cluster) : { ok: false, notes: `
|
|
1933
|
+
structural = edit.kind === "completed" ? await structuralClusterCheck(deps.worktree.path, cluster) : { ok: false, notes: `Sol escalation ${edit.kind}` };
|
|
1888
1934
|
}
|
|
1889
1935
|
if (edit.kind !== "completed" || !structural.ok) {
|
|
1890
1936
|
const failure = edit.kind === "context_overflow" ? "context_overflow" : edit.kind === "budget_exhausted" ? "context_overflow" : classifyFailure(edit.kind === "failed" ? edit.error : "");
|
|
@@ -1902,44 +1948,9 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
|
1902
1948
|
await deps.discardChanges();
|
|
1903
1949
|
continue;
|
|
1904
1950
|
}
|
|
1951
|
+
const finalStatus = "syntax_verified";
|
|
1905
1952
|
clusters = transitionCluster(clusters, cluster.id, "verified");
|
|
1906
1953
|
deps.saveClusters(clusters);
|
|
1907
|
-
const diff = await clusterDiff(cluster.files);
|
|
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;
|
|
1941
|
-
clusters = transitionCluster(clusters, cluster.id, "reviewed", review.ok ? void 0 : review.notes.slice(0, 500));
|
|
1942
|
-
deps.saveClusters(clusters);
|
|
1943
1954
|
for (const placement of cluster.events) {
|
|
1944
1955
|
eventStatuses.set(placement.eventCode, finalStatus);
|
|
1945
1956
|
}
|
|
@@ -1966,9 +1977,19 @@ async function structuralClusterCheck(repoPath, cluster) {
|
|
|
1966
1977
|
continue;
|
|
1967
1978
|
}
|
|
1968
1979
|
const content = await readFile2(path, "utf8");
|
|
1969
|
-
|
|
1980
|
+
const wrapper = eventWrapperName(placement.eventCode);
|
|
1981
|
+
if (!content.includes(wrapper)) {
|
|
1970
1982
|
missing.push(`${placement.eventCode}: wrapper call missing from ${placement.file}`);
|
|
1971
1983
|
}
|
|
1984
|
+
if (content.includes("<<<<<<<") || content.includes(">>>>>>>")) {
|
|
1985
|
+
missing.push(`${placement.eventCode}: merge conflict marker in ${placement.file}`);
|
|
1986
|
+
}
|
|
1987
|
+
if (/\.(?:ts|tsx|js|jsx|swift|dart|kt|java|go|py|php)$/.test(placement.file) && !balancedSourceDelimiters(content)) {
|
|
1988
|
+
missing.push(`${placement.eventCode}: basic syntax delimiters are unbalanced in ${placement.file}`);
|
|
1989
|
+
}
|
|
1990
|
+
if (/\.(?:ts|tsx|js|jsx)$/.test(placement.file) && content.includes("WhisperrEvents.") && !/import[\s\S]{0,300}\bWhisperrEvents\b/.test(content)) {
|
|
1991
|
+
missing.push(`${placement.eventCode}: WhisperrEvents import is missing from ${placement.file}`);
|
|
1992
|
+
}
|
|
1972
1993
|
} catch {
|
|
1973
1994
|
missing.push(`${placement.eventCode}: cannot read ${placement.file}`);
|
|
1974
1995
|
}
|
|
@@ -1978,25 +1999,221 @@ async function structuralClusterCheck(repoPath, cluster) {
|
|
|
1978
1999
|
notes: missing.join("; ")
|
|
1979
2000
|
};
|
|
1980
2001
|
}
|
|
1981
|
-
|
|
1982
|
-
const
|
|
2002
|
+
function balancedSourceDelimiters(content) {
|
|
2003
|
+
const stack = [];
|
|
2004
|
+
const pairs = { ")": "(", "]": "[", "}": "{" };
|
|
2005
|
+
let quote = "";
|
|
2006
|
+
let escaped = false;
|
|
2007
|
+
let lineComment = false;
|
|
2008
|
+
let blockComment = false;
|
|
2009
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
2010
|
+
const char = content[index];
|
|
2011
|
+
const next = content[index + 1] ?? "";
|
|
2012
|
+
if (lineComment) {
|
|
2013
|
+
if (char === "\n") lineComment = false;
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
if (blockComment) {
|
|
2017
|
+
if (char === "*" && next === "/") {
|
|
2018
|
+
blockComment = false;
|
|
2019
|
+
index += 1;
|
|
2020
|
+
}
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
if (quote) {
|
|
2024
|
+
if (escaped) {
|
|
2025
|
+
escaped = false;
|
|
2026
|
+
} else if (char === "\\") {
|
|
2027
|
+
escaped = true;
|
|
2028
|
+
} else if (char === quote) {
|
|
2029
|
+
quote = "";
|
|
2030
|
+
}
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
if (char === "/" && next === "/") {
|
|
2034
|
+
lineComment = true;
|
|
2035
|
+
index += 1;
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
if (char === "/" && next === "*") {
|
|
2039
|
+
blockComment = true;
|
|
2040
|
+
index += 1;
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
2044
|
+
quote = char;
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2047
|
+
if (char === "(" || char === "[" || char === "{") stack.push(char);
|
|
2048
|
+
if (pairs[char] && stack.pop() !== pairs[char]) return false;
|
|
2049
|
+
}
|
|
2050
|
+
return stack.length === 0 && !quote && !blockComment;
|
|
2051
|
+
}
|
|
2052
|
+
async function reviewIntegratedFile(deps, file, events, sessionSuffix = "") {
|
|
2053
|
+
let verdict;
|
|
2054
|
+
let issues = [];
|
|
2055
|
+
let notes = "";
|
|
2056
|
+
const expectedCodes = new Set(events.map((event) => event.code));
|
|
2057
|
+
const finishTool = {
|
|
2058
|
+
name: "finish_review",
|
|
2059
|
+
description: "Return the semantic verdict for this one file and its exact event contracts.",
|
|
2060
|
+
inputSchema: {
|
|
2061
|
+
verdict: z.enum(["pass", "fail"]),
|
|
2062
|
+
issues: z.array(z.object({
|
|
2063
|
+
eventCode: z.string(),
|
|
2064
|
+
reason: z.string()
|
|
2065
|
+
})).max(100),
|
|
2066
|
+
notes: z.string().optional()
|
|
2067
|
+
},
|
|
2068
|
+
jsonSchema: {
|
|
2069
|
+
type: "object",
|
|
2070
|
+
properties: {
|
|
2071
|
+
verdict: { type: "string", enum: ["pass", "fail"] },
|
|
2072
|
+
issues: {
|
|
2073
|
+
type: "array",
|
|
2074
|
+
maxItems: 100,
|
|
2075
|
+
items: {
|
|
2076
|
+
type: "object",
|
|
2077
|
+
properties: {
|
|
2078
|
+
eventCode: { type: "string" },
|
|
2079
|
+
reason: { type: "string" }
|
|
2080
|
+
},
|
|
2081
|
+
required: ["eventCode", "reason"]
|
|
2082
|
+
}
|
|
2083
|
+
},
|
|
2084
|
+
notes: { type: "string" }
|
|
2085
|
+
},
|
|
2086
|
+
required: ["verdict", "issues"]
|
|
2087
|
+
},
|
|
2088
|
+
handler: async (input) => {
|
|
2089
|
+
const rawIssues = Array.isArray(input.issues) ? input.issues : [];
|
|
2090
|
+
const normalized = [];
|
|
2091
|
+
for (const candidate of rawIssues) {
|
|
2092
|
+
const issue = candidate;
|
|
2093
|
+
const eventCode = String(issue.eventCode ?? "").trim();
|
|
2094
|
+
const reason = String(issue.reason ?? "").trim();
|
|
2095
|
+
if (!expectedCodes.has(eventCode) || !reason) {
|
|
2096
|
+
return { ok: false, reason: "issues must name an expected event and a concrete reason" };
|
|
2097
|
+
}
|
|
2098
|
+
normalized.push({ eventCode, reason });
|
|
2099
|
+
}
|
|
2100
|
+
verdict = input.verdict === "pass" ? "pass" : "fail";
|
|
2101
|
+
issues = normalized;
|
|
2102
|
+
notes = String(input.notes ?? "").trim();
|
|
2103
|
+
if (verdict === "pass" && issues.length > 0) {
|
|
2104
|
+
return { ok: false, reason: "a passing verdict cannot include issues" };
|
|
2105
|
+
}
|
|
2106
|
+
return { ok: true };
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
const content = await readFile2(resolve(deps.worktree.path, file), "utf8");
|
|
2110
|
+
const contracts = events.map((event) => ({
|
|
2111
|
+
code: event.code,
|
|
2112
|
+
reasoning: event.reasoning,
|
|
2113
|
+
anchorFile: event.anchorFile,
|
|
2114
|
+
anchorSymbol: event.anchorSymbol,
|
|
2115
|
+
payloadSchema: event.payloadSchema
|
|
2116
|
+
}));
|
|
2117
|
+
const surveyAnchors = deps.survey?.integratableCallSites.filter((anchor) => anchor.file === file) ?? [];
|
|
2118
|
+
const key = createHash2("sha256").update(file).digest("hex").slice(0, 12);
|
|
2119
|
+
const outcome = await deps.provider.invoke(
|
|
2120
|
+
{
|
|
2121
|
+
role: "review",
|
|
2122
|
+
task: "review",
|
|
2123
|
+
sessionKey: `review:${key}${sessionSuffix}`,
|
|
2124
|
+
systemPrompt: [
|
|
2125
|
+
"You are the file-isolated semantic review pass for product analytics instrumentation.",
|
|
2126
|
+
"Use finish_review exactly once. Do not browse the repository and do not edit files."
|
|
2127
|
+
].join("\n"),
|
|
2128
|
+
userPrompt: [
|
|
2129
|
+
"Validate every expected wrapper call for:",
|
|
2130
|
+
"- placement after the confirmed outcome rather than intent",
|
|
2131
|
+
"- deduplication, especially view/render/navigation events",
|
|
2132
|
+
"- payload correctness against the exact contract",
|
|
2133
|
+
"- absence of personal identifiers",
|
|
2134
|
+
"- generated wrapper usage rather than raw track calls",
|
|
2135
|
+
"",
|
|
2136
|
+
`Event contracts:
|
|
2137
|
+
${JSON.stringify(contracts)}`,
|
|
2138
|
+
`Saved survey anchors:
|
|
2139
|
+
${JSON.stringify(surveyAnchors)}`,
|
|
2140
|
+
"",
|
|
2141
|
+
`Expanded context for ${file}:
|
|
2142
|
+
${content.slice(0, 6e4)}`
|
|
2143
|
+
].join("\n"),
|
|
2144
|
+
tools: [finishTool],
|
|
2145
|
+
repositoryAccess: { mode: "review" },
|
|
2146
|
+
allowRepoWrite: false,
|
|
2147
|
+
cwd: deps.worktree.path
|
|
2148
|
+
},
|
|
2149
|
+
modelConfigFor(deps.models, "review", "review"),
|
|
2150
|
+
(action) => deps.sink.emit({ phase: "reviewing", file, action })
|
|
2151
|
+
);
|
|
2152
|
+
if (outcome.kind !== "completed" || verdict === void 0) {
|
|
2153
|
+
return {
|
|
2154
|
+
ok: false,
|
|
2155
|
+
issues: events.map((event) => ({
|
|
2156
|
+
eventCode: event.code,
|
|
2157
|
+
reason: `semantic review did not finish (${outcome.kind})`
|
|
2158
|
+
})),
|
|
2159
|
+
notes: `semantic review did not finish (${outcome.kind})`
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
return { ok: verdict === "pass" && issues.length === 0, issues, notes };
|
|
2163
|
+
}
|
|
2164
|
+
async function repairIntegratedFile(deps, file, events, review) {
|
|
2165
|
+
const key = createHash2("sha256").update(file).digest("hex").slice(0, 12);
|
|
2166
|
+
deps.sink.emit({ phase: "reviewing", file, action: "Sol is repairing semantic review failures" });
|
|
2167
|
+
const outcome = await deps.provider.invoke(
|
|
2168
|
+
{
|
|
2169
|
+
role: "edit",
|
|
2170
|
+
task: "repair",
|
|
2171
|
+
sessionKey: `semantic_repair:${key}`,
|
|
2172
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
2173
|
+
userPrompt: [
|
|
2174
|
+
`Repair only ${file}.`,
|
|
2175
|
+
"Preserve the exact event set and generated wrappers. Do not add events or edit other files.",
|
|
2176
|
+
"Correct placement timing, deduplication, payloads, PII safety, and wrapper usage as reported.",
|
|
2177
|
+
`Issues:
|
|
2178
|
+
${JSON.stringify(review.issues)}`,
|
|
2179
|
+
`Contracts:
|
|
2180
|
+
${JSON.stringify(events.map((event) => ({
|
|
2181
|
+
code: event.code,
|
|
2182
|
+
anchorSymbol: event.anchorSymbol,
|
|
2183
|
+
payloadSchema: event.payloadSchema,
|
|
2184
|
+
reasoning: event.reasoning
|
|
2185
|
+
})))}`
|
|
2186
|
+
].join("\n"),
|
|
2187
|
+
tools: [],
|
|
2188
|
+
repositoryAccess: { mode: "repair", allowedFiles: [file] },
|
|
2189
|
+
allowRepoWrite: true,
|
|
2190
|
+
cwd: deps.worktree.path
|
|
2191
|
+
},
|
|
2192
|
+
modelConfigFor(deps.models, "repair", "edit"),
|
|
2193
|
+
(action) => deps.sink.emit({ phase: "reviewing", file, action })
|
|
2194
|
+
);
|
|
2195
|
+
return outcome.kind === "completed";
|
|
2196
|
+
}
|
|
2197
|
+
async function finalizeIntegration(deps, verifyCwd = deps.worktree.path) {
|
|
2198
|
+
const steps = resolveVerifySteps(verifyCwd, deps.target);
|
|
1983
2199
|
deps.sink.emit({ phase: "verifying", action: "final verification pass" });
|
|
1984
|
-
const results = await runVerifySteps(
|
|
2200
|
+
const results = await runVerifySteps(verifyCwd, steps);
|
|
1985
2201
|
return {
|
|
1986
2202
|
results,
|
|
1987
2203
|
status: runVerificationStatus(results),
|
|
1988
2204
|
command: verificationCommand(results)
|
|
1989
2205
|
};
|
|
1990
2206
|
}
|
|
1991
|
-
async function repairVerification(deps, results) {
|
|
2207
|
+
async function repairVerification(deps, results, cycle = 1) {
|
|
1992
2208
|
const failed = results.filter((result) => !result.ok);
|
|
1993
2209
|
if (failed.length === 0) return true;
|
|
1994
|
-
deps.sink.emit({ phase: "verifying", action: "
|
|
2210
|
+
deps.sink.emit({ phase: "verifying", action: "Sol is repairing final verification failures" });
|
|
2211
|
+
const allowedFiles = deps.allowedRepairFiles ?? deps.registered.map((event) => event.anchorFile);
|
|
1995
2212
|
const outcome = await deps.provider.invoke(
|
|
1996
2213
|
{
|
|
1997
2214
|
role: "edit",
|
|
1998
2215
|
task: "repair",
|
|
1999
|
-
sessionKey:
|
|
2216
|
+
sessionKey: `final_repair:${cycle}`,
|
|
2000
2217
|
systemPrompt: deps.playbookSystemPrompt,
|
|
2001
2218
|
userPrompt: [
|
|
2002
2219
|
"The one final repository verification failed after all instrumentation patches were merged.",
|
|
@@ -2007,6 +2224,7 @@ async function repairVerification(deps, results) {
|
|
|
2007
2224
|
${result.output}`).join("\n\n").slice(0, 3e4)
|
|
2008
2225
|
].join("\n"),
|
|
2009
2226
|
tools: [],
|
|
2227
|
+
repositoryAccess: { mode: "repair", allowedFiles },
|
|
2010
2228
|
allowRepoWrite: true,
|
|
2011
2229
|
cwd: deps.worktree.path
|
|
2012
2230
|
},
|
|
@@ -2017,7 +2235,7 @@ ${result.output}`).join("\n\n").slice(0, 3e4)
|
|
|
2017
2235
|
}
|
|
2018
2236
|
|
|
2019
2237
|
// src/engine/runs.ts
|
|
2020
|
-
import { createHash as
|
|
2238
|
+
import { createHash as createHash3 } from "crypto";
|
|
2021
2239
|
var RunsApiError = class extends Error {
|
|
2022
2240
|
constructor(message, status, code) {
|
|
2023
2241
|
super(message);
|
|
@@ -2029,7 +2247,7 @@ var RunsApiError = class extends Error {
|
|
|
2029
2247
|
code;
|
|
2030
2248
|
};
|
|
2031
2249
|
function itemIdempotencyKey(kind, code, extra = "") {
|
|
2032
|
-
return
|
|
2250
|
+
return createHash3("sha256").update(`${kind}
|
|
2033
2251
|
${code}
|
|
2034
2252
|
${extra}`).digest("hex").slice(0, 32);
|
|
2035
2253
|
}
|
|
@@ -2091,6 +2309,20 @@ var RunsClient = class {
|
|
|
2091
2309
|
payload
|
|
2092
2310
|
}).then(normalizeItemWriteResult);
|
|
2093
2311
|
}
|
|
2312
|
+
persistSurveyArtifact(runId, content, metadata) {
|
|
2313
|
+
return this.request(
|
|
2314
|
+
"POST",
|
|
2315
|
+
`/wizard/runs/${runId}/artifacts/survey`,
|
|
2316
|
+
artifactRequest(content, metadata)
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
commitDesign(runId, content, events, metadata) {
|
|
2320
|
+
return this.request(
|
|
2321
|
+
"POST",
|
|
2322
|
+
`/wizard/runs/${runId}/design/commit`,
|
|
2323
|
+
{ ...artifactRequest(content, metadata), events }
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2094
2326
|
completeRun(runId, completion) {
|
|
2095
2327
|
return this.request(
|
|
2096
2328
|
"POST",
|
|
@@ -2150,9 +2382,59 @@ function normalizeRunBootstrap(payload) {
|
|
|
2150
2382
|
setupStatus: stringValue(snapshotSource.setupStatus),
|
|
2151
2383
|
events
|
|
2152
2384
|
},
|
|
2153
|
-
ingestion: { apiKey: ingestion.apiKey, baseUrl: ingestion.baseUrl }
|
|
2385
|
+
ingestion: { apiKey: ingestion.apiKey, baseUrl: ingestion.baseUrl },
|
|
2386
|
+
artifacts: normalizeArtifacts(value.artifacts ?? snapshotSource.artifacts)
|
|
2154
2387
|
};
|
|
2155
2388
|
}
|
|
2389
|
+
function normalizeArtifacts(payload) {
|
|
2390
|
+
if (!Array.isArray(payload)) return [];
|
|
2391
|
+
return payload.flatMap((candidate) => {
|
|
2392
|
+
const value = record(candidate);
|
|
2393
|
+
const kind = value?.kind;
|
|
2394
|
+
if (kind !== "survey" && kind !== "event_design" || typeof value?.contentHash !== "string" || !/^[0-9a-f]{64}$/.test(value.contentHash) || typeof value.model !== "string" || typeof value.createdAt !== "string") {
|
|
2395
|
+
return [];
|
|
2396
|
+
}
|
|
2397
|
+
return [{
|
|
2398
|
+
kind,
|
|
2399
|
+
content: value.content,
|
|
2400
|
+
contentHash: value.contentHash,
|
|
2401
|
+
model: value.model,
|
|
2402
|
+
...typeof value.effort === "string" ? { effort: value.effort } : {},
|
|
2403
|
+
...typeof value.serviceTier === "string" ? { serviceTier: value.serviceTier } : {},
|
|
2404
|
+
fastMode: value.fastMode === true,
|
|
2405
|
+
...typeof value.frozenAt === "string" ? { frozenAt: value.frozenAt } : {},
|
|
2406
|
+
createdAt: value.createdAt
|
|
2407
|
+
}];
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
function artifactRequest(content, metadata) {
|
|
2411
|
+
return {
|
|
2412
|
+
content,
|
|
2413
|
+
contentHash: artifactContentHash(content),
|
|
2414
|
+
model: metadata.model,
|
|
2415
|
+
...metadata.effort ? { effort: metadata.effort } : {},
|
|
2416
|
+
...metadata.serviceTier ? { serviceTier: metadata.serviceTier } : {},
|
|
2417
|
+
fastMode: metadata.fastMode === true
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
function artifactContentHash(content) {
|
|
2421
|
+
return createHash3("sha256").update(stableJSONStringify(content)).digest("hex");
|
|
2422
|
+
}
|
|
2423
|
+
function stableJSONStringify(value) {
|
|
2424
|
+
if (Array.isArray(value)) {
|
|
2425
|
+
return `[${value.map(stableJSONStringify).join(",")}]`;
|
|
2426
|
+
}
|
|
2427
|
+
if (value && typeof value === "object") {
|
|
2428
|
+
const entries = Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right));
|
|
2429
|
+
return `{${entries.map(
|
|
2430
|
+
([key, item]) => `${canonicalJSONString(key)}:${stableJSONStringify(item)}`
|
|
2431
|
+
).join(",")}}`;
|
|
2432
|
+
}
|
|
2433
|
+
return canonicalJSONString(value);
|
|
2434
|
+
}
|
|
2435
|
+
function canonicalJSONString(value) {
|
|
2436
|
+
return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
2437
|
+
}
|
|
2156
2438
|
function normalizeIntegrationProgress(payload) {
|
|
2157
2439
|
const value = record(payload);
|
|
2158
2440
|
if (!value || !Array.isArray(value.changedFiles) || !Array.isArray(value.events)) {
|
|
@@ -2190,7 +2472,9 @@ function isSnapshotEvent(value) {
|
|
|
2190
2472
|
}
|
|
2191
2473
|
|
|
2192
2474
|
// src/engine/selection.ts
|
|
2193
|
-
import {
|
|
2475
|
+
import { readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
2476
|
+
import { resolve as resolve2, sep as sep2 } from "path";
|
|
2477
|
+
import { z as z2 } from "zod";
|
|
2194
2478
|
|
|
2195
2479
|
// src/engine/prompts.ts
|
|
2196
2480
|
var SURVEY_SYSTEM_PROMPT = [
|
|
@@ -2198,8 +2482,8 @@ var SURVEY_SYSTEM_PROMPT = [
|
|
|
2198
2482
|
"Map the repository so a later pass can pick the product's important lifecycle events.",
|
|
2199
2483
|
"You cannot edit files. Be fast and decisive; read entry points and feature roots, not every file.",
|
|
2200
2484
|
"",
|
|
2201
|
-
"
|
|
2202
|
-
"1. Stack: frameworks, package manager, app entry point
|
|
2485
|
+
"Finish by calling finish_survey exactly once. Its structured fields must contain:",
|
|
2486
|
+
"1. Stack: frameworks, package manager, and app entry point files.",
|
|
2203
2487
|
"2. Feature areas: name, root directory, and every relevant file containing",
|
|
2204
2488
|
" committed end-user actions or meaningful lifecycle transitions.",
|
|
2205
2489
|
"3. The end-user identify() anchor: the login/signup success and session-restore call sites for the",
|
|
@@ -2208,7 +2492,8 @@ var SURVEY_SYSTEM_PROMPT = [
|
|
|
2208
2492
|
"5. Exhaustive inventory of integratable call sites: file, function/symbol,",
|
|
2209
2493
|
" confirmed outcome or state transition, available non-PII payload fields,",
|
|
2210
2494
|
" and whether similar paths exist elsewhere.",
|
|
2211
|
-
"Cite concrete
|
|
2495
|
+
"Cite concrete repository-relative paths for every claim. Do not propose event names yet.",
|
|
2496
|
+
"Never copy raw source code into the result."
|
|
2212
2497
|
].join("\n");
|
|
2213
2498
|
function surveyUserPrompt(repoPath, onboardingContext) {
|
|
2214
2499
|
return [
|
|
@@ -2216,13 +2501,13 @@ function surveyUserPrompt(repoPath, onboardingContext) {
|
|
|
2216
2501
|
"",
|
|
2217
2502
|
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
2218
2503
|
"",
|
|
2219
|
-
"
|
|
2504
|
+
"Complete the structured survey now."
|
|
2220
2505
|
].join("\n");
|
|
2221
2506
|
}
|
|
2222
2507
|
var SELECTION_SYSTEM_PROMPT = [
|
|
2223
2508
|
"You are the event-selection pass of the Whisperr integration wizard.",
|
|
2224
2509
|
"From the survey report and business context, choose the IMPORTANT product events this codebase",
|
|
2225
|
-
"can actually emit, and propose
|
|
2510
|
+
"can actually emit, and propose them with propose_event_batch calls.",
|
|
2226
2511
|
"",
|
|
2227
2512
|
"Rules \u2014 these are hard constraints:",
|
|
2228
2513
|
"- Only propose an event with a concrete call site: a real file and function where the committed",
|
|
@@ -2250,11 +2535,11 @@ var SELECTION_SYSTEM_PROMPT = [
|
|
|
2250
2535
|
" government ids, usernames, passwords, device ids, or IP addresses. Domain metrics are welcome.",
|
|
2251
2536
|
"- The end user is the paying customer/consumer. Never instrument admin, staff, or operator actions.",
|
|
2252
2537
|
"",
|
|
2253
|
-
"
|
|
2538
|
+
"Use only propose_event_batch for proposals. When done, call finish_event_design with a one-line summary."
|
|
2254
2539
|
].join("\n");
|
|
2255
2540
|
function selectionUserPrompt(surveyReport, onboardingContext, existingEventCodes, rejectedCodes) {
|
|
2256
2541
|
return [
|
|
2257
|
-
"
|
|
2542
|
+
"Saved structured survey artifact:",
|
|
2258
2543
|
surveyReport,
|
|
2259
2544
|
"",
|
|
2260
2545
|
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
@@ -2365,40 +2650,108 @@ function validateProposedEvent(input, seen, rejected) {
|
|
|
2365
2650
|
};
|
|
2366
2651
|
}
|
|
2367
2652
|
async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSessionId) {
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2653
|
+
let survey;
|
|
2654
|
+
let finished = false;
|
|
2655
|
+
const finishSurvey = {
|
|
2656
|
+
name: "finish_survey",
|
|
2657
|
+
description: "Return the complete structured repository survey without raw source code.",
|
|
2658
|
+
inputSchema: {
|
|
2659
|
+
stack: z2.object({
|
|
2660
|
+
frameworks: z2.array(z2.string()).max(30),
|
|
2661
|
+
packageManager: z2.string().optional(),
|
|
2662
|
+
entryPoints: z2.array(z2.string()).max(50)
|
|
2663
|
+
}),
|
|
2664
|
+
featureAreas: z2.array(z2.object({
|
|
2665
|
+
name: z2.string(),
|
|
2666
|
+
root: z2.string(),
|
|
2667
|
+
files: z2.array(z2.string()).max(200)
|
|
2668
|
+
})).max(200),
|
|
2669
|
+
identifyAnchors: z2.array(z2.object({
|
|
2670
|
+
file: z2.string(),
|
|
2671
|
+
symbol: z2.string().optional(),
|
|
2672
|
+
description: z2.string()
|
|
2673
|
+
})).max(50),
|
|
2674
|
+
logoutAnchors: z2.array(z2.object({
|
|
2675
|
+
file: z2.string(),
|
|
2676
|
+
symbol: z2.string().optional(),
|
|
2677
|
+
description: z2.string()
|
|
2678
|
+
})).max(50),
|
|
2679
|
+
existingAnalytics: z2.array(z2.object({
|
|
2680
|
+
file: z2.string(),
|
|
2681
|
+
symbol: z2.string().optional(),
|
|
2682
|
+
description: z2.string()
|
|
2683
|
+
})).max(100),
|
|
2684
|
+
integratableCallSites: z2.array(z2.object({
|
|
2685
|
+
file: z2.string(),
|
|
2686
|
+
symbol: z2.string().optional(),
|
|
2687
|
+
description: z2.string(),
|
|
2688
|
+
outcome: z2.string(),
|
|
2689
|
+
payloadFields: z2.array(z2.string()).max(50),
|
|
2690
|
+
similarPaths: z2.array(z2.string()).max(50)
|
|
2691
|
+
})).max(500)
|
|
2380
2692
|
},
|
|
2693
|
+
jsonSchema: surveyJSONSchema(),
|
|
2694
|
+
handler: async (input) => {
|
|
2695
|
+
if (finished) {
|
|
2696
|
+
return { ok: false, reason: "survey is already frozen" };
|
|
2697
|
+
}
|
|
2698
|
+
const normalized = normalizeSurveyArtifact(input);
|
|
2699
|
+
if (!normalized) {
|
|
2700
|
+
return { ok: false, reason: "survey fields or repository-relative paths are invalid" };
|
|
2701
|
+
}
|
|
2702
|
+
survey = normalized;
|
|
2703
|
+
finished = true;
|
|
2704
|
+
return { ok: true, callSites: normalized.integratableCallSites.length };
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
sink.emit({ phase: "surveying", action: "mapping the repository" });
|
|
2708
|
+
const invocation = {
|
|
2709
|
+
role: "survey",
|
|
2710
|
+
task: "survey",
|
|
2711
|
+
sessionKey: "survey",
|
|
2712
|
+
systemPrompt: SURVEY_SYSTEM_PROMPT,
|
|
2713
|
+
userPrompt: surveyUserPrompt(repoPath, renderOnboardingContext(bootstrap.onboardingContext)),
|
|
2714
|
+
tools: [finishSurvey],
|
|
2715
|
+
repositoryAccess: { mode: "survey" },
|
|
2716
|
+
allowRepoWrite: false,
|
|
2717
|
+
cwd: repoPath,
|
|
2718
|
+
resumeSessionId
|
|
2719
|
+
};
|
|
2720
|
+
let outcome = await provider.invoke(
|
|
2721
|
+
invocation,
|
|
2381
2722
|
modelConfigFor(models, "survey", "survey"),
|
|
2382
2723
|
(action) => sink.emit({ phase: "surveying", action })
|
|
2383
2724
|
);
|
|
2384
|
-
|
|
2725
|
+
if (outcome.kind === "completed" && !finished && provider.invokeFallback) {
|
|
2726
|
+
survey = void 0;
|
|
2727
|
+
finished = false;
|
|
2728
|
+
sink.emit({ phase: "surveying", action: "Opus survey was invalid; retrying once with Sol" });
|
|
2729
|
+
outcome = await provider.invokeFallback(
|
|
2730
|
+
{ ...invocation, sessionKey: "survey:sol_retry" },
|
|
2731
|
+
modelConfigFor(models, "repair", "survey"),
|
|
2732
|
+
(action) => sink.emit({ phase: "surveying", action })
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
return { survey, finished, outcome };
|
|
2385
2736
|
}
|
|
2386
|
-
async function runSelection(provider, models, repoPath, bootstrap,
|
|
2737
|
+
async function runSelection(provider, models, repoPath, bootstrap, survey, rejectedCodes, sink, existingEventCodes = bootstrap.snapshot.events.map((event) => event.code), recoveryCodes = []) {
|
|
2387
2738
|
const proposed = [];
|
|
2388
2739
|
const seen = /* @__PURE__ */ new Set();
|
|
2389
2740
|
const rejected = new Set(rejectedCodes);
|
|
2741
|
+
const existing = new Set(existingEventCodes.map(normalizeEventCode));
|
|
2742
|
+
const recovery = new Set(recoveryCodes.map(normalizeEventCode));
|
|
2390
2743
|
let summary = "";
|
|
2391
2744
|
let finished = false;
|
|
2392
2745
|
const proposeTool = {
|
|
2393
2746
|
name: "propose_event",
|
|
2394
2747
|
description: "Propose one important, concretely-anchored product event. Returns ok:true when accepted.",
|
|
2395
2748
|
inputSchema: {
|
|
2396
|
-
code:
|
|
2397
|
-
name:
|
|
2398
|
-
reasoning:
|
|
2399
|
-
anchorFile:
|
|
2400
|
-
anchorSymbol:
|
|
2401
|
-
payloadSchema:
|
|
2749
|
+
code: z2.string().describe("lowercase snake_case event code"),
|
|
2750
|
+
name: z2.string().describe("short human-readable name"),
|
|
2751
|
+
reasoning: z2.string().describe("why this event matters for this business"),
|
|
2752
|
+
anchorFile: z2.string().describe("repository file where the committed action happens"),
|
|
2753
|
+
anchorSymbol: z2.string().optional().describe("function/symbol at the anchor"),
|
|
2754
|
+
payloadSchema: z2.record(z2.string(), z2.string()).optional().describe("important payload fields: name -> short description; no personal identifiers")
|
|
2402
2755
|
},
|
|
2403
2756
|
jsonSchema: {
|
|
2404
2757
|
type: "object",
|
|
@@ -2417,6 +2770,9 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2417
2770
|
required: ["code", "name", "reasoning", "anchorFile"]
|
|
2418
2771
|
},
|
|
2419
2772
|
handler: async (input) => {
|
|
2773
|
+
if (finished) {
|
|
2774
|
+
return { ok: false, reason: "event design is already frozen" };
|
|
2775
|
+
}
|
|
2420
2776
|
const candidate = {
|
|
2421
2777
|
code: String(input.code ?? ""),
|
|
2422
2778
|
name: String(input.name ?? ""),
|
|
@@ -2429,6 +2785,16 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2429
2785
|
if (!validated.ok) {
|
|
2430
2786
|
return { ok: false, reason: validated.reason };
|
|
2431
2787
|
}
|
|
2788
|
+
if (recovery.size > 0 && !recovery.has(validated.event.code)) {
|
|
2789
|
+
return { ok: false, reason: `placement recovery cannot create event ${validated.event.code}` };
|
|
2790
|
+
}
|
|
2791
|
+
if (existing.has(validated.event.code) && !recovery.has(validated.event.code)) {
|
|
2792
|
+
return { ok: false, reason: `event ${validated.event.code} is already registered` };
|
|
2793
|
+
}
|
|
2794
|
+
const anchorError = await validateAnchor(repoPath, validated.event);
|
|
2795
|
+
if (anchorError) {
|
|
2796
|
+
return { ok: false, reason: anchorError };
|
|
2797
|
+
}
|
|
2432
2798
|
seen.add(validated.event.code);
|
|
2433
2799
|
proposed.push(validated.event);
|
|
2434
2800
|
sink.emit({
|
|
@@ -2443,7 +2809,7 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2443
2809
|
name: "propose_event_batch",
|
|
2444
2810
|
description: "Propose a batch of important, concretely-anchored product events in one tool call.",
|
|
2445
2811
|
inputSchema: {
|
|
2446
|
-
events:
|
|
2812
|
+
events: z2.array(z2.object(proposeTool.inputSchema)).min(1).max(200)
|
|
2447
2813
|
},
|
|
2448
2814
|
jsonSchema: {
|
|
2449
2815
|
type: "object",
|
|
@@ -2471,9 +2837,9 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2471
2837
|
}
|
|
2472
2838
|
};
|
|
2473
2839
|
const finishTool = {
|
|
2474
|
-
name: "
|
|
2475
|
-
description: "
|
|
2476
|
-
inputSchema: { summary:
|
|
2840
|
+
name: "finish_event_design",
|
|
2841
|
+
description: "Freeze the complete batch after every important event has been proposed.",
|
|
2842
|
+
inputSchema: { summary: z2.string().describe("one line on what was selected and why") },
|
|
2477
2843
|
jsonSchema: {
|
|
2478
2844
|
type: "object",
|
|
2479
2845
|
properties: { summary: { type: "string", description: "one line on what was selected and why" } },
|
|
@@ -2486,61 +2852,204 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2486
2852
|
}
|
|
2487
2853
|
};
|
|
2488
2854
|
sink.emit({ phase: "designing", action: "selecting important events" });
|
|
2489
|
-
const
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2855
|
+
const invocation = {
|
|
2856
|
+
role: "design",
|
|
2857
|
+
task: "design",
|
|
2858
|
+
sessionKey: "design",
|
|
2859
|
+
systemPrompt: SELECTION_SYSTEM_PROMPT,
|
|
2860
|
+
userPrompt: selectionUserPrompt(
|
|
2861
|
+
JSON.stringify(survey),
|
|
2862
|
+
renderOnboardingContext(bootstrap.onboardingContext),
|
|
2863
|
+
existingEventCodes,
|
|
2864
|
+
rejectedCodes
|
|
2865
|
+
),
|
|
2866
|
+
tools: [proposeBatchTool, finishTool],
|
|
2867
|
+
repositoryAccess: { mode: "none" },
|
|
2868
|
+
allowRepoWrite: false,
|
|
2869
|
+
cwd: repoPath
|
|
2870
|
+
};
|
|
2871
|
+
let outcome = await provider.invoke(
|
|
2872
|
+
invocation,
|
|
2504
2873
|
modelConfigFor(models, "design", "design"),
|
|
2505
2874
|
(action) => sink.emit({ phase: "designing", action })
|
|
2506
2875
|
);
|
|
2876
|
+
if (outcome.kind === "completed" && (!finished || proposed.length === 0) && provider.invokeFallback) {
|
|
2877
|
+
proposed.length = 0;
|
|
2878
|
+
seen.clear();
|
|
2879
|
+
summary = "";
|
|
2880
|
+
finished = false;
|
|
2881
|
+
sink.emit({ phase: "designing", action: "Opus design was invalid; retrying once with Sol" });
|
|
2882
|
+
outcome = await provider.invokeFallback(
|
|
2883
|
+
{ ...invocation, sessionKey: "design:sol_retry" },
|
|
2884
|
+
modelConfigFor(models, "repair", "design"),
|
|
2885
|
+
(action) => sink.emit({ phase: "designing", action })
|
|
2886
|
+
);
|
|
2887
|
+
}
|
|
2507
2888
|
return {
|
|
2508
2889
|
outcome: outcome.kind,
|
|
2509
2890
|
finished,
|
|
2510
2891
|
proposed,
|
|
2511
2892
|
summary,
|
|
2512
|
-
|
|
2893
|
+
survey,
|
|
2513
2894
|
sessionId: outcome.sessionId
|
|
2514
2895
|
};
|
|
2515
2896
|
}
|
|
2516
|
-
async function
|
|
2517
|
-
const
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
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;
|
|
2897
|
+
async function validateAnchor(repoPath, event) {
|
|
2898
|
+
const root = resolve2(repoPath);
|
|
2899
|
+
const path = resolve2(root, event.anchorFile);
|
|
2900
|
+
if (path === root || !path.startsWith(root + sep2)) {
|
|
2901
|
+
return `${event.code}: anchor escapes the repository`;
|
|
2902
|
+
}
|
|
2903
|
+
try {
|
|
2904
|
+
const info = await stat3(path);
|
|
2905
|
+
if (!info.isFile()) return `${event.code}: anchor is not a file`;
|
|
2906
|
+
if (event.anchorSymbol) {
|
|
2907
|
+
const content = await readFile3(path, "utf8");
|
|
2908
|
+
if (!content.includes(event.anchorSymbol)) {
|
|
2909
|
+
return `${event.code}: anchor symbol ${event.anchorSymbol} is not present in ${event.anchorFile}`;
|
|
2537
2910
|
}
|
|
2538
|
-
throw error;
|
|
2539
2911
|
}
|
|
2540
|
-
|
|
2541
|
-
|
|
2912
|
+
} catch {
|
|
2913
|
+
return `${event.code}: anchor file ${event.anchorFile} does not exist`;
|
|
2542
2914
|
}
|
|
2543
|
-
return
|
|
2915
|
+
return void 0;
|
|
2916
|
+
}
|
|
2917
|
+
function surveyJSONSchema() {
|
|
2918
|
+
const anchor = {
|
|
2919
|
+
type: "object",
|
|
2920
|
+
properties: {
|
|
2921
|
+
file: { type: "string" },
|
|
2922
|
+
symbol: { type: "string" },
|
|
2923
|
+
description: { type: "string" }
|
|
2924
|
+
},
|
|
2925
|
+
required: ["file", "description"]
|
|
2926
|
+
};
|
|
2927
|
+
return {
|
|
2928
|
+
type: "object",
|
|
2929
|
+
properties: {
|
|
2930
|
+
stack: {
|
|
2931
|
+
type: "object",
|
|
2932
|
+
properties: {
|
|
2933
|
+
frameworks: { type: "array", items: { type: "string" }, maxItems: 30 },
|
|
2934
|
+
packageManager: { type: "string" },
|
|
2935
|
+
entryPoints: { type: "array", items: { type: "string" }, maxItems: 50 }
|
|
2936
|
+
},
|
|
2937
|
+
required: ["frameworks", "entryPoints"]
|
|
2938
|
+
},
|
|
2939
|
+
featureAreas: {
|
|
2940
|
+
type: "array",
|
|
2941
|
+
maxItems: 200,
|
|
2942
|
+
items: {
|
|
2943
|
+
type: "object",
|
|
2944
|
+
properties: {
|
|
2945
|
+
name: { type: "string" },
|
|
2946
|
+
root: { type: "string" },
|
|
2947
|
+
files: { type: "array", items: { type: "string" }, maxItems: 200 }
|
|
2948
|
+
},
|
|
2949
|
+
required: ["name", "root", "files"]
|
|
2950
|
+
}
|
|
2951
|
+
},
|
|
2952
|
+
identifyAnchors: { type: "array", items: anchor, maxItems: 50 },
|
|
2953
|
+
logoutAnchors: { type: "array", items: anchor, maxItems: 50 },
|
|
2954
|
+
existingAnalytics: { type: "array", items: anchor, maxItems: 100 },
|
|
2955
|
+
integratableCallSites: {
|
|
2956
|
+
type: "array",
|
|
2957
|
+
maxItems: 500,
|
|
2958
|
+
items: {
|
|
2959
|
+
...anchor,
|
|
2960
|
+
properties: {
|
|
2961
|
+
...anchor.properties,
|
|
2962
|
+
outcome: { type: "string" },
|
|
2963
|
+
payloadFields: { type: "array", items: { type: "string" }, maxItems: 50 },
|
|
2964
|
+
similarPaths: { type: "array", items: { type: "string" }, maxItems: 50 }
|
|
2965
|
+
},
|
|
2966
|
+
required: ["file", "description", "outcome", "payloadFields", "similarPaths"]
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
},
|
|
2970
|
+
required: [
|
|
2971
|
+
"stack",
|
|
2972
|
+
"featureAreas",
|
|
2973
|
+
"identifyAnchors",
|
|
2974
|
+
"logoutAnchors",
|
|
2975
|
+
"existingAnalytics",
|
|
2976
|
+
"integratableCallSites"
|
|
2977
|
+
]
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
function normalizeSurveyArtifact(input) {
|
|
2981
|
+
const safePath = (value2) => {
|
|
2982
|
+
if (typeof value2 !== "string") return void 0;
|
|
2983
|
+
const normalized = value2.trim().replaceAll("\\", "/");
|
|
2984
|
+
if (!normalized || normalized.startsWith("/") || /^[a-z]:/i.test(normalized) || normalized.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
2985
|
+
return void 0;
|
|
2986
|
+
}
|
|
2987
|
+
return normalized;
|
|
2988
|
+
};
|
|
2989
|
+
const stringList = (value2, max) => Array.isArray(value2) && value2.length <= max && value2.every((item) => typeof item === "string" && item.trim()) ? [...new Set(value2.map((item) => String(item).trim()))] : void 0;
|
|
2990
|
+
const value = input;
|
|
2991
|
+
const stack = value.stack;
|
|
2992
|
+
const frameworks = stringList(stack?.frameworks, 30);
|
|
2993
|
+
const entryPoints = stringList(stack?.entryPoints, 50);
|
|
2994
|
+
if (!stack || !frameworks || !entryPoints || entryPoints.some((file) => !safePath(file))) {
|
|
2995
|
+
return void 0;
|
|
2996
|
+
}
|
|
2997
|
+
const anchors = (raw, max) => {
|
|
2998
|
+
if (!Array.isArray(raw) || raw.length > max) return void 0;
|
|
2999
|
+
const result = [];
|
|
3000
|
+
for (const item of raw) {
|
|
3001
|
+
const row = item;
|
|
3002
|
+
const file = safePath(row?.file);
|
|
3003
|
+
const description = typeof row?.description === "string" ? row.description.trim() : "";
|
|
3004
|
+
if (!file || !description) return void 0;
|
|
3005
|
+
result.push({
|
|
3006
|
+
file,
|
|
3007
|
+
...typeof row.symbol === "string" && row.symbol.trim() ? { symbol: row.symbol.trim() } : {},
|
|
3008
|
+
description
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
return result;
|
|
3012
|
+
};
|
|
3013
|
+
if (!Array.isArray(value.featureAreas) || value.featureAreas.length > 200) return void 0;
|
|
3014
|
+
const featureAreas = [];
|
|
3015
|
+
for (const item of value.featureAreas) {
|
|
3016
|
+
const row = item;
|
|
3017
|
+
const root = safePath(row?.root);
|
|
3018
|
+
const files = stringList(row?.files, 200);
|
|
3019
|
+
if (typeof row?.name !== "string" || !row.name.trim() || !root || !files || files.some((file) => !safePath(file))) return void 0;
|
|
3020
|
+
featureAreas.push({ name: row.name.trim(), root, files: files.map((file) => safePath(file)) });
|
|
3021
|
+
}
|
|
3022
|
+
const identifyAnchors = anchors(value.identifyAnchors, 50);
|
|
3023
|
+
const logoutAnchors = anchors(value.logoutAnchors, 50);
|
|
3024
|
+
const existingAnalytics = anchors(value.existingAnalytics, 100);
|
|
3025
|
+
const baseCallSites = anchors(value.integratableCallSites, 500);
|
|
3026
|
+
if (!identifyAnchors || !logoutAnchors || !existingAnalytics || !baseCallSites || !Array.isArray(value.integratableCallSites)) return void 0;
|
|
3027
|
+
const integratableCallSites = [];
|
|
3028
|
+
for (let index = 0; index < baseCallSites.length; index += 1) {
|
|
3029
|
+
const row = value.integratableCallSites[index];
|
|
3030
|
+
const outcome = typeof row.outcome === "string" ? row.outcome.trim() : "";
|
|
3031
|
+
const payloadFields = stringList(row.payloadFields, 50);
|
|
3032
|
+
const similarPaths = stringList(row.similarPaths, 50);
|
|
3033
|
+
if (!outcome || !payloadFields || !similarPaths || similarPaths.some((file) => !safePath(file))) return void 0;
|
|
3034
|
+
integratableCallSites.push({
|
|
3035
|
+
...baseCallSites[index],
|
|
3036
|
+
outcome,
|
|
3037
|
+
payloadFields,
|
|
3038
|
+
similarPaths: similarPaths.map((file) => safePath(file))
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
return {
|
|
3042
|
+
stack: {
|
|
3043
|
+
frameworks,
|
|
3044
|
+
...typeof stack.packageManager === "string" && stack.packageManager.trim() ? { packageManager: stack.packageManager.trim() } : {},
|
|
3045
|
+
entryPoints: entryPoints.map((file) => safePath(file))
|
|
3046
|
+
},
|
|
3047
|
+
featureAreas,
|
|
3048
|
+
identifyAnchors,
|
|
3049
|
+
logoutAnchors,
|
|
3050
|
+
existingAnalytics,
|
|
3051
|
+
integratableCallSites
|
|
3052
|
+
};
|
|
2544
3053
|
}
|
|
2545
3054
|
function placementsFor(registered) {
|
|
2546
3055
|
return registered.map((event) => ({
|
|
@@ -2556,7 +3065,7 @@ function placementsFor(registered) {
|
|
|
2556
3065
|
import { spawn as spawn2 } from "child_process";
|
|
2557
3066
|
import { cp, lstat, mkdir, mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
|
|
2558
3067
|
import { tmpdir } from "os";
|
|
2559
|
-
import { dirname, join as
|
|
3068
|
+
import { dirname as dirname2, join as join5, relative, resolve as resolve3, sep as sep3 } from "path";
|
|
2560
3069
|
var WorktreeError = class extends Error {
|
|
2561
3070
|
constructor(code, message) {
|
|
2562
3071
|
super(message);
|
|
@@ -2566,7 +3075,7 @@ var WorktreeError = class extends Error {
|
|
|
2566
3075
|
code;
|
|
2567
3076
|
};
|
|
2568
3077
|
function git(cwd, args) {
|
|
2569
|
-
return new Promise((
|
|
3078
|
+
return new Promise((resolve8) => {
|
|
2570
3079
|
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2571
3080
|
let stdout = "";
|
|
2572
3081
|
let stderr = "";
|
|
@@ -2576,8 +3085,8 @@ function git(cwd, args) {
|
|
|
2576
3085
|
child.stderr.on("data", (chunk) => {
|
|
2577
3086
|
stderr += String(chunk);
|
|
2578
3087
|
});
|
|
2579
|
-
child.on("error", () =>
|
|
2580
|
-
child.on("close", (exitCode) =>
|
|
3088
|
+
child.on("error", () => resolve8({ ok: false, stdout, stderr: "git is not available" }));
|
|
3089
|
+
child.on("close", (exitCode) => resolve8({ ok: exitCode === 0, stdout, stderr }));
|
|
2581
3090
|
});
|
|
2582
3091
|
}
|
|
2583
3092
|
async function must(cwd, args) {
|
|
@@ -2588,8 +3097,8 @@ async function must(cwd, args) {
|
|
|
2588
3097
|
return result.stdout;
|
|
2589
3098
|
}
|
|
2590
3099
|
function safeRepoFile(repoPath, file) {
|
|
2591
|
-
const path =
|
|
2592
|
-
if (path === repoPath || !path.startsWith(repoPath +
|
|
3100
|
+
const path = resolve3(repoPath, file);
|
|
3101
|
+
if (path === repoPath || !path.startsWith(repoPath + sep3) || relative(repoPath, path).startsWith("..")) {
|
|
2593
3102
|
throw new WorktreeError("git_failed", `unsafe progress file path: ${file}`);
|
|
2594
3103
|
}
|
|
2595
3104
|
return path;
|
|
@@ -2605,7 +3114,7 @@ async function seedWorktree(repoPath, worktreePath, files) {
|
|
|
2605
3114
|
const destination = safeRepoFile(worktreePath, file);
|
|
2606
3115
|
try {
|
|
2607
3116
|
await lstat(source);
|
|
2608
|
-
await mkdir(
|
|
3117
|
+
await mkdir(dirname2(destination), { recursive: true });
|
|
2609
3118
|
await cp(source, destination, { recursive: true, force: true });
|
|
2610
3119
|
} catch {
|
|
2611
3120
|
await rm(destination, { recursive: true, force: true });
|
|
@@ -2627,7 +3136,7 @@ async function createWorktree(repoPath, force = false, allowedDirtyFiles = []) {
|
|
|
2627
3136
|
);
|
|
2628
3137
|
}
|
|
2629
3138
|
let baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
|
|
2630
|
-
const path = await mkdtemp(
|
|
3139
|
+
const path = await mkdtemp(join5(tmpdir(), "whisperr-worktree-"));
|
|
2631
3140
|
await must(repoPath, ["worktree", "add", "--detach", "--force", path, "HEAD"]);
|
|
2632
3141
|
const seedFiles = force ? dirty : dirty.filter((file) => allowed.has(file));
|
|
2633
3142
|
if (seedFiles.length > 0) {
|
|
@@ -2652,12 +3161,18 @@ async function worktreePatch(handle) {
|
|
|
2652
3161
|
return must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
|
|
2653
3162
|
}
|
|
2654
3163
|
async function applyPatchToRepo(handle, patch) {
|
|
3164
|
+
return applyPatch(handle.repoPath, patch, false);
|
|
3165
|
+
}
|
|
3166
|
+
async function reversePatchInRepo(repoPath, patch) {
|
|
3167
|
+
return applyPatch(repoPath, patch, true);
|
|
3168
|
+
}
|
|
3169
|
+
async function applyPatch(repoPath, patch, reverse) {
|
|
2655
3170
|
if (patch.trim() === "") {
|
|
2656
3171
|
return;
|
|
2657
3172
|
}
|
|
2658
|
-
await new Promise((
|
|
2659
|
-
const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
|
|
2660
|
-
cwd:
|
|
3173
|
+
await new Promise((resolve8, reject) => {
|
|
3174
|
+
const child = spawn2("git", ["apply", ...reverse ? ["--reverse"] : [], "--whitespace=nowarn"], {
|
|
3175
|
+
cwd: repoPath,
|
|
2661
3176
|
stdio: ["pipe", "ignore", "pipe"]
|
|
2662
3177
|
});
|
|
2663
3178
|
let stderr = "";
|
|
@@ -2667,7 +3182,7 @@ async function applyPatchToRepo(handle, patch) {
|
|
|
2667
3182
|
child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
|
|
2668
3183
|
child.on("close", (exitCode) => {
|
|
2669
3184
|
if (exitCode === 0) {
|
|
2670
|
-
|
|
3185
|
+
resolve8();
|
|
2671
3186
|
} else {
|
|
2672
3187
|
reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
|
|
2673
3188
|
}
|
|
@@ -2675,24 +3190,6 @@ async function applyPatchToRepo(handle, patch) {
|
|
|
2675
3190
|
child.stdin.end(patch);
|
|
2676
3191
|
});
|
|
2677
3192
|
}
|
|
2678
|
-
async function commitWorktreeCheckpoint(handle) {
|
|
2679
|
-
await must(handle.path, ["add", "-A"]);
|
|
2680
|
-
const patch = await must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
|
|
2681
|
-
if (patch.trim() === "") {
|
|
2682
|
-
return;
|
|
2683
|
-
}
|
|
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
3193
|
async function discardWorktreeChanges(handle) {
|
|
2697
3194
|
await must(handle.path, ["reset", "--hard", handle.baseRef]);
|
|
2698
3195
|
await must(handle.path, ["clean", "-fd"]);
|
|
@@ -2705,6 +3202,16 @@ async function removeWorktree(handle) {
|
|
|
2705
3202
|
// src/engine/orchestrator.ts
|
|
2706
3203
|
async function runEngine(input) {
|
|
2707
3204
|
const runs = new RunsClient(input.config, input.session);
|
|
3205
|
+
const phaseTimings = [];
|
|
3206
|
+
let activePhase = "authorizing";
|
|
3207
|
+
let phaseStartedAt = Date.now();
|
|
3208
|
+
let timingsFinished = false;
|
|
3209
|
+
const finishTimings = () => {
|
|
3210
|
+
if (!timingsFinished) {
|
|
3211
|
+
phaseTimings.push({ phase: activePhase, ms: Date.now() - phaseStartedAt });
|
|
3212
|
+
timingsFinished = true;
|
|
3213
|
+
}
|
|
3214
|
+
};
|
|
2708
3215
|
let bootstrap;
|
|
2709
3216
|
try {
|
|
2710
3217
|
bootstrap = await runs.createOrResumeRun({
|
|
@@ -2722,6 +3229,11 @@ async function runEngine(input) {
|
|
|
2722
3229
|
const runId = bootstrap.run.id;
|
|
2723
3230
|
input.provider.onRunReady?.(runId, bootstrap.run.modelConversationId);
|
|
2724
3231
|
const patchPhase = async (phase, message) => {
|
|
3232
|
+
if (phase !== activePhase) {
|
|
3233
|
+
phaseTimings.push({ phase: activePhase, ms: Date.now() - phaseStartedAt });
|
|
3234
|
+
activePhase = phase;
|
|
3235
|
+
phaseStartedAt = Date.now();
|
|
3236
|
+
}
|
|
2725
3237
|
try {
|
|
2726
3238
|
await runs.patchRun(runId, { phase, ...message ? { message } : {} });
|
|
2727
3239
|
} catch {
|
|
@@ -2733,6 +3245,7 @@ async function runEngine(input) {
|
|
|
2733
3245
|
} catch {
|
|
2734
3246
|
}
|
|
2735
3247
|
};
|
|
3248
|
+
let survey = artifactContent(bootstrap, "survey");
|
|
2736
3249
|
let registered;
|
|
2737
3250
|
try {
|
|
2738
3251
|
const projectEvents = bootstrap.snapshot.events.filter(
|
|
@@ -2745,17 +3258,24 @@ async function runEngine(input) {
|
|
|
2745
3258
|
phase: "surveying",
|
|
2746
3259
|
action: projectEvents.length === 0 ? "no saved progress found; starting a fresh survey" : "saved events need placement recovery; verifying the repository"
|
|
2747
3260
|
});
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
3261
|
+
if (!survey) {
|
|
3262
|
+
await patchPhase("surveying");
|
|
3263
|
+
const surveyRun = await runSurvey(
|
|
3264
|
+
input.provider,
|
|
3265
|
+
input.models,
|
|
3266
|
+
input.repoPath,
|
|
3267
|
+
bootstrap,
|
|
3268
|
+
input.sink
|
|
3269
|
+
);
|
|
3270
|
+
if (surveyRun.outcome.kind !== "completed" || !surveyRun.finished || !surveyRun.survey) {
|
|
3271
|
+
await failRun(`survey ${surveyRun.outcome.kind}`);
|
|
3272
|
+
return { kind: "aborted", reason: `survey ${surveyRun.outcome.kind}` };
|
|
3273
|
+
}
|
|
3274
|
+
survey = surveyRun.survey;
|
|
3275
|
+
const config2 = modelConfigForTask(input.models, "survey", "survey");
|
|
3276
|
+
await runs.persistSurveyArtifact(runId, survey, modelMetadata(config2));
|
|
3277
|
+
} else {
|
|
3278
|
+
input.sink.emit({ phase: "surveying", action: "reusing the saved immutable survey" });
|
|
2759
3279
|
}
|
|
2760
3280
|
await patchPhase("designing");
|
|
2761
3281
|
const recoverableCodes = new Set(missingPlacement.map((event) => event.code));
|
|
@@ -2765,13 +3285,14 @@ async function runEngine(input) {
|
|
|
2765
3285
|
input.models,
|
|
2766
3286
|
input.repoPath,
|
|
2767
3287
|
bootstrap,
|
|
2768
|
-
survey
|
|
3288
|
+
survey,
|
|
2769
3289
|
[],
|
|
2770
3290
|
input.sink,
|
|
2771
|
-
existingCodes
|
|
3291
|
+
existingCodes,
|
|
3292
|
+
[...recoverableCodes]
|
|
2772
3293
|
);
|
|
2773
3294
|
if (!selection.finished) {
|
|
2774
|
-
const reason = selection.outcome === "completed" ? "
|
|
3295
|
+
const reason = selection.outcome === "completed" ? "design did not call finish_event_design" : `design ${selection.outcome}`;
|
|
2775
3296
|
await failRun(reason);
|
|
2776
3297
|
return { kind: "aborted", reason };
|
|
2777
3298
|
}
|
|
@@ -2779,18 +3300,29 @@ async function runEngine(input) {
|
|
|
2779
3300
|
await failRun("selection finished with no accepted events");
|
|
2780
3301
|
return { kind: "aborted", reason: "selection finished with no accepted events" };
|
|
2781
3302
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
await failRun("selection rejected by user");
|
|
2785
|
-
return { kind: "aborted", reason: "selection rejected by user" };
|
|
3303
|
+
if (recoverableCodes.size > 0 && (selection.proposed.length !== recoverableCodes.size || selection.proposed.some((event) => !recoverableCodes.has(event.code)))) {
|
|
3304
|
+
throw new Error("placement recovery must cover the frozen event set exactly");
|
|
2786
3305
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
3306
|
+
await patchPhase("persisting", `registering ${selection.proposed.length} events`);
|
|
3307
|
+
const design = {
|
|
3308
|
+
surveyHash: artifactContentHash(survey),
|
|
3309
|
+
summary: selection.summary,
|
|
3310
|
+
events: [...selection.proposed].sort((a, b) => a.code.localeCompare(b.code))
|
|
3311
|
+
};
|
|
3312
|
+
const config = modelConfigForTask(input.models, "design", "design");
|
|
3313
|
+
const committed = await runs.commitDesign(
|
|
3314
|
+
runId,
|
|
3315
|
+
design,
|
|
3316
|
+
design.events.map(eventRequest),
|
|
3317
|
+
modelMetadata(config)
|
|
3318
|
+
);
|
|
3319
|
+
registered = registeredFromSnapshot(
|
|
3320
|
+
committed.events.filter((event) => event.projectId === bootstrap.project.id)
|
|
3321
|
+
);
|
|
3322
|
+
input.sink.emit({
|
|
3323
|
+
phase: "persisting",
|
|
3324
|
+
action: `designed and registered ${selection.proposed.length} events`
|
|
3325
|
+
});
|
|
2794
3326
|
const unresolved = projectEvents.filter((event) => !registered.some((candidate) => candidate.code === event.code)).map((event) => event.code);
|
|
2795
3327
|
if (unresolved.length > 0) {
|
|
2796
3328
|
throw new Error(`could not recover placements for: ${unresolved.join(", ")}`);
|
|
@@ -2815,17 +3347,18 @@ async function runEngine(input) {
|
|
|
2815
3347
|
const changedFiles2 = new Set(prior?.changedFiles ?? []);
|
|
2816
3348
|
const eventStatuses = await reconcileProgress(input.repoPath, registered, prior, input.sink);
|
|
2817
3349
|
const eventFailureReasons = /* @__PURE__ */ new Map();
|
|
3350
|
+
let verificationStatus = "pending";
|
|
2818
3351
|
for (const event of registered) {
|
|
2819
3352
|
const status = eventStatuses.get(event.code) ?? "planned";
|
|
2820
3353
|
if (status !== "planned" && status !== "failed" && status !== "unsupported") {
|
|
2821
3354
|
changedFiles2.add(event.anchorFile);
|
|
2822
3355
|
}
|
|
2823
3356
|
}
|
|
2824
|
-
let identifyWired = prior?.identifyWired === true && await setupLooksPresent(input.repoPath, changedFiles2);
|
|
3357
|
+
let identifyWired = prior?.identifyWired === true && await setupLooksPresent(input.repoPath, changedFiles2, survey);
|
|
2825
3358
|
const progress = () => ({
|
|
2826
3359
|
changedFiles: [...changedFiles2].sort(),
|
|
2827
3360
|
identifyWired,
|
|
2828
|
-
verificationStatus
|
|
3361
|
+
verificationStatus,
|
|
2829
3362
|
events: buildEvidence(registered, eventStatuses)
|
|
2830
3363
|
});
|
|
2831
3364
|
const saveProgress = async () => {
|
|
@@ -2840,6 +3373,7 @@ async function runEngine(input) {
|
|
|
2840
3373
|
}
|
|
2841
3374
|
await patchPhase("binding");
|
|
2842
3375
|
let worktree;
|
|
3376
|
+
const ownedPatches = [];
|
|
2843
3377
|
try {
|
|
2844
3378
|
worktree = await createWorktree(
|
|
2845
3379
|
input.repoPath,
|
|
@@ -2851,8 +3385,9 @@ async function runEngine(input) {
|
|
|
2851
3385
|
await failRun(reason);
|
|
2852
3386
|
return { kind: "aborted", reason };
|
|
2853
3387
|
}
|
|
2854
|
-
const
|
|
2855
|
-
|
|
3388
|
+
const setupWorktree = worktree;
|
|
3389
|
+
const checkpoint = async (handle, kind, eventCodes = []) => {
|
|
3390
|
+
const patch = await worktreePatch(handle);
|
|
2856
3391
|
if (patch.trim() === "") {
|
|
2857
3392
|
return [];
|
|
2858
3393
|
}
|
|
@@ -2861,155 +3396,133 @@ async function runEngine(input) {
|
|
|
2861
3396
|
changedFiles2.add(file);
|
|
2862
3397
|
}
|
|
2863
3398
|
await saveProgress();
|
|
2864
|
-
await applyPatchToRepo(
|
|
2865
|
-
|
|
3399
|
+
await applyPatchToRepo(handle, patch);
|
|
3400
|
+
ownedPatches.push({ patch, files, eventCodes, kind });
|
|
2866
3401
|
return files;
|
|
2867
3402
|
};
|
|
2868
3403
|
try {
|
|
2869
|
-
const bindings = await writeBindingsModule(
|
|
3404
|
+
const bindings = await writeBindingsModule(setupWorktree, input.target, registered);
|
|
2870
3405
|
if (!identifyWired) {
|
|
3406
|
+
const validateSetup = async () => {
|
|
3407
|
+
if (await readFile4(resolve4(setupWorktree.path, bindings.relativePath), "utf8") !== bindings.content) {
|
|
3408
|
+
return false;
|
|
3409
|
+
}
|
|
3410
|
+
const candidatePatch = await worktreePatch(setupWorktree);
|
|
3411
|
+
if (patchAddsProductEvents(candidatePatch, bindings.relativePath, registered)) {
|
|
3412
|
+
return false;
|
|
3413
|
+
}
|
|
3414
|
+
return setupLooksPresent(
|
|
3415
|
+
setupWorktree.path,
|
|
3416
|
+
new Set(patchFiles(candidatePatch)),
|
|
3417
|
+
survey
|
|
3418
|
+
);
|
|
3419
|
+
};
|
|
2871
3420
|
const setupCompleted = await runSdkSetupPass({
|
|
2872
3421
|
provider: input.provider,
|
|
2873
3422
|
models: input.models,
|
|
2874
|
-
worktree,
|
|
3423
|
+
worktree: setupWorktree,
|
|
2875
3424
|
target: input.target,
|
|
2876
3425
|
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
2877
3426
|
registered,
|
|
2878
3427
|
ingestion: bootstrap.ingestion,
|
|
3428
|
+
survey,
|
|
2879
3429
|
sink: input.sink,
|
|
2880
3430
|
saveClusters: () => {
|
|
2881
3431
|
},
|
|
2882
|
-
discardChanges: async () => discardWorktreeChanges(
|
|
3432
|
+
discardChanges: async () => discardWorktreeChanges(setupWorktree),
|
|
2883
3433
|
onClusterIntegrated: async () => {
|
|
2884
3434
|
}
|
|
2885
|
-
}, bindings);
|
|
3435
|
+
}, bindings, validateSetup);
|
|
2886
3436
|
if (!setupCompleted) {
|
|
2887
|
-
throw new Error("SDK setup did not complete");
|
|
3437
|
+
throw new Error("SDK setup did not complete or failed deterministic validation");
|
|
2888
3438
|
}
|
|
2889
|
-
await
|
|
2890
|
-
|
|
3439
|
+
const setupPatch = await worktreePatch(setupWorktree);
|
|
3440
|
+
const setupFiles = patchFiles(setupPatch);
|
|
3441
|
+
if (!await setupLooksPresent(setupWorktree.path, new Set(setupFiles), survey)) {
|
|
3442
|
+
throw new Error("SDK initialization, identify(), session restoration, or logout reset() could not be verified");
|
|
3443
|
+
}
|
|
3444
|
+
await checkpoint(setupWorktree, "setup");
|
|
3445
|
+
identifyWired = await setupLooksPresent(input.repoPath, changedFiles2, survey);
|
|
2891
3446
|
if (!identifyWired) {
|
|
2892
|
-
throw new Error("SDK initialization, bindings,
|
|
3447
|
+
throw new Error("SDK initialization, bindings, identify(), and reset() could not be verified");
|
|
2893
3448
|
}
|
|
2894
3449
|
await saveProgress();
|
|
2895
3450
|
} else {
|
|
2896
|
-
await checkpoint();
|
|
3451
|
+
await checkpoint(setupWorktree, "setup");
|
|
2897
3452
|
}
|
|
2898
3453
|
const pending = registered.filter((event) => {
|
|
2899
3454
|
const status = eventStatuses.get(event.code) ?? "planned";
|
|
2900
3455
|
return status === "planned" || status === "failed";
|
|
2901
3456
|
});
|
|
2902
|
-
|
|
3457
|
+
const clusters = deriveClusters(placementsFor(pending));
|
|
2903
3458
|
await patchPhase("integrating");
|
|
2904
|
-
await removeWorktree(
|
|
3459
|
+
await removeWorktree(setupWorktree).catch(() => {
|
|
2905
3460
|
});
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
cluster,
|
|
2921
|
-
error: error instanceof Error ? error.message : String(error)
|
|
2922
|
-
});
|
|
2923
|
-
}
|
|
3461
|
+
worktree = void 0;
|
|
3462
|
+
const queue = [...clusters];
|
|
3463
|
+
const active = /* @__PURE__ */ new Map();
|
|
3464
|
+
const lockedFiles = /* @__PURE__ */ new Set();
|
|
3465
|
+
let workerSequence = 0;
|
|
3466
|
+
while (queue.length > 0 || active.size > 0) {
|
|
3467
|
+
while (active.size < 4) {
|
|
3468
|
+
const nextIndex = queue.findIndex((cluster2) => !lockedFiles.has(cluster2.files[0]));
|
|
3469
|
+
if (nextIndex < 0) break;
|
|
3470
|
+
const cluster = queue.splice(nextIndex, 1)[0];
|
|
3471
|
+
const file = cluster.files[0];
|
|
3472
|
+
lockedFiles.add(file);
|
|
3473
|
+
const id2 = workerSequence++;
|
|
3474
|
+
active.set(id2, runClusterWorker(input, bootstrap, registered, survey, changedFiles2, cluster));
|
|
2924
3475
|
}
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
3476
|
+
if (active.size === 0) break;
|
|
3477
|
+
const { id, result: worker } = await Promise.race(
|
|
3478
|
+
[...active.entries()].map(async ([id2, task]) => ({ id: id2, result: await task }))
|
|
3479
|
+
);
|
|
3480
|
+
active.delete(id);
|
|
3481
|
+
lockedFiles.delete(worker.cluster.files[0]);
|
|
3482
|
+
const returned = worker.integration?.clusters[0];
|
|
3483
|
+
let applied = false;
|
|
3484
|
+
let failure = worker.error ?? returned?.note ?? "cluster patch could not be merged";
|
|
3485
|
+
if (returned?.status === "verified" && worker.handle) {
|
|
3486
|
+
const files = patchFiles(worker.patch);
|
|
3487
|
+
const unexpected = files.filter((file) => !worker.cluster.files.includes(file));
|
|
3488
|
+
if (unexpected.length === 0) {
|
|
3489
|
+
try {
|
|
3490
|
+
for (const file of files) changedFiles2.add(file);
|
|
3491
|
+
await saveProgress();
|
|
3492
|
+
await applyPatchToRepo(worker.handle, worker.patch);
|
|
3493
|
+
ownedPatches.push({
|
|
3494
|
+
patch: worker.patch,
|
|
3495
|
+
files,
|
|
3496
|
+
eventCodes: worker.cluster.events.map((event) => event.eventCode),
|
|
3497
|
+
kind: "cluster"
|
|
3498
|
+
});
|
|
3499
|
+
applied = true;
|
|
3500
|
+
} catch (error) {
|
|
3501
|
+
failure = `serial patch merge failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
2942
3502
|
}
|
|
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
3503
|
} else {
|
|
2969
|
-
|
|
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
|
-
}
|
|
3504
|
+
failure = `cluster edited files outside its isolation boundary: ${unexpected.join(", ")}`;
|
|
3002
3505
|
}
|
|
3506
|
+
}
|
|
3507
|
+
if (!applied && worker.cluster.events.length > 1) {
|
|
3508
|
+
const singles = worker.cluster.events.map((event, index) => ({
|
|
3509
|
+
id: clusterIdFor([event.file], [event.eventId || event.eventCode], index),
|
|
3510
|
+
files: [event.file],
|
|
3511
|
+
events: [event],
|
|
3512
|
+
status: "pending",
|
|
3513
|
+
attempts: 0
|
|
3514
|
+
}));
|
|
3515
|
+
queue.push(...singles);
|
|
3516
|
+
input.sink.emit({
|
|
3517
|
+
phase: "integrating",
|
|
3518
|
+
file: worker.cluster.files[0],
|
|
3519
|
+
action: "Sol could not finish the cluster; retrying each event independently"
|
|
3520
|
+
});
|
|
3521
|
+
} else {
|
|
3003
3522
|
for (const placement of worker.cluster.events) {
|
|
3004
|
-
const status = applied ?
|
|
3523
|
+
const status = applied ? "syntax_verified" : "failed";
|
|
3005
3524
|
eventStatuses.set(placement.eventCode, status);
|
|
3006
|
-
if (
|
|
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
|
-
}
|
|
3525
|
+
if (!applied) eventFailureReasons.set(placement.eventCode, failure);
|
|
3013
3526
|
await saveProgress();
|
|
3014
3527
|
input.sink.emit({
|
|
3015
3528
|
phase: "integrating",
|
|
@@ -3017,78 +3530,216 @@ async function runEngine(input) {
|
|
|
3017
3530
|
action: applied ? `merged and saved ${placement.eventCode}` : `${placement.eventCode} needs manual attention`
|
|
3018
3531
|
});
|
|
3019
3532
|
}
|
|
3020
|
-
if (worker.handle) {
|
|
3021
|
-
await removeWorktree(worker.handle).catch(() => {
|
|
3022
|
-
});
|
|
3023
|
-
}
|
|
3024
3533
|
}
|
|
3534
|
+
if (worker.handle) await removeWorktree(worker.handle).catch(() => {
|
|
3535
|
+
});
|
|
3025
3536
|
}
|
|
3026
3537
|
await saveProgress();
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
if (
|
|
3032
|
-
|
|
3033
|
-
|
|
3538
|
+
await patchPhase("reviewing");
|
|
3539
|
+
const reviewableByFile = groupEventsByFile(registered.filter(
|
|
3540
|
+
(event) => eventStatuses.get(event.code) === "syntax_verified"
|
|
3541
|
+
));
|
|
3542
|
+
if (reviewableByFile.size > 0) {
|
|
3543
|
+
const reviewWorktree = await createWorktree(
|
|
3544
|
+
input.repoPath,
|
|
3545
|
+
input.force ?? false,
|
|
3546
|
+
[...changedFiles2]
|
|
3547
|
+
);
|
|
3548
|
+
const reviewDeps = integrationDeps(
|
|
3549
|
+
input,
|
|
3550
|
+
bootstrap,
|
|
3551
|
+
registered,
|
|
3552
|
+
survey,
|
|
3553
|
+
reviewWorktree,
|
|
3554
|
+
[...changedFiles2]
|
|
3034
3555
|
);
|
|
3556
|
+
const reviews = await mapLimit(
|
|
3557
|
+
[...reviewableByFile.entries()],
|
|
3558
|
+
2,
|
|
3559
|
+
async ([file, events]) => ({
|
|
3560
|
+
file,
|
|
3561
|
+
events,
|
|
3562
|
+
review: await reviewIntegratedFile(reviewDeps, file, events)
|
|
3563
|
+
})
|
|
3564
|
+
);
|
|
3565
|
+
await removeWorktree(reviewWorktree).catch(() => {
|
|
3566
|
+
});
|
|
3567
|
+
for (const reviewed of reviews) {
|
|
3568
|
+
if (reviewed.review.ok) {
|
|
3569
|
+
for (const event of reviewed.events) eventStatuses.set(event.code, "reviewed");
|
|
3570
|
+
await saveProgress();
|
|
3571
|
+
continue;
|
|
3572
|
+
}
|
|
3573
|
+
const repairWorktree = await createWorktree(
|
|
3574
|
+
input.repoPath,
|
|
3575
|
+
input.force ?? false,
|
|
3576
|
+
[...changedFiles2]
|
|
3577
|
+
);
|
|
3578
|
+
const repairDeps = integrationDeps(
|
|
3579
|
+
input,
|
|
3580
|
+
bootstrap,
|
|
3581
|
+
registered,
|
|
3582
|
+
survey,
|
|
3583
|
+
repairWorktree,
|
|
3584
|
+
[...changedFiles2]
|
|
3585
|
+
);
|
|
3586
|
+
const repaired = await repairIntegratedFile(
|
|
3587
|
+
repairDeps,
|
|
3588
|
+
reviewed.file,
|
|
3589
|
+
reviewed.events,
|
|
3590
|
+
reviewed.review
|
|
3591
|
+
);
|
|
3592
|
+
const reviewCluster = {
|
|
3593
|
+
id: clusterIdFor([reviewed.file], reviewed.events.map((event) => event.eventId)),
|
|
3594
|
+
files: [reviewed.file],
|
|
3595
|
+
events: placementsFor(reviewed.events),
|
|
3596
|
+
status: "verified",
|
|
3597
|
+
attempts: 1
|
|
3598
|
+
};
|
|
3599
|
+
const structural = repaired ? await structuralClusterCheck(repairWorktree.path, reviewCluster) : { ok: false, notes: "Sol repair did not complete" };
|
|
3600
|
+
const recheck = structural.ok ? await reviewIntegratedFile(repairDeps, reviewed.file, reviewed.events, ":recheck") : { ok: false, issues: [], notes: structural.notes };
|
|
3601
|
+
const repairPatch = await worktreePatch(repairWorktree);
|
|
3602
|
+
const repairFiles = patchFiles(repairPatch);
|
|
3603
|
+
if (repaired && structural.ok && recheck.ok && repairFiles.every((file) => file === reviewed.file)) {
|
|
3604
|
+
for (const file of repairFiles) changedFiles2.add(file);
|
|
3605
|
+
await saveProgress();
|
|
3606
|
+
await applyPatchToRepo(repairWorktree, repairPatch);
|
|
3607
|
+
ownedPatches.push({
|
|
3608
|
+
patch: repairPatch,
|
|
3609
|
+
files: repairFiles,
|
|
3610
|
+
eventCodes: reviewed.events.map((event) => event.code),
|
|
3611
|
+
kind: "semantic"
|
|
3612
|
+
});
|
|
3613
|
+
for (const event of reviewed.events) eventStatuses.set(event.code, "reviewed");
|
|
3614
|
+
} else {
|
|
3615
|
+
const reasons = reviewed.review.issues.map((issue) => issue.reason).join("; ") || recheck.notes || structural.notes;
|
|
3616
|
+
await rollbackFileEventPatches(
|
|
3617
|
+
input.repoPath,
|
|
3618
|
+
reviewed.file,
|
|
3619
|
+
ownedPatches
|
|
3620
|
+
);
|
|
3621
|
+
for (const event of reviewed.events) {
|
|
3622
|
+
eventStatuses.set(event.code, "failed");
|
|
3623
|
+
eventFailureReasons.set(event.code, reasons.slice(0, 500));
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
await removeWorktree(repairWorktree).catch(() => {
|
|
3627
|
+
});
|
|
3628
|
+
await saveProgress();
|
|
3629
|
+
}
|
|
3035
3630
|
}
|
|
3036
3631
|
await patchPhase("verifying");
|
|
3037
|
-
|
|
3632
|
+
let verificationWorktree = await createWorktree(
|
|
3038
3633
|
input.repoPath,
|
|
3039
3634
|
input.force ?? false,
|
|
3040
3635
|
[...changedFiles2]
|
|
3041
3636
|
);
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
worktree: verificationWorktree,
|
|
3046
|
-
target: input.target,
|
|
3047
|
-
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
3637
|
+
let verificationDeps = integrationDeps(
|
|
3638
|
+
input,
|
|
3639
|
+
bootstrap,
|
|
3048
3640
|
registered,
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3641
|
+
survey,
|
|
3642
|
+
verificationWorktree,
|
|
3643
|
+
[...changedFiles2]
|
|
3644
|
+
);
|
|
3645
|
+
let finalVerify = await finalizeIntegration(verificationDeps, input.repoPath);
|
|
3646
|
+
for (let cycle = 0; finalVerify.status === "failed" && cycle < 2; cycle += 1) {
|
|
3647
|
+
const repaired = await repairVerification(verificationDeps, finalVerify.results, cycle + 1);
|
|
3648
|
+
if (!repaired) break;
|
|
3649
|
+
const repairPatch = await worktreePatch(verificationWorktree);
|
|
3650
|
+
const repairFiles = patchFiles(repairPatch);
|
|
3651
|
+
const allowedRepairFiles = new Set(verificationDeps.allowedRepairFiles ?? []);
|
|
3652
|
+
const structural = await structuralRepairCheck(
|
|
3653
|
+
verificationWorktree.path,
|
|
3654
|
+
registered,
|
|
3655
|
+
repairFiles
|
|
3656
|
+
);
|
|
3657
|
+
if (repairPatch.trim() === "" || repairFiles.some((file) => !allowedRepairFiles.has(file)) || !structural.ok) {
|
|
3658
|
+
break;
|
|
3055
3659
|
}
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3660
|
+
for (const file of repairFiles) changedFiles2.add(file);
|
|
3661
|
+
await saveProgress();
|
|
3662
|
+
await applyPatchToRepo(verificationWorktree, repairPatch);
|
|
3663
|
+
ownedPatches.push({
|
|
3664
|
+
patch: repairPatch,
|
|
3665
|
+
files: repairFiles,
|
|
3666
|
+
eventCodes: registered.map((event) => event.code),
|
|
3667
|
+
kind: "verification"
|
|
3668
|
+
});
|
|
3669
|
+
await removeWorktree(verificationWorktree).catch(() => {
|
|
3670
|
+
});
|
|
3671
|
+
finalVerify = await finalizeIntegration(verificationDeps, input.repoPath);
|
|
3672
|
+
if (finalVerify.status === "failed" && cycle + 1 < 2) {
|
|
3673
|
+
verificationWorktree = await createWorktree(
|
|
3674
|
+
input.repoPath,
|
|
3675
|
+
input.force ?? false,
|
|
3676
|
+
[...changedFiles2]
|
|
3677
|
+
);
|
|
3678
|
+
verificationDeps = integrationDeps(
|
|
3679
|
+
input,
|
|
3680
|
+
bootstrap,
|
|
3681
|
+
registered,
|
|
3682
|
+
survey,
|
|
3683
|
+
verificationWorktree,
|
|
3684
|
+
[...changedFiles2]
|
|
3685
|
+
);
|
|
3069
3686
|
}
|
|
3070
3687
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3688
|
+
verificationStatus = finalVerify.status;
|
|
3689
|
+
if (finalVerify.status !== "passed") {
|
|
3690
|
+
await removeWorktree(verificationWorktree).catch(() => {
|
|
3691
|
+
});
|
|
3692
|
+
await rollbackOwnedPatches(input.repoPath, ownedPatches);
|
|
3693
|
+
changedFiles2.clear();
|
|
3694
|
+
identifyWired = false;
|
|
3074
3695
|
for (const event of registered) {
|
|
3075
3696
|
eventStatuses.set(event.code, "failed");
|
|
3076
3697
|
eventFailureReasons.set(
|
|
3077
3698
|
event.code,
|
|
3078
|
-
`final verification
|
|
3699
|
+
`final verification ${finalVerify.status}${finalVerify.command ? `: ${finalVerify.command}` : ""}`
|
|
3079
3700
|
);
|
|
3080
3701
|
}
|
|
3081
3702
|
await saveProgress();
|
|
3082
|
-
throw new Error(
|
|
3703
|
+
throw new Error(`final verification ${finalVerify.status} after two Sol repair cycles`);
|
|
3083
3704
|
}
|
|
3084
|
-
|
|
3705
|
+
await removeWorktree(verificationWorktree).catch(() => {
|
|
3706
|
+
});
|
|
3707
|
+
identifyWired = await setupLooksPresent(input.repoPath, changedFiles2, survey);
|
|
3085
3708
|
if (!identifyWired) {
|
|
3709
|
+
await rollbackOwnedPatches(input.repoPath, ownedPatches);
|
|
3710
|
+
changedFiles2.clear();
|
|
3086
3711
|
for (const event of registered) {
|
|
3087
3712
|
eventStatuses.set(event.code, "failed");
|
|
3088
3713
|
eventFailureReasons.set(event.code, "SDK initialization or identify() is missing after final repair");
|
|
3089
3714
|
}
|
|
3090
3715
|
await saveProgress();
|
|
3091
|
-
throw new Error("SDK initialization and
|
|
3716
|
+
throw new Error("SDK initialization, identify(), and logout reset() must be present before completion");
|
|
3717
|
+
}
|
|
3718
|
+
for (const event of registered) {
|
|
3719
|
+
if (eventStatuses.get(event.code) === "reviewed") {
|
|
3720
|
+
eventStatuses.set(event.code, "build_verified");
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
await saveProgress();
|
|
3724
|
+
const failures = registered.filter((event) => eventStatuses.get(event.code) !== "build_verified");
|
|
3725
|
+
if (failures.length > 0) {
|
|
3726
|
+
const reason = `${failures.length} event${failures.length === 1 ? "" : "s"} could not be verified`;
|
|
3727
|
+
await failRun(reason);
|
|
3728
|
+
finishTimings();
|
|
3729
|
+
return {
|
|
3730
|
+
kind: "partial",
|
|
3731
|
+
runId,
|
|
3732
|
+
registered,
|
|
3733
|
+
eventStatuses,
|
|
3734
|
+
reportEvents: buildReportEvents(registered, eventStatuses, eventFailureReasons),
|
|
3735
|
+
changedFiles: [...changedFiles2].sort(),
|
|
3736
|
+
identifyWired,
|
|
3737
|
+
applied: changedFiles2.size > 0,
|
|
3738
|
+
summary: summarize(registered, eventStatuses).join("\n"),
|
|
3739
|
+
verificationStatus,
|
|
3740
|
+
phaseTimings,
|
|
3741
|
+
modelRoutes: modelRoutes(input.models)
|
|
3742
|
+
};
|
|
3092
3743
|
}
|
|
3093
3744
|
await patchPhase("reporting");
|
|
3094
3745
|
await runs.completeRun(runId, {
|
|
@@ -3098,12 +3749,12 @@ async function runEngine(input) {
|
|
|
3098
3749
|
...finalVerify.command ? { verificationCommand: finalVerify.command } : {},
|
|
3099
3750
|
events: buildEvidence(registered, eventStatuses)
|
|
3100
3751
|
});
|
|
3101
|
-
await removeWorktree(worktree).catch(() => {
|
|
3752
|
+
if (worktree) await removeWorktree(worktree).catch(() => {
|
|
3102
3753
|
});
|
|
3103
3754
|
const summaryLines = summarize(registered, eventStatuses);
|
|
3104
|
-
|
|
3755
|
+
finishTimings();
|
|
3105
3756
|
return {
|
|
3106
|
-
kind:
|
|
3757
|
+
kind: "completed",
|
|
3107
3758
|
runId,
|
|
3108
3759
|
registered,
|
|
3109
3760
|
eventStatuses,
|
|
@@ -3111,7 +3762,10 @@ async function runEngine(input) {
|
|
|
3111
3762
|
changedFiles: [...changedFiles2].sort(),
|
|
3112
3763
|
identifyWired,
|
|
3113
3764
|
applied: changedFiles2.size > 0,
|
|
3114
|
-
summary: summaryLines.join("\n")
|
|
3765
|
+
summary: summaryLines.join("\n"),
|
|
3766
|
+
verificationStatus,
|
|
3767
|
+
phaseTimings,
|
|
3768
|
+
modelRoutes: modelRoutes(input.models)
|
|
3115
3769
|
};
|
|
3116
3770
|
} catch (error) {
|
|
3117
3771
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -3123,8 +3777,9 @@ async function runEngine(input) {
|
|
|
3123
3777
|
}
|
|
3124
3778
|
}
|
|
3125
3779
|
await failRun(reason);
|
|
3126
|
-
await removeWorktree(worktree).catch(() => {
|
|
3780
|
+
if (worktree) await removeWorktree(worktree).catch(() => {
|
|
3127
3781
|
});
|
|
3782
|
+
finishTimings();
|
|
3128
3783
|
return {
|
|
3129
3784
|
kind: "partial",
|
|
3130
3785
|
runId,
|
|
@@ -3134,10 +3789,155 @@ async function runEngine(input) {
|
|
|
3134
3789
|
changedFiles: [...changedFiles2].sort(),
|
|
3135
3790
|
identifyWired,
|
|
3136
3791
|
applied: changedFiles2.size > 0,
|
|
3137
|
-
summary: `Integration stopped early: ${reason}. Saved event progress will be verified on rerun
|
|
3792
|
+
summary: `Integration stopped early: ${reason}. Saved event progress will be verified on rerun.`,
|
|
3793
|
+
verificationStatus,
|
|
3794
|
+
phaseTimings,
|
|
3795
|
+
modelRoutes: modelRoutes(input.models)
|
|
3138
3796
|
};
|
|
3139
3797
|
}
|
|
3140
3798
|
}
|
|
3799
|
+
function artifactContent(bootstrap, kind) {
|
|
3800
|
+
return bootstrap.artifacts.find((artifact) => artifact.kind === kind)?.content;
|
|
3801
|
+
}
|
|
3802
|
+
function modelConfigForTask(models, task, role) {
|
|
3803
|
+
return models[task] ?? models[role];
|
|
3804
|
+
}
|
|
3805
|
+
function modelMetadata(config) {
|
|
3806
|
+
return {
|
|
3807
|
+
model: config.model,
|
|
3808
|
+
...config.effort ? { effort: config.effort } : {},
|
|
3809
|
+
...config.serviceTier ? { serviceTier: config.serviceTier } : {},
|
|
3810
|
+
fastMode: config.fastMode === true
|
|
3811
|
+
};
|
|
3812
|
+
}
|
|
3813
|
+
function eventRequest(event) {
|
|
3814
|
+
return {
|
|
3815
|
+
code: event.code,
|
|
3816
|
+
name: event.name,
|
|
3817
|
+
reasoning: event.reasoning,
|
|
3818
|
+
anchorFile: event.anchorFile,
|
|
3819
|
+
...event.anchorSymbol ? { anchorSymbol: event.anchorSymbol } : {},
|
|
3820
|
+
...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
async function runClusterWorker(input, bootstrap, registered, survey, changedFiles2, cluster) {
|
|
3824
|
+
let handle;
|
|
3825
|
+
try {
|
|
3826
|
+
handle = await createWorktree(input.repoPath, input.force ?? false, [...changedFiles2]);
|
|
3827
|
+
const deps = integrationDeps(input, bootstrap, registered, survey, handle, [...changedFiles2]);
|
|
3828
|
+
const integration = await runClusterIntegration(
|
|
3829
|
+
deps,
|
|
3830
|
+
[cluster],
|
|
3831
|
+
async () => worktreePatch(handle)
|
|
3832
|
+
);
|
|
3833
|
+
return {
|
|
3834
|
+
cluster,
|
|
3835
|
+
handle,
|
|
3836
|
+
integration,
|
|
3837
|
+
patch: await worktreePatch(handle)
|
|
3838
|
+
};
|
|
3839
|
+
} catch (error) {
|
|
3840
|
+
return {
|
|
3841
|
+
cluster,
|
|
3842
|
+
handle,
|
|
3843
|
+
patch: "",
|
|
3844
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
function integrationDeps(input, bootstrap, registered, survey, worktree, allowedRepairFiles) {
|
|
3849
|
+
return {
|
|
3850
|
+
provider: input.provider,
|
|
3851
|
+
models: input.models,
|
|
3852
|
+
worktree,
|
|
3853
|
+
target: input.target,
|
|
3854
|
+
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
3855
|
+
registered,
|
|
3856
|
+
ingestion: bootstrap.ingestion,
|
|
3857
|
+
survey,
|
|
3858
|
+
allowedRepairFiles: allowedRepairFiles.filter(
|
|
3859
|
+
(file) => file !== generateBindings(input.target, registered).relativePath
|
|
3860
|
+
),
|
|
3861
|
+
sink: input.sink,
|
|
3862
|
+
saveClusters: () => {
|
|
3863
|
+
},
|
|
3864
|
+
discardChanges: async () => discardWorktreeChanges(worktree),
|
|
3865
|
+
onClusterIntegrated: async () => {
|
|
3866
|
+
}
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
function groupEventsByFile(events) {
|
|
3870
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3871
|
+
for (const event of events) {
|
|
3872
|
+
const current = grouped.get(event.anchorFile) ?? [];
|
|
3873
|
+
current.push(event);
|
|
3874
|
+
grouped.set(event.anchorFile, current);
|
|
3875
|
+
}
|
|
3876
|
+
return grouped;
|
|
3877
|
+
}
|
|
3878
|
+
async function structuralRepairCheck(repoPath, events, files) {
|
|
3879
|
+
const repairedFiles = new Set(files);
|
|
3880
|
+
for (const [file, fileEvents] of groupEventsByFile(
|
|
3881
|
+
events.filter((event) => repairedFiles.has(event.anchorFile))
|
|
3882
|
+
)) {
|
|
3883
|
+
const result = await structuralClusterCheck(repoPath, {
|
|
3884
|
+
id: clusterIdFor([file], fileEvents.map((event) => event.eventId)),
|
|
3885
|
+
files: [file],
|
|
3886
|
+
events: placementsFor(fileEvents),
|
|
3887
|
+
status: "verified",
|
|
3888
|
+
attempts: 1
|
|
3889
|
+
});
|
|
3890
|
+
if (!result.ok) return result;
|
|
3891
|
+
}
|
|
3892
|
+
return { ok: true, notes: "" };
|
|
3893
|
+
}
|
|
3894
|
+
async function mapLimit(values, limit, operation) {
|
|
3895
|
+
const results = new Array(values.length);
|
|
3896
|
+
let nextIndex = 0;
|
|
3897
|
+
const workers = Array.from({ length: Math.min(limit, values.length) }, async () => {
|
|
3898
|
+
for (; ; ) {
|
|
3899
|
+
const index = nextIndex++;
|
|
3900
|
+
if (index >= values.length) return;
|
|
3901
|
+
results[index] = await operation(values[index]);
|
|
3902
|
+
}
|
|
3903
|
+
});
|
|
3904
|
+
await Promise.all(workers);
|
|
3905
|
+
return results;
|
|
3906
|
+
}
|
|
3907
|
+
async function rollbackFileEventPatches(repoPath, file, patches) {
|
|
3908
|
+
for (let index = patches.length - 1; index >= 0; index -= 1) {
|
|
3909
|
+
const patch = patches[index];
|
|
3910
|
+
if (patch.rolledBack || patch.kind !== "cluster" && patch.kind !== "semantic" || !patch.files.includes(file)) {
|
|
3911
|
+
continue;
|
|
3912
|
+
}
|
|
3913
|
+
await reversePatchInRepo(repoPath, patch.patch);
|
|
3914
|
+
patch.rolledBack = true;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
async function rollbackOwnedPatches(repoPath, patches) {
|
|
3918
|
+
for (let index = patches.length - 1; index >= 0; index -= 1) {
|
|
3919
|
+
const patch = patches[index];
|
|
3920
|
+
if (patch.rolledBack) continue;
|
|
3921
|
+
await reversePatchInRepo(repoPath, patch.patch);
|
|
3922
|
+
patch.rolledBack = true;
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
function modelRoutes(models) {
|
|
3926
|
+
const result = {};
|
|
3927
|
+
for (const task of ["survey", "design", "sdk_setup", "cluster_edit", "review", "repair"]) {
|
|
3928
|
+
const role = task === "survey" || task === "design" ? task : task === "review" ? "review" : "edit";
|
|
3929
|
+
const config = models[task] ?? models[role];
|
|
3930
|
+
result[task] = {
|
|
3931
|
+
model: config.model,
|
|
3932
|
+
...config.effort ? { effort: config.effort } : {},
|
|
3933
|
+
serviceTier: config.fastMode ? "fast" : config.serviceTier ?? "default",
|
|
3934
|
+
maxTurns: config.maxTurns,
|
|
3935
|
+
maxMs: config.maxMs,
|
|
3936
|
+
...config.maxOutputTokens ? { maxOutputTokens: config.maxOutputTokens } : {}
|
|
3937
|
+
};
|
|
3938
|
+
}
|
|
3939
|
+
return result;
|
|
3940
|
+
}
|
|
3141
3941
|
function registeredFromSnapshot(events) {
|
|
3142
3942
|
return events.filter((event) => Boolean(event.anchorFile)).map((event) => ({
|
|
3143
3943
|
code: event.code,
|
|
@@ -3149,13 +3949,6 @@ function registeredFromSnapshot(events) {
|
|
|
3149
3949
|
payloadSchema: event.payloadSchema ?? {}
|
|
3150
3950
|
}));
|
|
3151
3951
|
}
|
|
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
3952
|
async function reconcileProgress(repoPath, registered, prior, sink) {
|
|
3160
3953
|
const priorById = new Map((prior?.events ?? []).map((event) => [event.eventId, event]));
|
|
3161
3954
|
const statuses = /* @__PURE__ */ new Map();
|
|
@@ -3182,36 +3975,46 @@ async function reconcileProgress(repoPath, registered, prior, sink) {
|
|
|
3182
3975
|
async function fileContainsWrapper(repoPath, file, eventCode) {
|
|
3183
3976
|
try {
|
|
3184
3977
|
const path = safeRepoPath(repoPath, file);
|
|
3185
|
-
const content = await
|
|
3978
|
+
const content = await readFile4(path, "utf8");
|
|
3186
3979
|
return content.includes(eventWrapperName(eventCode));
|
|
3187
3980
|
} catch {
|
|
3188
3981
|
return false;
|
|
3189
3982
|
}
|
|
3190
3983
|
}
|
|
3191
|
-
async function setupLooksPresent(repoPath, changedFiles2) {
|
|
3984
|
+
async function setupLooksPresent(repoPath, changedFiles2, survey) {
|
|
3192
3985
|
let bindingsFound = false;
|
|
3193
3986
|
let bindingCallFound = false;
|
|
3194
3987
|
let identifyFound = false;
|
|
3988
|
+
let resetFound = false;
|
|
3195
3989
|
let sdkInitializationFound = false;
|
|
3990
|
+
const contents = /* @__PURE__ */ new Map();
|
|
3196
3991
|
for (const file of changedFiles2) {
|
|
3197
3992
|
try {
|
|
3198
|
-
const content = await
|
|
3993
|
+
const content = await readFile4(safeRepoPath(repoPath, file), "utf8");
|
|
3994
|
+
contents.set(file, content);
|
|
3199
3995
|
const generated = content.includes("Generated by @whisperr/wizard");
|
|
3200
3996
|
bindingsFound ||= generated;
|
|
3201
3997
|
if (!generated) {
|
|
3202
3998
|
bindingCallFound ||= content.includes("bindWhisperr(") || content.includes("WhisperrEvents.bind(");
|
|
3203
3999
|
identifyFound ||= /\bidentify\s*\(/.test(content);
|
|
4000
|
+
resetFound ||= /\breset\s*\(/.test(content);
|
|
3204
4001
|
sdkInitializationFound ||= /whisperr/i.test(content) && /(initiali[sz]e|configure|client|setup)/i.test(content);
|
|
3205
4002
|
}
|
|
3206
4003
|
} catch {
|
|
3207
4004
|
}
|
|
3208
4005
|
}
|
|
3209
|
-
|
|
4006
|
+
const identifyAnchorsCovered = (survey?.identifyAnchors ?? []).every(
|
|
4007
|
+
(anchor) => /\bidentify\s*\(/.test(contents.get(anchor.file) ?? "")
|
|
4008
|
+
);
|
|
4009
|
+
const logoutAnchorsCovered = (survey?.logoutAnchors ?? []).every(
|
|
4010
|
+
(anchor) => /\breset\s*\(/.test(contents.get(anchor.file) ?? "")
|
|
4011
|
+
);
|
|
4012
|
+
return bindingsFound && bindingCallFound && identifyFound && resetFound && sdkInitializationFound && identifyAnchorsCovered && logoutAnchorsCovered;
|
|
3210
4013
|
}
|
|
3211
4014
|
function safeRepoPath(repoPath, file) {
|
|
3212
|
-
const root =
|
|
3213
|
-
const path =
|
|
3214
|
-
if (path === root || !path.startsWith(root +
|
|
4015
|
+
const root = resolve4(repoPath);
|
|
4016
|
+
const path = resolve4(root, file);
|
|
4017
|
+
if (path === root || !path.startsWith(root + sep4)) {
|
|
3215
4018
|
throw new Error(`unsafe progress file path: ${file}`);
|
|
3216
4019
|
}
|
|
3217
4020
|
return path;
|
|
@@ -3252,6 +4055,21 @@ function patchFiles(patch) {
|
|
|
3252
4055
|
}
|
|
3253
4056
|
return [...files];
|
|
3254
4057
|
}
|
|
4058
|
+
function patchAddsProductEvents(patch, bindingsFile, events) {
|
|
4059
|
+
const wrappers = events.map((event) => eventWrapperName(event.code));
|
|
4060
|
+
let file = "";
|
|
4061
|
+
for (const line of patch.split("\n")) {
|
|
4062
|
+
const header = /^\+\+\+ b\/(.+)$/.exec(line);
|
|
4063
|
+
if (header?.[1]) {
|
|
4064
|
+
file = header[1];
|
|
4065
|
+
continue;
|
|
4066
|
+
}
|
|
4067
|
+
if (file !== bindingsFile && line.startsWith("+") && !line.startsWith("+++") && wrappers.some((wrapper) => line.includes(wrapper))) {
|
|
4068
|
+
return true;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
return false;
|
|
4072
|
+
}
|
|
3255
4073
|
|
|
3256
4074
|
// src/engine/providers/claude.ts
|
|
3257
4075
|
import { createSdkMcpServer, query as query2, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -3263,9 +4081,9 @@ import {
|
|
|
3263
4081
|
|
|
3264
4082
|
// src/core/git.ts
|
|
3265
4083
|
import { spawn as spawn3 } from "child_process";
|
|
3266
|
-
import { createHash as
|
|
3267
|
-
import { readFile as
|
|
3268
|
-
import { basename as basename2, join as
|
|
4084
|
+
import { createHash as createHash4 } from "crypto";
|
|
4085
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
4086
|
+
import { basename as basename2, join as join6 } from "path";
|
|
3269
4087
|
async function takeCheckpoint(repoPath) {
|
|
3270
4088
|
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
3271
4089
|
if (!isRepo) return { isRepo: false };
|
|
@@ -3314,7 +4132,7 @@ async function revertToCheckpoint(repoPath, checkpoint) {
|
|
|
3314
4132
|
async function repoFingerprint(repoPath) {
|
|
3315
4133
|
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
3316
4134
|
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename2(repoPath)}`;
|
|
3317
|
-
return
|
|
4135
|
+
return createHash4("sha256").update(seed).digest("hex").slice(0, 16);
|
|
3318
4136
|
}
|
|
3319
4137
|
function normalizeRemote(url) {
|
|
3320
4138
|
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
@@ -3331,7 +4149,7 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
|
3331
4149
|
const contents = [];
|
|
3332
4150
|
for (const file of files) {
|
|
3333
4151
|
try {
|
|
3334
|
-
contents.push({ file, content: await
|
|
4152
|
+
contents.push({ file, content: await readFile5(join6(repoPath, file), "utf8") });
|
|
3335
4153
|
} catch {
|
|
3336
4154
|
continue;
|
|
3337
4155
|
}
|
|
@@ -3361,14 +4179,14 @@ function escapeRegExp(s) {
|
|
|
3361
4179
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3362
4180
|
}
|
|
3363
4181
|
function run(cwd, args) {
|
|
3364
|
-
return new Promise((
|
|
4182
|
+
return new Promise((resolve8) => {
|
|
3365
4183
|
const child = spawn3("git", args, { cwd });
|
|
3366
4184
|
let stdout = "";
|
|
3367
4185
|
let stderr = "";
|
|
3368
4186
|
child.stdout.on("data", (d) => stdout += d.toString());
|
|
3369
4187
|
child.stderr.on("data", (d) => stderr += d.toString());
|
|
3370
|
-
child.on("close", (code) =>
|
|
3371
|
-
child.on("error", () =>
|
|
4188
|
+
child.on("close", (code) => resolve8({ ok: code === 0, stdout, stderr }));
|
|
4189
|
+
child.on("error", () => resolve8({ ok: false, stdout, stderr }));
|
|
3372
4190
|
});
|
|
3373
4191
|
}
|
|
3374
4192
|
|
|
@@ -3607,7 +4425,7 @@ function coverageNote(coverage) {
|
|
|
3607
4425
|
}
|
|
3608
4426
|
|
|
3609
4427
|
// src/core/toolPolicy.ts
|
|
3610
|
-
import { isAbsolute, relative as relative2, resolve as
|
|
4428
|
+
import { isAbsolute, relative as relative2, resolve as resolve5 } from "path";
|
|
3611
4429
|
var SECRET_MATERIAL_DENIAL = "blocked: secret material must not be accessed";
|
|
3612
4430
|
var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
|
|
3613
4431
|
var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
|
|
@@ -3703,8 +4521,8 @@ function isSecretPathLike(value) {
|
|
|
3703
4521
|
}
|
|
3704
4522
|
function isPathInsideRepo(pathValue, repoPath) {
|
|
3705
4523
|
if (pathValue.includes("..")) return false;
|
|
3706
|
-
const repoRoot =
|
|
3707
|
-
const candidate = isAbsolute(pathValue) ?
|
|
4524
|
+
const repoRoot = resolve5(repoPath);
|
|
4525
|
+
const candidate = isAbsolute(pathValue) ? resolve5(pathValue) : resolve5(repoRoot, pathValue);
|
|
3708
4526
|
const rel = relative2(repoRoot, candidate);
|
|
3709
4527
|
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
3710
4528
|
}
|
|
@@ -3721,6 +4539,12 @@ function evaluateReadLike(secretValues, repoPathValues, context) {
|
|
|
3721
4539
|
message: "blocked: reads must stay inside the target repository"
|
|
3722
4540
|
};
|
|
3723
4541
|
}
|
|
4542
|
+
if (context.allowedFiles && !allowedPath(value, context)) {
|
|
4543
|
+
return {
|
|
4544
|
+
behavior: "deny",
|
|
4545
|
+
message: "blocked: this task may only access its explicitly supplied repository files"
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
3724
4548
|
}
|
|
3725
4549
|
return { behavior: "allow" };
|
|
3726
4550
|
}
|
|
@@ -3738,8 +4562,19 @@ function evaluateWrite(input, context) {
|
|
|
3738
4562
|
if (isSensitiveWritePath(pathValue, context.repoPath)) {
|
|
3739
4563
|
return { behavior: "deny", message: SENSITIVE_WRITE_DENIAL };
|
|
3740
4564
|
}
|
|
4565
|
+
if (context.allowedFiles && !allowedPath(pathValue, context)) {
|
|
4566
|
+
return {
|
|
4567
|
+
behavior: "deny",
|
|
4568
|
+
message: "blocked: this task may only edit its explicitly supplied repository files"
|
|
4569
|
+
};
|
|
4570
|
+
}
|
|
3741
4571
|
return { behavior: "allow" };
|
|
3742
4572
|
}
|
|
4573
|
+
function allowedPath(pathValue, context) {
|
|
4574
|
+
if (!context.allowedFiles) return true;
|
|
4575
|
+
const relativePath = repoRelativePath(pathValue, context.repoPath);
|
|
4576
|
+
return context.allowedFiles.has(relativePath);
|
|
4577
|
+
}
|
|
3743
4578
|
function evaluateBash(input) {
|
|
3744
4579
|
const command = firstString(input, ["command"]);
|
|
3745
4580
|
if (!command) {
|
|
@@ -3828,8 +4663,8 @@ function normalizePathLike(value) {
|
|
|
3828
4663
|
return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
|
|
3829
4664
|
}
|
|
3830
4665
|
function repoRelativePath(pathValue, repoPath) {
|
|
3831
|
-
const repoRoot =
|
|
3832
|
-
const candidate = isAbsolute(pathValue) ?
|
|
4666
|
+
const repoRoot = resolve5(repoPath);
|
|
4667
|
+
const candidate = isAbsolute(pathValue) ? resolve5(pathValue) : resolve5(repoRoot, pathValue);
|
|
3833
4668
|
return normalizePathLike(relative2(repoRoot, candidate));
|
|
3834
4669
|
}
|
|
3835
4670
|
function isSensitiveWritePath(pathValue, repoPath) {
|
|
@@ -4696,7 +5531,7 @@ async function runPass(opts) {
|
|
|
4696
5531
|
}
|
|
4697
5532
|
return { summary, costUsd, ok, maxedOut };
|
|
4698
5533
|
}
|
|
4699
|
-
function buildToolPolicyHooks(repoPath, trustedTools) {
|
|
5534
|
+
function buildToolPolicyHooks(repoPath, trustedTools, allowedFiles) {
|
|
4700
5535
|
return {
|
|
4701
5536
|
PreToolUse: [
|
|
4702
5537
|
{
|
|
@@ -4705,7 +5540,8 @@ function buildToolPolicyHooks(repoPath, trustedTools) {
|
|
|
4705
5540
|
if (input.hook_event_name !== "PreToolUse") return { continue: true };
|
|
4706
5541
|
const decision = evaluateToolUse(input.tool_name, toolInputRecord(input.tool_input), {
|
|
4707
5542
|
repoPath,
|
|
4708
|
-
trustedTools
|
|
5543
|
+
trustedTools,
|
|
5544
|
+
allowedFiles
|
|
4709
5545
|
});
|
|
4710
5546
|
if (decision.behavior === "allow") {
|
|
4711
5547
|
return { continue: true };
|
|
@@ -4723,9 +5559,9 @@ function buildToolPolicyHooks(repoPath, trustedTools) {
|
|
|
4723
5559
|
]
|
|
4724
5560
|
};
|
|
4725
5561
|
}
|
|
4726
|
-
function createToolPermissionCallback(repoPath, trustedTools) {
|
|
5562
|
+
function createToolPermissionCallback(repoPath, trustedTools, allowedFiles) {
|
|
4727
5563
|
return async (toolName, input, options) => {
|
|
4728
|
-
const decision = evaluateToolUse(toolName, input, { repoPath, trustedTools });
|
|
5564
|
+
const decision = evaluateToolUse(toolName, input, { repoPath, trustedTools, allowedFiles });
|
|
4729
5565
|
return decision.behavior === "allow" ? { behavior: "allow", toolUseID: options.toolUseID } : {
|
|
4730
5566
|
behavior: "deny",
|
|
4731
5567
|
message: decision.message,
|
|
@@ -4792,7 +5628,8 @@ function short(s, max = 60) {
|
|
|
4792
5628
|
|
|
4793
5629
|
// src/engine/providers/claude.ts
|
|
4794
5630
|
var READ_ONLY_TOOLS2 = ["Read", "Glob", "Grep"];
|
|
4795
|
-
var
|
|
5631
|
+
var CONSTRAINED_EDIT_TOOLS = ["Read", "Edit", "Write"];
|
|
5632
|
+
var SETUP_TOOLS = ["Read", "Edit", "Write", "Bash"];
|
|
4796
5633
|
var FAST_MODE_MODELS = /* @__PURE__ */ new Set(["claude-opus-5", "claude-opus-4-8"]);
|
|
4797
5634
|
function supportsClaudeFastMode(model) {
|
|
4798
5635
|
return FAST_MODE_MODELS.has(model);
|
|
@@ -4813,10 +5650,10 @@ function createClaudeProvider(config, session) {
|
|
|
4813
5650
|
})
|
|
4814
5651
|
);
|
|
4815
5652
|
const trustedTools = trustedEngineMcpTools(invocation.tools);
|
|
4816
|
-
const
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
5653
|
+
const accessMode = invocation.repositoryAccess?.mode ?? (invocation.allowRepoWrite ? "repair" : "survey");
|
|
5654
|
+
const repositoryTools = accessMode === "none" || accessMode === "review" ? [] : accessMode === "survey" ? READ_ONLY_TOOLS2 : accessMode === "sdk_setup" ? SETUP_TOOLS : CONSTRAINED_EDIT_TOOLS;
|
|
5655
|
+
const allowedTools = [...repositoryTools, ...trustedTools];
|
|
5656
|
+
const allowedFiles = invocation.repositoryAccess?.allowedFiles ? new Set(invocation.repositoryAccess.allowedFiles.map((file) => file.replaceAll("\\", "/"))) : void 0;
|
|
4820
5657
|
let sessionId = invocation.resumeSessionId;
|
|
4821
5658
|
let costUsd = 0;
|
|
4822
5659
|
let turns = 0;
|
|
@@ -4832,7 +5669,8 @@ function createClaudeProvider(config, session) {
|
|
|
4832
5669
|
`X-Whisperr-Wizard-Session-Key: ${headerValue(sessionKey)}`,
|
|
4833
5670
|
`X-Whisperr-Wizard-Model: ${headerValue(roleConfig.model)}`,
|
|
4834
5671
|
`X-Whisperr-Wizard-Effort: ${headerValue(roleConfig.effort ?? "")}`,
|
|
4835
|
-
`X-Whisperr-Wizard-Service-Tier: ${roleConfig.fastMode ? "fast" : "default"}
|
|
5672
|
+
`X-Whisperr-Wizard-Service-Tier: ${roleConfig.fastMode ? "fast" : "default"}`,
|
|
5673
|
+
`X-Whisperr-Wizard-Fallback: ${invocation.fallback ? "true" : "false"}`
|
|
4836
5674
|
].filter(Boolean).join("\n");
|
|
4837
5675
|
const response = query2({
|
|
4838
5676
|
prompt: invocation.userPrompt,
|
|
@@ -4844,9 +5682,10 @@ function createClaudeProvider(config, session) {
|
|
|
4844
5682
|
...roleConfig.effort ? { effort: roleConfig.effort } : {},
|
|
4845
5683
|
tools: [...allowedTools],
|
|
4846
5684
|
...hostTools.length > 0 ? { mcpServers: { engine: createSdkMcpServer({ name: "engine", tools: hostTools }) } } : {},
|
|
4847
|
-
hooks: buildToolPolicyHooks(invocation.cwd, trustedTools),
|
|
4848
|
-
canUseTool: createToolPermissionCallback(invocation.cwd, trustedTools),
|
|
5685
|
+
hooks: buildToolPolicyHooks(invocation.cwd, trustedTools, allowedFiles),
|
|
5686
|
+
canUseTool: createToolPermissionCallback(invocation.cwd, trustedTools, allowedFiles),
|
|
4849
5687
|
maxTurns: roleConfig.maxTurns,
|
|
5688
|
+
...roleConfig.maxOutputTokens ? { taskBudget: { total: roleConfig.maxOutputTokens } } : {},
|
|
4850
5689
|
...roleConfig.fastMode ?? supportsClaudeFastMode(roleConfig.model) ? { settings: { fastMode: true } } : {},
|
|
4851
5690
|
...invocation.resumeSessionId ? { resume: invocation.resumeSessionId } : {},
|
|
4852
5691
|
settingSources: [],
|
|
@@ -4906,28 +5745,32 @@ function headerValue(value) {
|
|
|
4906
5745
|
}
|
|
4907
5746
|
|
|
4908
5747
|
// src/engine/providers/openai.ts
|
|
4909
|
-
import { readdirSync, readFileSync as
|
|
4910
|
-
import { join as
|
|
5748
|
+
import { readdirSync, readFileSync as readFileSync3, statSync, writeFileSync } from "fs";
|
|
5749
|
+
import { join as join7, resolve as resolve6, sep as sep5 } from "path";
|
|
4911
5750
|
function jailedPath(cwd, candidate) {
|
|
4912
|
-
const resolved =
|
|
4913
|
-
if (resolved !== cwd && !resolved.startsWith(cwd +
|
|
5751
|
+
const resolved = resolve6(cwd, candidate);
|
|
5752
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + sep5)) {
|
|
4914
5753
|
throw new Error(`path ${candidate} escapes the repository`);
|
|
4915
5754
|
}
|
|
4916
5755
|
return resolved;
|
|
4917
5756
|
}
|
|
4918
|
-
function guardedPath(cwd, toolName, candidate) {
|
|
5757
|
+
function guardedPath(cwd, toolName, candidate, allowedFiles) {
|
|
4919
5758
|
const path = jailedPath(cwd, candidate);
|
|
4920
5759
|
const decision = evaluateToolUse(
|
|
4921
5760
|
toolName,
|
|
4922
5761
|
toolName === "Glob" ? { path: candidate, pattern: "*" } : { file_path: candidate },
|
|
4923
|
-
{ repoPath: cwd }
|
|
5762
|
+
{ repoPath: cwd, allowedFiles }
|
|
4924
5763
|
);
|
|
4925
5764
|
if (decision.behavior === "deny") {
|
|
4926
5765
|
throw new Error(decision.message);
|
|
4927
5766
|
}
|
|
4928
5767
|
return path;
|
|
4929
5768
|
}
|
|
4930
|
-
function fileTools(cwd, allowWrite) {
|
|
5769
|
+
function fileTools(cwd, allowWrite, access = { mode: allowWrite ? "repair" : "survey" }) {
|
|
5770
|
+
if (access.mode === "none" || access.mode === "review") {
|
|
5771
|
+
return [];
|
|
5772
|
+
}
|
|
5773
|
+
const allowedFiles = access.allowedFiles ? new Set(access.allowedFiles.map((file) => file.replaceAll("\\", "/"))) : void 0;
|
|
4931
5774
|
const tools = [
|
|
4932
5775
|
{
|
|
4933
5776
|
name: "read_file",
|
|
@@ -4939,8 +5782,8 @@ function fileTools(cwd, allowWrite) {
|
|
|
4939
5782
|
required: ["path"]
|
|
4940
5783
|
},
|
|
4941
5784
|
handler: async (input) => {
|
|
4942
|
-
const path = guardedPath(cwd, "Read", String(input.path ?? ""));
|
|
4943
|
-
const content =
|
|
5785
|
+
const path = guardedPath(cwd, "Read", String(input.path ?? ""), allowedFiles);
|
|
5786
|
+
const content = readFileSync3(path, "utf8");
|
|
4944
5787
|
return { content: content.slice(0, 4e4) };
|
|
4945
5788
|
}
|
|
4946
5789
|
},
|
|
@@ -4954,9 +5797,9 @@ function fileTools(cwd, allowWrite) {
|
|
|
4954
5797
|
required: ["path"]
|
|
4955
5798
|
},
|
|
4956
5799
|
handler: async (input) => {
|
|
4957
|
-
const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."));
|
|
5800
|
+
const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."), allowedFiles);
|
|
4958
5801
|
const entries = readdirSync(target9).filter((entry) => !isSecretPathLike(entry)).map((entry) => {
|
|
4959
|
-
const kind = statSync(
|
|
5802
|
+
const kind = statSync(join7(target9, entry)).isDirectory() ? "dir" : "file";
|
|
4960
5803
|
return `${kind}:${entry}`;
|
|
4961
5804
|
});
|
|
4962
5805
|
return { entries: entries.slice(0, 500) };
|
|
@@ -4975,7 +5818,7 @@ function fileTools(cwd, allowWrite) {
|
|
|
4975
5818
|
required: ["path", "content"]
|
|
4976
5819
|
},
|
|
4977
5820
|
handler: async (input) => {
|
|
4978
|
-
const path = guardedPath(cwd, "Write", String(input.path ?? ""));
|
|
5821
|
+
const path = guardedPath(cwd, "Write", String(input.path ?? ""), allowedFiles);
|
|
4979
5822
|
writeFileSync(path, String(input.content ?? ""));
|
|
4980
5823
|
return { ok: true };
|
|
4981
5824
|
}
|
|
@@ -4994,8 +5837,8 @@ function fileTools(cwd, allowWrite) {
|
|
|
4994
5837
|
required: ["path", "oldText", "newText"]
|
|
4995
5838
|
},
|
|
4996
5839
|
handler: async (input) => {
|
|
4997
|
-
const path = guardedPath(cwd, "Edit", String(input.path ?? ""));
|
|
4998
|
-
const content =
|
|
5840
|
+
const path = guardedPath(cwd, "Edit", String(input.path ?? ""), allowedFiles);
|
|
5841
|
+
const content = readFileSync3(path, "utf8");
|
|
4999
5842
|
const oldText = String(input.oldText ?? "");
|
|
5000
5843
|
if (!content.includes(oldText)) {
|
|
5001
5844
|
return { ok: false, reason: "oldText not found" };
|
|
@@ -5025,7 +5868,8 @@ function createOpenAIProvider(config, session) {
|
|
|
5025
5868
|
Authorization: `Bearer ${session.token}`,
|
|
5026
5869
|
...sessionKey === "legacy" ? {} : {
|
|
5027
5870
|
"X-Whisperr-Wizard-Session-Key": sessionKey,
|
|
5028
|
-
"X-Whisperr-Wizard-Task": task
|
|
5871
|
+
"X-Whisperr-Wizard-Task": task,
|
|
5872
|
+
"X-Whisperr-Wizard-Fallback": sessionKey.startsWith("fallback:") ? "true" : "false"
|
|
5029
5873
|
}
|
|
5030
5874
|
},
|
|
5031
5875
|
body: JSON.stringify(body)
|
|
@@ -5094,7 +5938,10 @@ function createOpenAIProvider(config, session) {
|
|
|
5094
5938
|
const sessionKey = invocation.sessionKey?.trim() || "legacy";
|
|
5095
5939
|
let activeConversationId = sessionKey === "legacy" ? legacyConversationId : conversationIds.get(sessionKey);
|
|
5096
5940
|
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 };
|
|
5097
|
-
const allTools = [
|
|
5941
|
+
const allTools = [
|
|
5942
|
+
...invocation.tools,
|
|
5943
|
+
...fileTools(invocation.cwd, invocation.allowRepoWrite, invocation.repositoryAccess)
|
|
5944
|
+
];
|
|
5098
5945
|
const toolsByName = new Map(allTools.map((tool2) => [tool2.name, tool2]));
|
|
5099
5946
|
const toolDefs = allTools.map((tool2) => ({
|
|
5100
5947
|
type: "function",
|
|
@@ -5125,7 +5972,8 @@ function createOpenAIProvider(config, session) {
|
|
|
5125
5972
|
input,
|
|
5126
5973
|
tools: toolDefs,
|
|
5127
5974
|
...roleConfig.effort ? { reasoning: { effort: roleConfig.effort } } : {},
|
|
5128
|
-
...roleConfig.serviceTier ? { service_tier: roleConfig.serviceTier } : {}
|
|
5975
|
+
...roleConfig.serviceTier ? { service_tier: roleConfig.serviceTier } : {},
|
|
5976
|
+
...roleConfig.maxOutputTokens ? { max_output_tokens: roleConfig.maxOutputTokens } : {}
|
|
5129
5977
|
}, sessionKey, task);
|
|
5130
5978
|
const items = response.output ?? [];
|
|
5131
5979
|
const calls = items.filter(
|
|
@@ -5181,7 +6029,7 @@ var Semaphore = class {
|
|
|
5181
6029
|
waiting = [];
|
|
5182
6030
|
async run(operation) {
|
|
5183
6031
|
if (this.active >= this.limit) {
|
|
5184
|
-
await new Promise((
|
|
6032
|
+
await new Promise((resolve8) => this.waiting.push(resolve8));
|
|
5185
6033
|
}
|
|
5186
6034
|
this.active += 1;
|
|
5187
6035
|
try {
|
|
@@ -5193,10 +6041,7 @@ var Semaphore = class {
|
|
|
5193
6041
|
}
|
|
5194
6042
|
};
|
|
5195
6043
|
function shouldFallback(outcome) {
|
|
5196
|
-
|
|
5197
|
-
return true;
|
|
5198
|
-
}
|
|
5199
|
-
return outcome.kind === "failed" && classifyFailure(outcome.error) !== "fatal";
|
|
6044
|
+
return outcome.kind !== "completed";
|
|
5200
6045
|
}
|
|
5201
6046
|
function shouldDisableProvider(outcome) {
|
|
5202
6047
|
return outcome.kind === "failed" && classifyFailure(outcome.error) === "transient";
|
|
@@ -5214,21 +6059,27 @@ function createRoutedProvider(routes, fallback, fallbackConcurrency = 2) {
|
|
|
5214
6059
|
...[...normalized.values()].map((route) => route.provider)
|
|
5215
6060
|
]);
|
|
5216
6061
|
const semaphores = /* @__PURE__ */ new Map();
|
|
5217
|
-
const
|
|
5218
|
-
const semaphoreFor = (
|
|
5219
|
-
let semaphore = semaphores.get(
|
|
6062
|
+
const disabledRoutes = /* @__PURE__ */ new Set();
|
|
6063
|
+
const semaphoreFor = (key, concurrency) => {
|
|
6064
|
+
let semaphore = semaphores.get(key);
|
|
5220
6065
|
if (!semaphore) {
|
|
5221
|
-
semaphore = new Semaphore(concurrency ??
|
|
5222
|
-
semaphores.set(
|
|
6066
|
+
semaphore = new Semaphore(concurrency ?? fallbackConcurrency);
|
|
6067
|
+
semaphores.set(key, semaphore);
|
|
5223
6068
|
}
|
|
5224
6069
|
return semaphore;
|
|
5225
6070
|
};
|
|
6071
|
+
const invocationRouteKey = (invocation, config) => `${invocation.task ?? invocation.role}:${config.model}`;
|
|
5226
6072
|
const routeFor = (invocation) => normalized.get(invocation.task ?? invocation.role) ?? normalized.get(invocation.role);
|
|
5227
|
-
const call = (provider, concurrency, invocation, config, onProgress) => semaphoreFor(
|
|
6073
|
+
const call = (provider, concurrency, invocation, config, onProgress, key = invocationRouteKey(invocation, config)) => semaphoreFor(key, concurrency).run(() => provider.invoke(invocation, config, onProgress));
|
|
6074
|
+
const fallbackInvocation = (invocation) => ({
|
|
6075
|
+
...invocation,
|
|
6076
|
+
fallback: true,
|
|
6077
|
+
sessionKey: `fallback:${invocation.sessionKey ?? invocation.task ?? invocation.role}`
|
|
6078
|
+
});
|
|
5228
6079
|
return {
|
|
5229
6080
|
name: "routed",
|
|
5230
6081
|
onRunReady(runId, resumeSessionId) {
|
|
5231
|
-
|
|
6082
|
+
disabledRoutes.clear();
|
|
5232
6083
|
for (const provider of providers) {
|
|
5233
6084
|
provider.onRunReady?.(runId, resumeSessionId);
|
|
5234
6085
|
}
|
|
@@ -5238,28 +6089,31 @@ function createRoutedProvider(routes, fallback, fallbackConcurrency = 2) {
|
|
|
5238
6089
|
if (!route) {
|
|
5239
6090
|
return call(fallback, fallbackConcurrency, invocation, config, onProgress);
|
|
5240
6091
|
}
|
|
5241
|
-
|
|
6092
|
+
const routeKey = invocationRouteKey(invocation, config);
|
|
6093
|
+
if (disabledRoutes.has(routeKey)) {
|
|
5242
6094
|
return call(
|
|
5243
6095
|
fallback,
|
|
5244
6096
|
fallbackConcurrency,
|
|
5245
|
-
invocation,
|
|
6097
|
+
fallbackInvocation(invocation),
|
|
5246
6098
|
route.fallbackConfig ?? config,
|
|
5247
|
-
onProgress
|
|
6099
|
+
onProgress,
|
|
6100
|
+
`fallback:${routeKey}`
|
|
5248
6101
|
);
|
|
5249
6102
|
}
|
|
5250
6103
|
return call(route.provider, route.concurrency, invocation, config, onProgress).then(
|
|
5251
6104
|
async (outcome) => {
|
|
5252
6105
|
if (shouldDisableProvider(outcome)) {
|
|
5253
|
-
|
|
6106
|
+
disabledRoutes.add(routeKey);
|
|
5254
6107
|
}
|
|
5255
6108
|
if (!invocation.allowRepoWrite && shouldFallback(outcome)) {
|
|
5256
6109
|
onProgress(`${route.provider.name} unavailable; continuing with ${fallback.name}`);
|
|
5257
6110
|
return call(
|
|
5258
6111
|
fallback,
|
|
5259
6112
|
fallbackConcurrency,
|
|
5260
|
-
invocation,
|
|
6113
|
+
fallbackInvocation(invocation),
|
|
5261
6114
|
route.fallbackConfig ?? config,
|
|
5262
|
-
onProgress
|
|
6115
|
+
onProgress,
|
|
6116
|
+
`fallback:${routeKey}`
|
|
5263
6117
|
);
|
|
5264
6118
|
}
|
|
5265
6119
|
return outcome;
|
|
@@ -5267,7 +6121,14 @@ function createRoutedProvider(routes, fallback, fallbackConcurrency = 2) {
|
|
|
5267
6121
|
);
|
|
5268
6122
|
},
|
|
5269
6123
|
invokeFallback(invocation, config, onProgress) {
|
|
5270
|
-
return call(
|
|
6124
|
+
return call(
|
|
6125
|
+
fallback,
|
|
6126
|
+
fallbackConcurrency,
|
|
6127
|
+
fallbackInvocation(invocation),
|
|
6128
|
+
config,
|
|
6129
|
+
onProgress,
|
|
6130
|
+
`fallback:${invocationRouteKey(invocation, config)}`
|
|
6131
|
+
);
|
|
5271
6132
|
}
|
|
5272
6133
|
};
|
|
5273
6134
|
}
|
|
@@ -5277,14 +6138,53 @@ var MINUTE = 6e4;
|
|
|
5277
6138
|
function engineModelConfig(config) {
|
|
5278
6139
|
const model = (task, fallback) => process.env[`WHISPERR_WIZARD_ENGINE_${task.toUpperCase()}_MODEL`]?.trim() || fallback;
|
|
5279
6140
|
const terra = "gpt-5.6-terra";
|
|
6141
|
+
const sol = "gpt-5.6-sol";
|
|
5280
6142
|
const opus = "claude-opus-5";
|
|
5281
6143
|
const opusOnly = process.env.WHISPERR_WIZARD_PRESET?.trim() === "claude-default" || config.offline === true;
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
6144
|
+
if (opusOnly) {
|
|
6145
|
+
const survey2 = { model: model("survey", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 5 * MINUTE };
|
|
6146
|
+
const design2 = { model: model("design", opus), effort: "medium", fastMode: true, maxTurns: 80, maxMs: 20 * MINUTE };
|
|
6147
|
+
const sdkSetup2 = { model: model("sdk_setup", opus), effort: "medium", fastMode: true, maxTurns: 60, maxMs: 12 * MINUTE };
|
|
6148
|
+
const clusterEdit2 = { model: model("cluster_edit", opus), effort: "medium", fastMode: true, maxTurns: 60, maxMs: 12 * MINUTE };
|
|
6149
|
+
const repair2 = { model: model("repair", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 8 * MINUTE };
|
|
6150
|
+
const review2 = { model: model("review", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 8 * MINUTE };
|
|
6151
|
+
return {
|
|
6152
|
+
survey: survey2,
|
|
6153
|
+
design: design2,
|
|
6154
|
+
edit: sdkSetup2,
|
|
6155
|
+
review: review2,
|
|
6156
|
+
sdk_setup: sdkSetup2,
|
|
6157
|
+
cluster_edit: clusterEdit2,
|
|
6158
|
+
repair: repair2
|
|
6159
|
+
};
|
|
6160
|
+
}
|
|
6161
|
+
const survey = { model: model("survey", opus), effort: "low", fastMode: true, maxTurns: 30, maxMs: 5 * MINUTE };
|
|
6162
|
+
const design = { model: model("design", opus), effort: "low", fastMode: true, maxTurns: 20, maxMs: 5 * MINUTE };
|
|
6163
|
+
const sdkSetup = { model: model("sdk_setup", opus), effort: "low", fastMode: true, maxTurns: 30, maxMs: 6 * MINUTE };
|
|
6164
|
+
const clusterEdit = opusOnly ? { model: model("cluster_edit", opus), effort: "medium", fastMode: true, maxTurns: 60, maxMs: 12 * MINUTE } : {
|
|
6165
|
+
model: model("cluster_edit", terra),
|
|
6166
|
+
effort: "medium",
|
|
6167
|
+
serviceTier: "priority",
|
|
6168
|
+
maxTurns: 20,
|
|
6169
|
+
maxMs: 4 * MINUTE,
|
|
6170
|
+
maxOutputTokens: 8e3
|
|
6171
|
+
};
|
|
6172
|
+
const repair = {
|
|
6173
|
+
model: model("repair", sol),
|
|
6174
|
+
effort: "medium",
|
|
6175
|
+
serviceTier: "priority",
|
|
6176
|
+
maxTurns: 30,
|
|
6177
|
+
maxMs: 8 * MINUTE,
|
|
6178
|
+
maxOutputTokens: 12e3
|
|
6179
|
+
};
|
|
6180
|
+
const review = {
|
|
6181
|
+
model: model("review", sol),
|
|
6182
|
+
effort: "medium",
|
|
6183
|
+
serviceTier: "priority",
|
|
6184
|
+
maxTurns: 8,
|
|
6185
|
+
maxMs: 3 * MINUTE,
|
|
6186
|
+
maxOutputTokens: 4e3
|
|
6187
|
+
};
|
|
5288
6188
|
return {
|
|
5289
6189
|
survey,
|
|
5290
6190
|
design,
|
|
@@ -5303,21 +6203,19 @@ function engineProvider(config, session) {
|
|
|
5303
6203
|
return createRoutedProvider({}, claude, 2);
|
|
5304
6204
|
}
|
|
5305
6205
|
const openai = createOpenAIProvider(config, session);
|
|
5306
|
-
if (preset === "sol-design") {
|
|
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);
|
|
5312
|
-
}
|
|
5313
6206
|
return createRoutedProvider({
|
|
5314
|
-
survey: { provider:
|
|
5315
|
-
|
|
5316
|
-
|
|
6207
|
+
survey: { provider: claude, concurrency: 3, fallbackConfig: models.repair },
|
|
6208
|
+
design: { provider: claude, concurrency: 3, fallbackConfig: models.repair },
|
|
6209
|
+
sdk_setup: { provider: claude, concurrency: 2, fallbackConfig: models.repair },
|
|
6210
|
+
cluster_edit: { provider: openai, concurrency: 4, fallbackConfig: models.repair },
|
|
6211
|
+
review: { provider: openai, concurrency: 2, fallbackConfig: models.review },
|
|
6212
|
+
repair: { provider: openai, concurrency: 2, fallbackConfig: models.repair }
|
|
6213
|
+
}, openai, 2);
|
|
5317
6214
|
}
|
|
5318
6215
|
async function tryEngineFlow(options) {
|
|
5319
6216
|
const { repoPath, fingerprint, config, session, playbook, theme: theme2 } = options;
|
|
5320
6217
|
const startedAt = Date.now();
|
|
6218
|
+
const models = engineModelConfig(config);
|
|
5321
6219
|
const sink = createProgressSink({
|
|
5322
6220
|
json: false,
|
|
5323
6221
|
isTTY: Boolean(process.stdout.isTTY),
|
|
@@ -5329,16 +6227,13 @@ async function tryEngineFlow(options) {
|
|
|
5329
6227
|
config,
|
|
5330
6228
|
session,
|
|
5331
6229
|
provider: engineProvider(config, session),
|
|
5332
|
-
models
|
|
6230
|
+
models,
|
|
5333
6231
|
target: playbook.target.id,
|
|
5334
6232
|
projectKind: ["node", "python", "php", "go", "express", "django", "fastapi", "laravel", "spring-boot"].includes(
|
|
5335
6233
|
playbook.target.id
|
|
5336
6234
|
) ? "backend" : "frontend",
|
|
5337
6235
|
playbookSystemPrompt: playbook.systemPrompt,
|
|
5338
|
-
sink
|
|
5339
|
-
callbacks: {
|
|
5340
|
-
reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2)
|
|
5341
|
-
}
|
|
6236
|
+
sink
|
|
5342
6237
|
});
|
|
5343
6238
|
if (result.kind === "wrong_mode") {
|
|
5344
6239
|
return { handled: false, exitCode: 0 };
|
|
@@ -5355,11 +6250,18 @@ async function tryEngineFlow(options) {
|
|
|
5355
6250
|
target: playbook.target.id,
|
|
5356
6251
|
repo_fingerprint: fingerprint,
|
|
5357
6252
|
identify_wired: result.identifyWired,
|
|
5358
|
-
verified: null,
|
|
6253
|
+
verified: result.verificationStatus === "passed" ? true : result.verificationStatus === "failed" ? false : null,
|
|
6254
|
+
verification_status: result.verificationStatus,
|
|
5359
6255
|
cost_usd: 0,
|
|
5360
6256
|
duration_ms: Date.now() - startedAt,
|
|
5361
6257
|
summary: result.summary.slice(0, 2e3),
|
|
5362
6258
|
events: result.reportEvents,
|
|
6259
|
+
wizard_version: packageVersion(),
|
|
6260
|
+
planner_model: models.survey.model,
|
|
6261
|
+
editor_model: models.cluster_edit?.model ?? models.edit.model,
|
|
6262
|
+
effort: models.cluster_edit?.effort ?? models.edit.effort,
|
|
6263
|
+
phase_timings: result.phaseTimings,
|
|
6264
|
+
model_routes: result.modelRoutes,
|
|
5363
6265
|
exit_class: result.kind === "completed" ? "engine_completed" : "engine_partial"
|
|
5364
6266
|
});
|
|
5365
6267
|
if (result.applied && result.registered.length > 0) {
|
|
@@ -5372,32 +6274,6 @@ async function tryEngineFlow(options) {
|
|
|
5372
6274
|
}
|
|
5373
6275
|
return { handled: true, exitCode: result.kind === "completed" ? 0 : 1 };
|
|
5374
6276
|
}
|
|
5375
|
-
async function reviewEventsInTerminal(proposed, theme2) {
|
|
5376
|
-
if (proposed.length === 0) {
|
|
5377
|
-
return [];
|
|
5378
|
-
}
|
|
5379
|
-
const lines = proposed.map((event) => {
|
|
5380
|
-
const fields = Object.keys(event.payloadSchema);
|
|
5381
|
-
return [
|
|
5382
|
-
`${theme2.bright(event.code)} \u2014 ${event.name}`,
|
|
5383
|
-
` ${event.reasoning}`,
|
|
5384
|
-
` anchor: ${event.anchorFile}${event.anchorSymbol ? ` (${event.anchorSymbol})` : ""}`,
|
|
5385
|
-
fields.length > 0 ? ` payload: ${fields.join(", ")}` : " payload: none"
|
|
5386
|
-
].join("\n");
|
|
5387
|
-
});
|
|
5388
|
-
p.note(lines.join("\n\n"), `Proposed events (${proposed.length})`);
|
|
5389
|
-
const selection = await p.multiselect({
|
|
5390
|
-
message: "Register and integrate these events? Deselect any you don't want.",
|
|
5391
|
-
options: proposed.map((event) => ({ value: event.code, label: event.code, hint: event.name })),
|
|
5392
|
-
initialValues: proposed.map((event) => event.code),
|
|
5393
|
-
required: false
|
|
5394
|
-
});
|
|
5395
|
-
if (p.isCancel(selection)) {
|
|
5396
|
-
return null;
|
|
5397
|
-
}
|
|
5398
|
-
const chosen = new Set(selection);
|
|
5399
|
-
return proposed.filter((event) => chosen.has(event.code));
|
|
5400
|
-
}
|
|
5401
6277
|
|
|
5402
6278
|
// src/core/opportunities.ts
|
|
5403
6279
|
function normalizeCode(value) {
|
|
@@ -5630,7 +6506,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5630
6506
|
return Promise.resolve({ ran: false, ok: true, output: "" });
|
|
5631
6507
|
}
|
|
5632
6508
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
5633
|
-
return new Promise((
|
|
6509
|
+
return new Promise((resolve8) => {
|
|
5634
6510
|
const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
|
|
5635
6511
|
let out = "";
|
|
5636
6512
|
const append = (d) => {
|
|
@@ -5641,11 +6517,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5641
6517
|
child.stderr?.on("data", append);
|
|
5642
6518
|
const timer = setTimeout(() => {
|
|
5643
6519
|
child.kill("SIGKILL");
|
|
5644
|
-
|
|
6520
|
+
resolve8({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
|
|
5645
6521
|
}, timeoutMs);
|
|
5646
6522
|
child.on("close", (code) => {
|
|
5647
6523
|
clearTimeout(timer);
|
|
5648
|
-
|
|
6524
|
+
resolve8({
|
|
5649
6525
|
ran: true,
|
|
5650
6526
|
ok: code === 0,
|
|
5651
6527
|
command,
|
|
@@ -5657,7 +6533,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5657
6533
|
});
|
|
5658
6534
|
child.on("error", () => {
|
|
5659
6535
|
clearTimeout(timer);
|
|
5660
|
-
|
|
6536
|
+
resolve8({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
|
|
5661
6537
|
});
|
|
5662
6538
|
});
|
|
5663
6539
|
}
|
|
@@ -5666,25 +6542,6 @@ function tail2(s) {
|
|
|
5666
6542
|
return t.length > MAX_OUTPUT ? `\u2026${t.slice(-MAX_OUTPUT)}` : t;
|
|
5667
6543
|
}
|
|
5668
6544
|
|
|
5669
|
-
// src/core/version.ts
|
|
5670
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
5671
|
-
import { dirname as dirname3, join as join7, parse } from "path";
|
|
5672
|
-
import { fileURLToPath } from "url";
|
|
5673
|
-
var PACKAGE_NAME = "@whisperr/wizard";
|
|
5674
|
-
function packageVersion() {
|
|
5675
|
-
let dir = dirname3(fileURLToPath(import.meta.url));
|
|
5676
|
-
const { root } = parse(dir);
|
|
5677
|
-
while (true) {
|
|
5678
|
-
try {
|
|
5679
|
-
const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf8"));
|
|
5680
|
-
if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
|
|
5681
|
-
} catch {
|
|
5682
|
-
}
|
|
5683
|
-
if (dir === root) return "0.0.0";
|
|
5684
|
-
dir = dirname3(dir);
|
|
5685
|
-
}
|
|
5686
|
-
}
|
|
5687
|
-
|
|
5688
6545
|
// src/core/gapreport.ts
|
|
5689
6546
|
function buildGapReport(input) {
|
|
5690
6547
|
const outcomes = new Map(
|
|
@@ -5814,7 +6671,7 @@ function banner() {
|
|
|
5814
6671
|
|
|
5815
6672
|
// src/cli.ts
|
|
5816
6673
|
async function run2(options) {
|
|
5817
|
-
const repoPath =
|
|
6674
|
+
const repoPath = resolve7(options.path ?? process.cwd());
|
|
5818
6675
|
const config = resolveConfig(options);
|
|
5819
6676
|
console.log(banner());
|
|
5820
6677
|
p2.intro(theme.signal("Let's wire Whisperr into your app."));
|
|
@@ -5926,40 +6783,23 @@ Flutter is live today; ${theme.bright(
|
|
|
5926
6783
|
}
|
|
5927
6784
|
const tick = useIntegrationSpinner ? setInterval(updateIntegrationMessage, 1e3) : void 0;
|
|
5928
6785
|
const additions = { proposed: 0, applied: [] };
|
|
5929
|
-
const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
5930
6786
|
const outcome = await runIntegrationAgent({
|
|
5931
6787
|
repoPath,
|
|
5932
6788
|
config,
|
|
5933
6789
|
session,
|
|
5934
6790
|
playbook: chosen.playbook,
|
|
5935
6791
|
manifest,
|
|
5936
|
-
// The plan is
|
|
5937
|
-
//
|
|
5938
|
-
// default run reads it out and keeps going.
|
|
6792
|
+
// The plan is informational. Event selection is frozen automatically, so
|
|
6793
|
+
// integration never pauses at a terminal approval prompt.
|
|
5939
6794
|
async onPlanReady(plan) {
|
|
5940
6795
|
if (useIntegrationSpinner) {
|
|
5941
6796
|
spin.stop(theme.success("Placement plan ready"));
|
|
5942
6797
|
}
|
|
5943
6798
|
p2.note(renderPlacementPlan(plan), "Placement plan");
|
|
5944
|
-
const placeEntries = plan.filter((entry) => entry.decision === "place");
|
|
5945
|
-
let selectedEvents = placeEntries.map((entry) => entry.event);
|
|
5946
|
-
if (config.reviewPlan && interactive && placeEntries.length) {
|
|
5947
|
-
const selection = await p2.multiselect({
|
|
5948
|
-
message: "Wire these events?",
|
|
5949
|
-
options: placeEntries.map((entry) => ({
|
|
5950
|
-
value: entry.event,
|
|
5951
|
-
label: entry.event,
|
|
5952
|
-
hint: `${entry.file ?? "unknown file"}@${entry.anchor ?? "unknown anchor"}`
|
|
5953
|
-
})),
|
|
5954
|
-
initialValues: selectedEvents,
|
|
5955
|
-
required: false
|
|
5956
|
-
});
|
|
5957
|
-
selectedEvents = p2.isCancel(selection) ? [] : selection;
|
|
5958
|
-
}
|
|
5959
6799
|
if (useIntegrationSpinner) {
|
|
5960
6800
|
spin.start(theme.bright("Continuing integration"));
|
|
5961
6801
|
}
|
|
5962
|
-
return
|
|
6802
|
+
return plan;
|
|
5963
6803
|
},
|
|
5964
6804
|
async onOpportunitiesReady(raw) {
|
|
5965
6805
|
if (useIntegrationSpinner) spin.stop(theme.success("Planning done"));
|
|
@@ -6527,12 +7367,6 @@ function formatDuration(ms) {
|
|
|
6527
7367
|
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
6528
7368
|
return `${minutes}m${seconds}s`;
|
|
6529
7369
|
}
|
|
6530
|
-
function filterEventPlan(plan, selectedEvents) {
|
|
6531
|
-
const selected = new Set(selectedEvents);
|
|
6532
|
-
return plan.map(
|
|
6533
|
-
(entry) => entry.decision === "place" && !selected.has(entry.event) ? { ...entry, decision: "skip", reason: "Deselected in plan review." } : entry
|
|
6534
|
-
);
|
|
6535
|
-
}
|
|
6536
7370
|
function renderPlacementPlan(plan) {
|
|
6537
7371
|
return plan.map((entry) => {
|
|
6538
7372
|
if (entry.decision === "place") {
|
|
@@ -6658,7 +7492,6 @@ function printHelp() {
|
|
|
6658
7492
|
` ${theme.bright("Options")}`,
|
|
6659
7493
|
" --offline Use a demo manifest, no account/browser needed",
|
|
6660
7494
|
" --force Proceed without a clean git tree (no safe undo)",
|
|
6661
|
-
" --review-plan Confirm the placement plan before anything is wired",
|
|
6662
7495
|
" --propose-only Send new events/interventions for dashboard approval",
|
|
6663
7496
|
" instead of adding them to your universe now",
|
|
6664
7497
|
" --strict-review Review the finished diff with the deeper planner model",
|