@whisperr/wizard 0.8.0 → 0.9.1
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 +1481 -636
- 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,128 @@ 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 outcome.kind === "completed";
|
|
1793
|
+
try {
|
|
1794
|
+
return await validate();
|
|
1795
|
+
} catch {
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
let valid = await passesValidation();
|
|
1800
|
+
if (valid && outcome.kind !== "completed") {
|
|
1801
|
+
deps.sink.emit({
|
|
1802
|
+
phase: "binding",
|
|
1803
|
+
action: `setup changes passed deterministic validation after ${outcome.kind.replaceAll("_", " ")}`
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
if (!valid && deps.provider.invokeFallback) {
|
|
1807
|
+
await deps.discardChanges();
|
|
1808
|
+
await writeFile(join4(deps.worktree.path, bindings.relativePath), bindings.content);
|
|
1809
|
+
deps.sink.emit({ phase: "binding", action: "Opus setup rolled back; retrying with Sol" });
|
|
1810
|
+
outcome = await deps.provider.invokeFallback(
|
|
1811
|
+
{ ...invocation, sessionKey: "sdk_setup:sol" },
|
|
1812
|
+
modelConfigFor(deps.models, "repair", "edit"),
|
|
1813
|
+
(action) => deps.sink.emit({ phase: "binding", action })
|
|
1814
|
+
);
|
|
1815
|
+
valid = await passesValidation();
|
|
1816
|
+
if (valid && outcome.kind !== "completed") {
|
|
1817
|
+
deps.sink.emit({
|
|
1818
|
+
phase: "binding",
|
|
1819
|
+
action: `Sol setup passed deterministic validation after ${outcome.kind.replaceAll("_", " ")}`
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
return valid;
|
|
1824
|
+
}
|
|
1825
|
+
async function sdkSetupFiles(repoPath, survey, bindingsFile) {
|
|
1826
|
+
const candidates = /* @__PURE__ */ new Set([
|
|
1827
|
+
bindingsFile,
|
|
1828
|
+
...survey?.stack.entryPoints ?? [],
|
|
1829
|
+
...survey?.identifyAnchors.map((anchor) => anchor.file) ?? [],
|
|
1830
|
+
...survey?.logoutAnchors.map((anchor) => anchor.file) ?? [],
|
|
1831
|
+
...survey?.featureAreas.flatMap(
|
|
1832
|
+
(area) => area.files.filter(isSdkConfigurationFile)
|
|
1833
|
+
) ?? [],
|
|
1834
|
+
"package.json",
|
|
1835
|
+
"package-lock.json",
|
|
1836
|
+
"pnpm-lock.yaml",
|
|
1837
|
+
"yarn.lock",
|
|
1838
|
+
"bun.lock",
|
|
1839
|
+
"bun.lockb",
|
|
1840
|
+
"pyproject.toml",
|
|
1841
|
+
"requirements.txt",
|
|
1842
|
+
"Pipfile",
|
|
1843
|
+
"Package.swift",
|
|
1844
|
+
"Podfile",
|
|
1845
|
+
"pubspec.yaml",
|
|
1846
|
+
"go.mod",
|
|
1847
|
+
"composer.json",
|
|
1848
|
+
"tsconfig.json",
|
|
1849
|
+
"next.config.js",
|
|
1850
|
+
"next.config.mjs",
|
|
1851
|
+
"next.config.ts",
|
|
1852
|
+
"vite.config.js",
|
|
1853
|
+
"vite.config.ts",
|
|
1854
|
+
"app.json",
|
|
1855
|
+
"Info.plist"
|
|
1856
|
+
]);
|
|
1857
|
+
const available = [];
|
|
1858
|
+
for (const file of candidates) {
|
|
1859
|
+
try {
|
|
1860
|
+
if ((await stat2(join4(repoPath, file))).isFile()) available.push(file.replaceAll("\\", "/"));
|
|
1861
|
+
} catch {
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
return [...new Set(available)].sort();
|
|
1865
|
+
}
|
|
1866
|
+
function isSdkConfigurationFile(file) {
|
|
1867
|
+
const normalized = file.replaceAll("\\", "/");
|
|
1868
|
+
return /(^|\/)(?:[^/]*config[^/]*|settings[^/]*|Info\.plist|AndroidManifest\.xml|Podfile|pubspec\.yaml)$/.test(
|
|
1869
|
+
normalized
|
|
1870
|
+
);
|
|
1780
1871
|
}
|
|
1781
1872
|
function clusterPrompt(cluster, registered, bindings) {
|
|
1782
1873
|
const byCode = new Map(registered.map((event) => [event.code, event]));
|
|
@@ -1803,39 +1894,7 @@ function clusterPrompt(cluster, registered, bindings) {
|
|
|
1803
1894
|
"Only edit the listed files (plus imports of the bindings module). Then stop."
|
|
1804
1895
|
].join("\n");
|
|
1805
1896
|
}
|
|
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) {
|
|
1897
|
+
async function runClusterIntegration(deps, initialClusters, _clusterDiff) {
|
|
1839
1898
|
let clusters = initialClusters;
|
|
1840
1899
|
const eventStatuses = /* @__PURE__ */ new Map();
|
|
1841
1900
|
const bindings = generateBindings(deps.target, deps.registered);
|
|
@@ -1861,6 +1920,7 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
|
1861
1920
|
systemPrompt: deps.playbookSystemPrompt,
|
|
1862
1921
|
userPrompt: clusterPrompt(cluster, deps.registered, bindings),
|
|
1863
1922
|
tools: [],
|
|
1923
|
+
repositoryAccess: { mode: "cluster", allowedFiles: cluster.files },
|
|
1864
1924
|
allowRepoWrite: true,
|
|
1865
1925
|
cwd: deps.worktree.path
|
|
1866
1926
|
};
|
|
@@ -1869,24 +1929,22 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
|
1869
1929
|
modelConfigFor(deps.models, "cluster_edit", "edit"),
|
|
1870
1930
|
(action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
|
|
1871
1931
|
);
|
|
1872
|
-
let
|
|
1873
|
-
|
|
1874
|
-
if ((!structural.ok || edit.kind !== "completed") && deps.provider.invokeFallback) {
|
|
1932
|
+
let structural = await structuralClusterCheck(deps.worktree.path, cluster);
|
|
1933
|
+
if (!structural.ok && deps.provider.invokeFallback) {
|
|
1875
1934
|
await deps.discardChanges();
|
|
1876
|
-
escalated = true;
|
|
1877
1935
|
deps.sink.emit({
|
|
1878
1936
|
phase: "integrating",
|
|
1879
1937
|
cluster: { index, total },
|
|
1880
|
-
action: "Terra attempt rolled back; retrying with
|
|
1938
|
+
action: "Terra attempt rolled back; retrying the clean cluster with Sol"
|
|
1881
1939
|
});
|
|
1882
1940
|
edit = await deps.provider.invokeFallback(
|
|
1883
|
-
{ ...invocation, task: "repair", sessionKey: `
|
|
1884
|
-
modelConfigFor(deps.models, "
|
|
1941
|
+
{ ...invocation, task: "repair", sessionKey: `cluster_sol:${cluster.id}` },
|
|
1942
|
+
modelConfigFor(deps.models, "repair", "edit"),
|
|
1885
1943
|
(action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
|
|
1886
1944
|
);
|
|
1887
|
-
structural =
|
|
1945
|
+
structural = await structuralClusterCheck(deps.worktree.path, cluster);
|
|
1888
1946
|
}
|
|
1889
|
-
if (
|
|
1947
|
+
if (!structural.ok) {
|
|
1890
1948
|
const failure = edit.kind === "context_overflow" ? "context_overflow" : edit.kind === "budget_exhausted" ? "context_overflow" : classifyFailure(edit.kind === "failed" ? edit.error : "");
|
|
1891
1949
|
const nextStatus = nextClusterStatusAfterFailure(failure, cluster.attempts);
|
|
1892
1950
|
clusters = transitionCluster(
|
|
@@ -1902,44 +1960,9 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
|
1902
1960
|
await deps.discardChanges();
|
|
1903
1961
|
continue;
|
|
1904
1962
|
}
|
|
1963
|
+
const finalStatus = "syntax_verified";
|
|
1905
1964
|
clusters = transitionCluster(clusters, cluster.id, "verified");
|
|
1906
1965
|
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
1966
|
for (const placement of cluster.events) {
|
|
1944
1967
|
eventStatuses.set(placement.eventCode, finalStatus);
|
|
1945
1968
|
}
|
|
@@ -1966,9 +1989,19 @@ async function structuralClusterCheck(repoPath, cluster) {
|
|
|
1966
1989
|
continue;
|
|
1967
1990
|
}
|
|
1968
1991
|
const content = await readFile2(path, "utf8");
|
|
1969
|
-
|
|
1992
|
+
const wrapper = eventWrapperName(placement.eventCode);
|
|
1993
|
+
if (!content.includes(wrapper)) {
|
|
1970
1994
|
missing.push(`${placement.eventCode}: wrapper call missing from ${placement.file}`);
|
|
1971
1995
|
}
|
|
1996
|
+
if (content.includes("<<<<<<<") || content.includes(">>>>>>>")) {
|
|
1997
|
+
missing.push(`${placement.eventCode}: merge conflict marker in ${placement.file}`);
|
|
1998
|
+
}
|
|
1999
|
+
if (/\.(?:ts|tsx|js|jsx|swift|dart|kt|java|go|py|php)$/.test(placement.file) && !balancedSourceDelimiters(content)) {
|
|
2000
|
+
missing.push(`${placement.eventCode}: basic syntax delimiters are unbalanced in ${placement.file}`);
|
|
2001
|
+
}
|
|
2002
|
+
if (/\.(?:ts|tsx|js|jsx)$/.test(placement.file) && content.includes("WhisperrEvents.") && !/import[\s\S]{0,300}\bWhisperrEvents\b/.test(content)) {
|
|
2003
|
+
missing.push(`${placement.eventCode}: WhisperrEvents import is missing from ${placement.file}`);
|
|
2004
|
+
}
|
|
1972
2005
|
} catch {
|
|
1973
2006
|
missing.push(`${placement.eventCode}: cannot read ${placement.file}`);
|
|
1974
2007
|
}
|
|
@@ -1978,25 +2011,221 @@ async function structuralClusterCheck(repoPath, cluster) {
|
|
|
1978
2011
|
notes: missing.join("; ")
|
|
1979
2012
|
};
|
|
1980
2013
|
}
|
|
1981
|
-
|
|
1982
|
-
const
|
|
2014
|
+
function balancedSourceDelimiters(content) {
|
|
2015
|
+
const stack = [];
|
|
2016
|
+
const pairs = { ")": "(", "]": "[", "}": "{" };
|
|
2017
|
+
let quote = "";
|
|
2018
|
+
let escaped = false;
|
|
2019
|
+
let lineComment = false;
|
|
2020
|
+
let blockComment = false;
|
|
2021
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
2022
|
+
const char = content[index];
|
|
2023
|
+
const next = content[index + 1] ?? "";
|
|
2024
|
+
if (lineComment) {
|
|
2025
|
+
if (char === "\n") lineComment = false;
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
if (blockComment) {
|
|
2029
|
+
if (char === "*" && next === "/") {
|
|
2030
|
+
blockComment = false;
|
|
2031
|
+
index += 1;
|
|
2032
|
+
}
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
if (quote) {
|
|
2036
|
+
if (escaped) {
|
|
2037
|
+
escaped = false;
|
|
2038
|
+
} else if (char === "\\") {
|
|
2039
|
+
escaped = true;
|
|
2040
|
+
} else if (char === quote) {
|
|
2041
|
+
quote = "";
|
|
2042
|
+
}
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
if (char === "/" && next === "/") {
|
|
2046
|
+
lineComment = true;
|
|
2047
|
+
index += 1;
|
|
2048
|
+
continue;
|
|
2049
|
+
}
|
|
2050
|
+
if (char === "/" && next === "*") {
|
|
2051
|
+
blockComment = true;
|
|
2052
|
+
index += 1;
|
|
2053
|
+
continue;
|
|
2054
|
+
}
|
|
2055
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
2056
|
+
quote = char;
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
if (char === "(" || char === "[" || char === "{") stack.push(char);
|
|
2060
|
+
if (pairs[char] && stack.pop() !== pairs[char]) return false;
|
|
2061
|
+
}
|
|
2062
|
+
return stack.length === 0 && !quote && !blockComment;
|
|
2063
|
+
}
|
|
2064
|
+
async function reviewIntegratedFile(deps, file, events, sessionSuffix = "") {
|
|
2065
|
+
let verdict;
|
|
2066
|
+
let issues = [];
|
|
2067
|
+
let notes = "";
|
|
2068
|
+
const expectedCodes = new Set(events.map((event) => event.code));
|
|
2069
|
+
const finishTool = {
|
|
2070
|
+
name: "finish_review",
|
|
2071
|
+
description: "Return the semantic verdict for this one file and its exact event contracts.",
|
|
2072
|
+
inputSchema: {
|
|
2073
|
+
verdict: z.enum(["pass", "fail"]),
|
|
2074
|
+
issues: z.array(z.object({
|
|
2075
|
+
eventCode: z.string(),
|
|
2076
|
+
reason: z.string()
|
|
2077
|
+
})).max(100),
|
|
2078
|
+
notes: z.string().optional()
|
|
2079
|
+
},
|
|
2080
|
+
jsonSchema: {
|
|
2081
|
+
type: "object",
|
|
2082
|
+
properties: {
|
|
2083
|
+
verdict: { type: "string", enum: ["pass", "fail"] },
|
|
2084
|
+
issues: {
|
|
2085
|
+
type: "array",
|
|
2086
|
+
maxItems: 100,
|
|
2087
|
+
items: {
|
|
2088
|
+
type: "object",
|
|
2089
|
+
properties: {
|
|
2090
|
+
eventCode: { type: "string" },
|
|
2091
|
+
reason: { type: "string" }
|
|
2092
|
+
},
|
|
2093
|
+
required: ["eventCode", "reason"]
|
|
2094
|
+
}
|
|
2095
|
+
},
|
|
2096
|
+
notes: { type: "string" }
|
|
2097
|
+
},
|
|
2098
|
+
required: ["verdict", "issues"]
|
|
2099
|
+
},
|
|
2100
|
+
handler: async (input) => {
|
|
2101
|
+
const rawIssues = Array.isArray(input.issues) ? input.issues : [];
|
|
2102
|
+
const normalized = [];
|
|
2103
|
+
for (const candidate of rawIssues) {
|
|
2104
|
+
const issue = candidate;
|
|
2105
|
+
const eventCode = String(issue.eventCode ?? "").trim();
|
|
2106
|
+
const reason = String(issue.reason ?? "").trim();
|
|
2107
|
+
if (!expectedCodes.has(eventCode) || !reason) {
|
|
2108
|
+
return { ok: false, reason: "issues must name an expected event and a concrete reason" };
|
|
2109
|
+
}
|
|
2110
|
+
normalized.push({ eventCode, reason });
|
|
2111
|
+
}
|
|
2112
|
+
verdict = input.verdict === "pass" ? "pass" : "fail";
|
|
2113
|
+
issues = normalized;
|
|
2114
|
+
notes = String(input.notes ?? "").trim();
|
|
2115
|
+
if (verdict === "pass" && issues.length > 0) {
|
|
2116
|
+
return { ok: false, reason: "a passing verdict cannot include issues" };
|
|
2117
|
+
}
|
|
2118
|
+
return { ok: true };
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
const content = await readFile2(resolve(deps.worktree.path, file), "utf8");
|
|
2122
|
+
const contracts = events.map((event) => ({
|
|
2123
|
+
code: event.code,
|
|
2124
|
+
reasoning: event.reasoning,
|
|
2125
|
+
anchorFile: event.anchorFile,
|
|
2126
|
+
anchorSymbol: event.anchorSymbol,
|
|
2127
|
+
payloadSchema: event.payloadSchema
|
|
2128
|
+
}));
|
|
2129
|
+
const surveyAnchors = deps.survey?.integratableCallSites.filter((anchor) => anchor.file === file) ?? [];
|
|
2130
|
+
const key = createHash2("sha256").update(file).digest("hex").slice(0, 12);
|
|
2131
|
+
const outcome = await deps.provider.invoke(
|
|
2132
|
+
{
|
|
2133
|
+
role: "review",
|
|
2134
|
+
task: "review",
|
|
2135
|
+
sessionKey: `review:${key}${sessionSuffix}`,
|
|
2136
|
+
systemPrompt: [
|
|
2137
|
+
"You are the file-isolated semantic review pass for product analytics instrumentation.",
|
|
2138
|
+
"Use finish_review exactly once. Do not browse the repository and do not edit files."
|
|
2139
|
+
].join("\n"),
|
|
2140
|
+
userPrompt: [
|
|
2141
|
+
"Validate every expected wrapper call for:",
|
|
2142
|
+
"- placement after the confirmed outcome rather than intent",
|
|
2143
|
+
"- deduplication, especially view/render/navigation events",
|
|
2144
|
+
"- payload correctness against the exact contract",
|
|
2145
|
+
"- absence of personal identifiers",
|
|
2146
|
+
"- generated wrapper usage rather than raw track calls",
|
|
2147
|
+
"",
|
|
2148
|
+
`Event contracts:
|
|
2149
|
+
${JSON.stringify(contracts)}`,
|
|
2150
|
+
`Saved survey anchors:
|
|
2151
|
+
${JSON.stringify(surveyAnchors)}`,
|
|
2152
|
+
"",
|
|
2153
|
+
`Expanded context for ${file}:
|
|
2154
|
+
${content.slice(0, 6e4)}`
|
|
2155
|
+
].join("\n"),
|
|
2156
|
+
tools: [finishTool],
|
|
2157
|
+
repositoryAccess: { mode: "review" },
|
|
2158
|
+
allowRepoWrite: false,
|
|
2159
|
+
cwd: deps.worktree.path
|
|
2160
|
+
},
|
|
2161
|
+
modelConfigFor(deps.models, "review", "review"),
|
|
2162
|
+
(action) => deps.sink.emit({ phase: "reviewing", file, action })
|
|
2163
|
+
);
|
|
2164
|
+
if (outcome.kind !== "completed" || verdict === void 0) {
|
|
2165
|
+
return {
|
|
2166
|
+
ok: false,
|
|
2167
|
+
issues: events.map((event) => ({
|
|
2168
|
+
eventCode: event.code,
|
|
2169
|
+
reason: `semantic review did not finish (${outcome.kind})`
|
|
2170
|
+
})),
|
|
2171
|
+
notes: `semantic review did not finish (${outcome.kind})`
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
return { ok: verdict === "pass" && issues.length === 0, issues, notes };
|
|
2175
|
+
}
|
|
2176
|
+
async function repairIntegratedFile(deps, file, events, review) {
|
|
2177
|
+
const key = createHash2("sha256").update(file).digest("hex").slice(0, 12);
|
|
2178
|
+
deps.sink.emit({ phase: "reviewing", file, action: "Sol is repairing semantic review failures" });
|
|
2179
|
+
const outcome = await deps.provider.invoke(
|
|
2180
|
+
{
|
|
2181
|
+
role: "edit",
|
|
2182
|
+
task: "repair",
|
|
2183
|
+
sessionKey: `semantic_repair:${key}`,
|
|
2184
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
2185
|
+
userPrompt: [
|
|
2186
|
+
`Repair only ${file}.`,
|
|
2187
|
+
"Preserve the exact event set and generated wrappers. Do not add events or edit other files.",
|
|
2188
|
+
"Correct placement timing, deduplication, payloads, PII safety, and wrapper usage as reported.",
|
|
2189
|
+
`Issues:
|
|
2190
|
+
${JSON.stringify(review.issues)}`,
|
|
2191
|
+
`Contracts:
|
|
2192
|
+
${JSON.stringify(events.map((event) => ({
|
|
2193
|
+
code: event.code,
|
|
2194
|
+
anchorSymbol: event.anchorSymbol,
|
|
2195
|
+
payloadSchema: event.payloadSchema,
|
|
2196
|
+
reasoning: event.reasoning
|
|
2197
|
+
})))}`
|
|
2198
|
+
].join("\n"),
|
|
2199
|
+
tools: [],
|
|
2200
|
+
repositoryAccess: { mode: "repair", allowedFiles: [file] },
|
|
2201
|
+
allowRepoWrite: true,
|
|
2202
|
+
cwd: deps.worktree.path
|
|
2203
|
+
},
|
|
2204
|
+
modelConfigFor(deps.models, "repair", "edit"),
|
|
2205
|
+
(action) => deps.sink.emit({ phase: "reviewing", file, action })
|
|
2206
|
+
);
|
|
2207
|
+
return outcome.kind === "completed";
|
|
2208
|
+
}
|
|
2209
|
+
async function finalizeIntegration(deps, verifyCwd = deps.worktree.path) {
|
|
2210
|
+
const steps = resolveVerifySteps(verifyCwd, deps.target);
|
|
1983
2211
|
deps.sink.emit({ phase: "verifying", action: "final verification pass" });
|
|
1984
|
-
const results = await runVerifySteps(
|
|
2212
|
+
const results = await runVerifySteps(verifyCwd, steps);
|
|
1985
2213
|
return {
|
|
1986
2214
|
results,
|
|
1987
2215
|
status: runVerificationStatus(results),
|
|
1988
2216
|
command: verificationCommand(results)
|
|
1989
2217
|
};
|
|
1990
2218
|
}
|
|
1991
|
-
async function repairVerification(deps, results) {
|
|
2219
|
+
async function repairVerification(deps, results, cycle = 1) {
|
|
1992
2220
|
const failed = results.filter((result) => !result.ok);
|
|
1993
2221
|
if (failed.length === 0) return true;
|
|
1994
|
-
deps.sink.emit({ phase: "verifying", action: "
|
|
2222
|
+
deps.sink.emit({ phase: "verifying", action: "Sol is repairing final verification failures" });
|
|
2223
|
+
const allowedFiles = deps.allowedRepairFiles ?? deps.registered.map((event) => event.anchorFile);
|
|
1995
2224
|
const outcome = await deps.provider.invoke(
|
|
1996
2225
|
{
|
|
1997
2226
|
role: "edit",
|
|
1998
2227
|
task: "repair",
|
|
1999
|
-
sessionKey:
|
|
2228
|
+
sessionKey: `final_repair:${cycle}`,
|
|
2000
2229
|
systemPrompt: deps.playbookSystemPrompt,
|
|
2001
2230
|
userPrompt: [
|
|
2002
2231
|
"The one final repository verification failed after all instrumentation patches were merged.",
|
|
@@ -2007,6 +2236,7 @@ async function repairVerification(deps, results) {
|
|
|
2007
2236
|
${result.output}`).join("\n\n").slice(0, 3e4)
|
|
2008
2237
|
].join("\n"),
|
|
2009
2238
|
tools: [],
|
|
2239
|
+
repositoryAccess: { mode: "repair", allowedFiles },
|
|
2010
2240
|
allowRepoWrite: true,
|
|
2011
2241
|
cwd: deps.worktree.path
|
|
2012
2242
|
},
|
|
@@ -2017,7 +2247,7 @@ ${result.output}`).join("\n\n").slice(0, 3e4)
|
|
|
2017
2247
|
}
|
|
2018
2248
|
|
|
2019
2249
|
// src/engine/runs.ts
|
|
2020
|
-
import { createHash as
|
|
2250
|
+
import { createHash as createHash3 } from "crypto";
|
|
2021
2251
|
var RunsApiError = class extends Error {
|
|
2022
2252
|
constructor(message, status, code) {
|
|
2023
2253
|
super(message);
|
|
@@ -2029,7 +2259,7 @@ var RunsApiError = class extends Error {
|
|
|
2029
2259
|
code;
|
|
2030
2260
|
};
|
|
2031
2261
|
function itemIdempotencyKey(kind, code, extra = "") {
|
|
2032
|
-
return
|
|
2262
|
+
return createHash3("sha256").update(`${kind}
|
|
2033
2263
|
${code}
|
|
2034
2264
|
${extra}`).digest("hex").slice(0, 32);
|
|
2035
2265
|
}
|
|
@@ -2091,6 +2321,20 @@ var RunsClient = class {
|
|
|
2091
2321
|
payload
|
|
2092
2322
|
}).then(normalizeItemWriteResult);
|
|
2093
2323
|
}
|
|
2324
|
+
persistSurveyArtifact(runId, content, metadata) {
|
|
2325
|
+
return this.request(
|
|
2326
|
+
"POST",
|
|
2327
|
+
`/wizard/runs/${runId}/artifacts/survey`,
|
|
2328
|
+
artifactRequest(content, metadata)
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
commitDesign(runId, content, events, metadata) {
|
|
2332
|
+
return this.request(
|
|
2333
|
+
"POST",
|
|
2334
|
+
`/wizard/runs/${runId}/design/commit`,
|
|
2335
|
+
{ ...artifactRequest(content, metadata), events }
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2094
2338
|
completeRun(runId, completion) {
|
|
2095
2339
|
return this.request(
|
|
2096
2340
|
"POST",
|
|
@@ -2150,9 +2394,59 @@ function normalizeRunBootstrap(payload) {
|
|
|
2150
2394
|
setupStatus: stringValue(snapshotSource.setupStatus),
|
|
2151
2395
|
events
|
|
2152
2396
|
},
|
|
2153
|
-
ingestion: { apiKey: ingestion.apiKey, baseUrl: ingestion.baseUrl }
|
|
2397
|
+
ingestion: { apiKey: ingestion.apiKey, baseUrl: ingestion.baseUrl },
|
|
2398
|
+
artifacts: normalizeArtifacts(value.artifacts ?? snapshotSource.artifacts)
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
function normalizeArtifacts(payload) {
|
|
2402
|
+
if (!Array.isArray(payload)) return [];
|
|
2403
|
+
return payload.flatMap((candidate) => {
|
|
2404
|
+
const value = record(candidate);
|
|
2405
|
+
const kind = value?.kind;
|
|
2406
|
+
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") {
|
|
2407
|
+
return [];
|
|
2408
|
+
}
|
|
2409
|
+
return [{
|
|
2410
|
+
kind,
|
|
2411
|
+
content: value.content,
|
|
2412
|
+
contentHash: value.contentHash,
|
|
2413
|
+
model: value.model,
|
|
2414
|
+
...typeof value.effort === "string" ? { effort: value.effort } : {},
|
|
2415
|
+
...typeof value.serviceTier === "string" ? { serviceTier: value.serviceTier } : {},
|
|
2416
|
+
fastMode: value.fastMode === true,
|
|
2417
|
+
...typeof value.frozenAt === "string" ? { frozenAt: value.frozenAt } : {},
|
|
2418
|
+
createdAt: value.createdAt
|
|
2419
|
+
}];
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
function artifactRequest(content, metadata) {
|
|
2423
|
+
return {
|
|
2424
|
+
content,
|
|
2425
|
+
contentHash: artifactContentHash(content),
|
|
2426
|
+
model: metadata.model,
|
|
2427
|
+
...metadata.effort ? { effort: metadata.effort } : {},
|
|
2428
|
+
...metadata.serviceTier ? { serviceTier: metadata.serviceTier } : {},
|
|
2429
|
+
fastMode: metadata.fastMode === true
|
|
2154
2430
|
};
|
|
2155
2431
|
}
|
|
2432
|
+
function artifactContentHash(content) {
|
|
2433
|
+
return createHash3("sha256").update(stableJSONStringify(content)).digest("hex");
|
|
2434
|
+
}
|
|
2435
|
+
function stableJSONStringify(value) {
|
|
2436
|
+
if (Array.isArray(value)) {
|
|
2437
|
+
return `[${value.map(stableJSONStringify).join(",")}]`;
|
|
2438
|
+
}
|
|
2439
|
+
if (value && typeof value === "object") {
|
|
2440
|
+
const entries = Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right));
|
|
2441
|
+
return `{${entries.map(
|
|
2442
|
+
([key, item]) => `${canonicalJSONString(key)}:${stableJSONStringify(item)}`
|
|
2443
|
+
).join(",")}}`;
|
|
2444
|
+
}
|
|
2445
|
+
return canonicalJSONString(value);
|
|
2446
|
+
}
|
|
2447
|
+
function canonicalJSONString(value) {
|
|
2448
|
+
return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
2449
|
+
}
|
|
2156
2450
|
function normalizeIntegrationProgress(payload) {
|
|
2157
2451
|
const value = record(payload);
|
|
2158
2452
|
if (!value || !Array.isArray(value.changedFiles) || !Array.isArray(value.events)) {
|
|
@@ -2190,7 +2484,9 @@ function isSnapshotEvent(value) {
|
|
|
2190
2484
|
}
|
|
2191
2485
|
|
|
2192
2486
|
// src/engine/selection.ts
|
|
2193
|
-
import {
|
|
2487
|
+
import { readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
2488
|
+
import { resolve as resolve2, sep as sep2 } from "path";
|
|
2489
|
+
import { z as z2 } from "zod";
|
|
2194
2490
|
|
|
2195
2491
|
// src/engine/prompts.ts
|
|
2196
2492
|
var SURVEY_SYSTEM_PROMPT = [
|
|
@@ -2198,8 +2494,8 @@ var SURVEY_SYSTEM_PROMPT = [
|
|
|
2198
2494
|
"Map the repository so a later pass can pick the product's important lifecycle events.",
|
|
2199
2495
|
"You cannot edit files. Be fast and decisive; read entry points and feature roots, not every file.",
|
|
2200
2496
|
"",
|
|
2201
|
-
"
|
|
2202
|
-
"1. Stack: frameworks, package manager, app entry point
|
|
2497
|
+
"Finish by calling finish_survey exactly once. Its structured fields must contain:",
|
|
2498
|
+
"1. Stack: frameworks, package manager, and app entry point files.",
|
|
2203
2499
|
"2. Feature areas: name, root directory, and every relevant file containing",
|
|
2204
2500
|
" committed end-user actions or meaningful lifecycle transitions.",
|
|
2205
2501
|
"3. The end-user identify() anchor: the login/signup success and session-restore call sites for the",
|
|
@@ -2208,7 +2504,8 @@ var SURVEY_SYSTEM_PROMPT = [
|
|
|
2208
2504
|
"5. Exhaustive inventory of integratable call sites: file, function/symbol,",
|
|
2209
2505
|
" confirmed outcome or state transition, available non-PII payload fields,",
|
|
2210
2506
|
" and whether similar paths exist elsewhere.",
|
|
2211
|
-
"Cite concrete
|
|
2507
|
+
"Cite concrete repository-relative paths for every claim. Do not propose event names yet.",
|
|
2508
|
+
"Never copy raw source code into the result."
|
|
2212
2509
|
].join("\n");
|
|
2213
2510
|
function surveyUserPrompt(repoPath, onboardingContext) {
|
|
2214
2511
|
return [
|
|
@@ -2216,13 +2513,13 @@ function surveyUserPrompt(repoPath, onboardingContext) {
|
|
|
2216
2513
|
"",
|
|
2217
2514
|
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
2218
2515
|
"",
|
|
2219
|
-
"
|
|
2516
|
+
"Complete the structured survey now."
|
|
2220
2517
|
].join("\n");
|
|
2221
2518
|
}
|
|
2222
2519
|
var SELECTION_SYSTEM_PROMPT = [
|
|
2223
2520
|
"You are the event-selection pass of the Whisperr integration wizard.",
|
|
2224
2521
|
"From the survey report and business context, choose the IMPORTANT product events this codebase",
|
|
2225
|
-
"can actually emit, and propose
|
|
2522
|
+
"can actually emit, and propose them with propose_event_batch calls.",
|
|
2226
2523
|
"",
|
|
2227
2524
|
"Rules \u2014 these are hard constraints:",
|
|
2228
2525
|
"- Only propose an event with a concrete call site: a real file and function where the committed",
|
|
@@ -2250,11 +2547,11 @@ var SELECTION_SYSTEM_PROMPT = [
|
|
|
2250
2547
|
" government ids, usernames, passwords, device ids, or IP addresses. Domain metrics are welcome.",
|
|
2251
2548
|
"- The end user is the paying customer/consumer. Never instrument admin, staff, or operator actions.",
|
|
2252
2549
|
"",
|
|
2253
|
-
"
|
|
2550
|
+
"Use only propose_event_batch for proposals. When done, call finish_event_design with a one-line summary."
|
|
2254
2551
|
].join("\n");
|
|
2255
2552
|
function selectionUserPrompt(surveyReport, onboardingContext, existingEventCodes, rejectedCodes) {
|
|
2256
2553
|
return [
|
|
2257
|
-
"
|
|
2554
|
+
"Saved structured survey artifact:",
|
|
2258
2555
|
surveyReport,
|
|
2259
2556
|
"",
|
|
2260
2557
|
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
@@ -2365,40 +2662,108 @@ function validateProposedEvent(input, seen, rejected) {
|
|
|
2365
2662
|
};
|
|
2366
2663
|
}
|
|
2367
2664
|
async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSessionId) {
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2665
|
+
let survey;
|
|
2666
|
+
let finished = false;
|
|
2667
|
+
const finishSurvey = {
|
|
2668
|
+
name: "finish_survey",
|
|
2669
|
+
description: "Return the complete structured repository survey without raw source code.",
|
|
2670
|
+
inputSchema: {
|
|
2671
|
+
stack: z2.object({
|
|
2672
|
+
frameworks: z2.array(z2.string()).max(30),
|
|
2673
|
+
packageManager: z2.string().optional(),
|
|
2674
|
+
entryPoints: z2.array(z2.string()).max(50)
|
|
2675
|
+
}),
|
|
2676
|
+
featureAreas: z2.array(z2.object({
|
|
2677
|
+
name: z2.string(),
|
|
2678
|
+
root: z2.string(),
|
|
2679
|
+
files: z2.array(z2.string()).max(200)
|
|
2680
|
+
})).max(200),
|
|
2681
|
+
identifyAnchors: z2.array(z2.object({
|
|
2682
|
+
file: z2.string(),
|
|
2683
|
+
symbol: z2.string().optional(),
|
|
2684
|
+
description: z2.string()
|
|
2685
|
+
})).max(50),
|
|
2686
|
+
logoutAnchors: z2.array(z2.object({
|
|
2687
|
+
file: z2.string(),
|
|
2688
|
+
symbol: z2.string().optional(),
|
|
2689
|
+
description: z2.string()
|
|
2690
|
+
})).max(50),
|
|
2691
|
+
existingAnalytics: z2.array(z2.object({
|
|
2692
|
+
file: z2.string(),
|
|
2693
|
+
symbol: z2.string().optional(),
|
|
2694
|
+
description: z2.string()
|
|
2695
|
+
})).max(100),
|
|
2696
|
+
integratableCallSites: z2.array(z2.object({
|
|
2697
|
+
file: z2.string(),
|
|
2698
|
+
symbol: z2.string().optional(),
|
|
2699
|
+
description: z2.string(),
|
|
2700
|
+
outcome: z2.string(),
|
|
2701
|
+
payloadFields: z2.array(z2.string()).max(50),
|
|
2702
|
+
similarPaths: z2.array(z2.string()).max(50)
|
|
2703
|
+
})).max(500)
|
|
2380
2704
|
},
|
|
2705
|
+
jsonSchema: surveyJSONSchema(),
|
|
2706
|
+
handler: async (input) => {
|
|
2707
|
+
if (finished) {
|
|
2708
|
+
return { ok: false, reason: "survey is already frozen" };
|
|
2709
|
+
}
|
|
2710
|
+
const normalized = normalizeSurveyArtifact(input);
|
|
2711
|
+
if (!normalized) {
|
|
2712
|
+
return { ok: false, reason: "survey fields or repository-relative paths are invalid" };
|
|
2713
|
+
}
|
|
2714
|
+
survey = normalized;
|
|
2715
|
+
finished = true;
|
|
2716
|
+
return { ok: true, callSites: normalized.integratableCallSites.length };
|
|
2717
|
+
}
|
|
2718
|
+
};
|
|
2719
|
+
sink.emit({ phase: "surveying", action: "mapping the repository" });
|
|
2720
|
+
const invocation = {
|
|
2721
|
+
role: "survey",
|
|
2722
|
+
task: "survey",
|
|
2723
|
+
sessionKey: "survey",
|
|
2724
|
+
systemPrompt: SURVEY_SYSTEM_PROMPT,
|
|
2725
|
+
userPrompt: surveyUserPrompt(repoPath, renderOnboardingContext(bootstrap.onboardingContext)),
|
|
2726
|
+
tools: [finishSurvey],
|
|
2727
|
+
repositoryAccess: { mode: "survey" },
|
|
2728
|
+
allowRepoWrite: false,
|
|
2729
|
+
cwd: repoPath,
|
|
2730
|
+
resumeSessionId
|
|
2731
|
+
};
|
|
2732
|
+
let outcome = await provider.invoke(
|
|
2733
|
+
invocation,
|
|
2381
2734
|
modelConfigFor(models, "survey", "survey"),
|
|
2382
2735
|
(action) => sink.emit({ phase: "surveying", action })
|
|
2383
2736
|
);
|
|
2384
|
-
|
|
2737
|
+
if (outcome.kind === "completed" && !finished && provider.invokeFallback) {
|
|
2738
|
+
survey = void 0;
|
|
2739
|
+
finished = false;
|
|
2740
|
+
sink.emit({ phase: "surveying", action: "Opus survey was invalid; retrying once with Sol" });
|
|
2741
|
+
outcome = await provider.invokeFallback(
|
|
2742
|
+
{ ...invocation, sessionKey: "survey:sol_retry" },
|
|
2743
|
+
modelConfigFor(models, "repair", "survey"),
|
|
2744
|
+
(action) => sink.emit({ phase: "surveying", action })
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
return { survey, finished, outcome };
|
|
2385
2748
|
}
|
|
2386
|
-
async function runSelection(provider, models, repoPath, bootstrap,
|
|
2749
|
+
async function runSelection(provider, models, repoPath, bootstrap, survey, rejectedCodes, sink, existingEventCodes = bootstrap.snapshot.events.map((event) => event.code), recoveryCodes = []) {
|
|
2387
2750
|
const proposed = [];
|
|
2388
2751
|
const seen = /* @__PURE__ */ new Set();
|
|
2389
2752
|
const rejected = new Set(rejectedCodes);
|
|
2753
|
+
const existing = new Set(existingEventCodes.map(normalizeEventCode));
|
|
2754
|
+
const recovery = new Set(recoveryCodes.map(normalizeEventCode));
|
|
2390
2755
|
let summary = "";
|
|
2391
2756
|
let finished = false;
|
|
2392
2757
|
const proposeTool = {
|
|
2393
2758
|
name: "propose_event",
|
|
2394
2759
|
description: "Propose one important, concretely-anchored product event. Returns ok:true when accepted.",
|
|
2395
2760
|
inputSchema: {
|
|
2396
|
-
code:
|
|
2397
|
-
name:
|
|
2398
|
-
reasoning:
|
|
2399
|
-
anchorFile:
|
|
2400
|
-
anchorSymbol:
|
|
2401
|
-
payloadSchema:
|
|
2761
|
+
code: z2.string().describe("lowercase snake_case event code"),
|
|
2762
|
+
name: z2.string().describe("short human-readable name"),
|
|
2763
|
+
reasoning: z2.string().describe("why this event matters for this business"),
|
|
2764
|
+
anchorFile: z2.string().describe("repository file where the committed action happens"),
|
|
2765
|
+
anchorSymbol: z2.string().optional().describe("function/symbol at the anchor"),
|
|
2766
|
+
payloadSchema: z2.record(z2.string(), z2.string()).optional().describe("important payload fields: name -> short description; no personal identifiers")
|
|
2402
2767
|
},
|
|
2403
2768
|
jsonSchema: {
|
|
2404
2769
|
type: "object",
|
|
@@ -2417,6 +2782,9 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2417
2782
|
required: ["code", "name", "reasoning", "anchorFile"]
|
|
2418
2783
|
},
|
|
2419
2784
|
handler: async (input) => {
|
|
2785
|
+
if (finished) {
|
|
2786
|
+
return { ok: false, reason: "event design is already frozen" };
|
|
2787
|
+
}
|
|
2420
2788
|
const candidate = {
|
|
2421
2789
|
code: String(input.code ?? ""),
|
|
2422
2790
|
name: String(input.name ?? ""),
|
|
@@ -2429,6 +2797,16 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2429
2797
|
if (!validated.ok) {
|
|
2430
2798
|
return { ok: false, reason: validated.reason };
|
|
2431
2799
|
}
|
|
2800
|
+
if (recovery.size > 0 && !recovery.has(validated.event.code)) {
|
|
2801
|
+
return { ok: false, reason: `placement recovery cannot create event ${validated.event.code}` };
|
|
2802
|
+
}
|
|
2803
|
+
if (existing.has(validated.event.code) && !recovery.has(validated.event.code)) {
|
|
2804
|
+
return { ok: false, reason: `event ${validated.event.code} is already registered` };
|
|
2805
|
+
}
|
|
2806
|
+
const anchorError = await validateAnchor(repoPath, validated.event);
|
|
2807
|
+
if (anchorError) {
|
|
2808
|
+
return { ok: false, reason: anchorError };
|
|
2809
|
+
}
|
|
2432
2810
|
seen.add(validated.event.code);
|
|
2433
2811
|
proposed.push(validated.event);
|
|
2434
2812
|
sink.emit({
|
|
@@ -2443,7 +2821,7 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2443
2821
|
name: "propose_event_batch",
|
|
2444
2822
|
description: "Propose a batch of important, concretely-anchored product events in one tool call.",
|
|
2445
2823
|
inputSchema: {
|
|
2446
|
-
events:
|
|
2824
|
+
events: z2.array(z2.object(proposeTool.inputSchema)).min(1).max(200)
|
|
2447
2825
|
},
|
|
2448
2826
|
jsonSchema: {
|
|
2449
2827
|
type: "object",
|
|
@@ -2471,9 +2849,9 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2471
2849
|
}
|
|
2472
2850
|
};
|
|
2473
2851
|
const finishTool = {
|
|
2474
|
-
name: "
|
|
2475
|
-
description: "
|
|
2476
|
-
inputSchema: { summary:
|
|
2852
|
+
name: "finish_event_design",
|
|
2853
|
+
description: "Freeze the complete batch after every important event has been proposed.",
|
|
2854
|
+
inputSchema: { summary: z2.string().describe("one line on what was selected and why") },
|
|
2477
2855
|
jsonSchema: {
|
|
2478
2856
|
type: "object",
|
|
2479
2857
|
properties: { summary: { type: "string", description: "one line on what was selected and why" } },
|
|
@@ -2486,61 +2864,204 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2486
2864
|
}
|
|
2487
2865
|
};
|
|
2488
2866
|
sink.emit({ phase: "designing", action: "selecting important events" });
|
|
2489
|
-
const
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2867
|
+
const invocation = {
|
|
2868
|
+
role: "design",
|
|
2869
|
+
task: "design",
|
|
2870
|
+
sessionKey: "design",
|
|
2871
|
+
systemPrompt: SELECTION_SYSTEM_PROMPT,
|
|
2872
|
+
userPrompt: selectionUserPrompt(
|
|
2873
|
+
JSON.stringify(survey),
|
|
2874
|
+
renderOnboardingContext(bootstrap.onboardingContext),
|
|
2875
|
+
existingEventCodes,
|
|
2876
|
+
rejectedCodes
|
|
2877
|
+
),
|
|
2878
|
+
tools: [proposeBatchTool, finishTool],
|
|
2879
|
+
repositoryAccess: { mode: "none" },
|
|
2880
|
+
allowRepoWrite: false,
|
|
2881
|
+
cwd: repoPath
|
|
2882
|
+
};
|
|
2883
|
+
let outcome = await provider.invoke(
|
|
2884
|
+
invocation,
|
|
2504
2885
|
modelConfigFor(models, "design", "design"),
|
|
2505
2886
|
(action) => sink.emit({ phase: "designing", action })
|
|
2506
2887
|
);
|
|
2888
|
+
if (outcome.kind === "completed" && (!finished || proposed.length === 0) && provider.invokeFallback) {
|
|
2889
|
+
proposed.length = 0;
|
|
2890
|
+
seen.clear();
|
|
2891
|
+
summary = "";
|
|
2892
|
+
finished = false;
|
|
2893
|
+
sink.emit({ phase: "designing", action: "Opus design was invalid; retrying once with Sol" });
|
|
2894
|
+
outcome = await provider.invokeFallback(
|
|
2895
|
+
{ ...invocation, sessionKey: "design:sol_retry" },
|
|
2896
|
+
modelConfigFor(models, "repair", "design"),
|
|
2897
|
+
(action) => sink.emit({ phase: "designing", action })
|
|
2898
|
+
);
|
|
2899
|
+
}
|
|
2507
2900
|
return {
|
|
2508
2901
|
outcome: outcome.kind,
|
|
2509
2902
|
finished,
|
|
2510
2903
|
proposed,
|
|
2511
2904
|
summary,
|
|
2512
|
-
|
|
2905
|
+
survey,
|
|
2513
2906
|
sessionId: outcome.sessionId
|
|
2514
2907
|
};
|
|
2515
2908
|
}
|
|
2516
|
-
async function
|
|
2517
|
-
const
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2909
|
+
async function validateAnchor(repoPath, event) {
|
|
2910
|
+
const root = resolve2(repoPath);
|
|
2911
|
+
const path = resolve2(root, event.anchorFile);
|
|
2912
|
+
if (path === root || !path.startsWith(root + sep2)) {
|
|
2913
|
+
return `${event.code}: anchor escapes the repository`;
|
|
2914
|
+
}
|
|
2915
|
+
try {
|
|
2916
|
+
const info = await stat3(path);
|
|
2917
|
+
if (!info.isFile()) return `${event.code}: anchor is not a file`;
|
|
2918
|
+
if (event.anchorSymbol) {
|
|
2919
|
+
const content = await readFile3(path, "utf8");
|
|
2920
|
+
if (!content.includes(event.anchorSymbol)) {
|
|
2921
|
+
return `${event.code}: anchor symbol ${event.anchorSymbol} is not present in ${event.anchorFile}`;
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
} catch {
|
|
2925
|
+
return `${event.code}: anchor file ${event.anchorFile} does not exist`;
|
|
2926
|
+
}
|
|
2927
|
+
return void 0;
|
|
2928
|
+
}
|
|
2929
|
+
function surveyJSONSchema() {
|
|
2930
|
+
const anchor = {
|
|
2931
|
+
type: "object",
|
|
2932
|
+
properties: {
|
|
2933
|
+
file: { type: "string" },
|
|
2934
|
+
symbol: { type: "string" },
|
|
2935
|
+
description: { type: "string" }
|
|
2936
|
+
},
|
|
2937
|
+
required: ["file", "description"]
|
|
2938
|
+
};
|
|
2939
|
+
return {
|
|
2940
|
+
type: "object",
|
|
2941
|
+
properties: {
|
|
2942
|
+
stack: {
|
|
2943
|
+
type: "object",
|
|
2944
|
+
properties: {
|
|
2945
|
+
frameworks: { type: "array", items: { type: "string" }, maxItems: 30 },
|
|
2946
|
+
packageManager: { type: "string" },
|
|
2947
|
+
entryPoints: { type: "array", items: { type: "string" }, maxItems: 50 }
|
|
2948
|
+
},
|
|
2949
|
+
required: ["frameworks", "entryPoints"]
|
|
2950
|
+
},
|
|
2951
|
+
featureAreas: {
|
|
2952
|
+
type: "array",
|
|
2953
|
+
maxItems: 200,
|
|
2954
|
+
items: {
|
|
2955
|
+
type: "object",
|
|
2956
|
+
properties: {
|
|
2957
|
+
name: { type: "string" },
|
|
2958
|
+
root: { type: "string" },
|
|
2959
|
+
files: { type: "array", items: { type: "string" }, maxItems: 200 }
|
|
2960
|
+
},
|
|
2961
|
+
required: ["name", "root", "files"]
|
|
2962
|
+
}
|
|
2963
|
+
},
|
|
2964
|
+
identifyAnchors: { type: "array", items: anchor, maxItems: 50 },
|
|
2965
|
+
logoutAnchors: { type: "array", items: anchor, maxItems: 50 },
|
|
2966
|
+
existingAnalytics: { type: "array", items: anchor, maxItems: 100 },
|
|
2967
|
+
integratableCallSites: {
|
|
2968
|
+
type: "array",
|
|
2969
|
+
maxItems: 500,
|
|
2970
|
+
items: {
|
|
2971
|
+
...anchor,
|
|
2972
|
+
properties: {
|
|
2973
|
+
...anchor.properties,
|
|
2974
|
+
outcome: { type: "string" },
|
|
2975
|
+
payloadFields: { type: "array", items: { type: "string" }, maxItems: 50 },
|
|
2976
|
+
similarPaths: { type: "array", items: { type: "string" }, maxItems: 50 }
|
|
2977
|
+
},
|
|
2978
|
+
required: ["file", "description", "outcome", "payloadFields", "similarPaths"]
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
},
|
|
2982
|
+
required: [
|
|
2983
|
+
"stack",
|
|
2984
|
+
"featureAreas",
|
|
2985
|
+
"identifyAnchors",
|
|
2986
|
+
"logoutAnchors",
|
|
2987
|
+
"existingAnalytics",
|
|
2988
|
+
"integratableCallSites"
|
|
2989
|
+
]
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
function normalizeSurveyArtifact(input) {
|
|
2993
|
+
const safePath = (value2) => {
|
|
2994
|
+
if (typeof value2 !== "string") return void 0;
|
|
2995
|
+
const normalized = value2.trim().replaceAll("\\", "/");
|
|
2996
|
+
if (!normalized || normalized.startsWith("/") || /^[a-z]:/i.test(normalized) || normalized.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
2997
|
+
return void 0;
|
|
2998
|
+
}
|
|
2999
|
+
return normalized;
|
|
3000
|
+
};
|
|
3001
|
+
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;
|
|
3002
|
+
const value = input;
|
|
3003
|
+
const stack = value.stack;
|
|
3004
|
+
const frameworks = stringList(stack?.frameworks, 30);
|
|
3005
|
+
const entryPoints = stringList(stack?.entryPoints, 50);
|
|
3006
|
+
if (!stack || !frameworks || !entryPoints || entryPoints.some((file) => !safePath(file))) {
|
|
3007
|
+
return void 0;
|
|
3008
|
+
}
|
|
3009
|
+
const anchors = (raw, max) => {
|
|
3010
|
+
if (!Array.isArray(raw) || raw.length > max) return void 0;
|
|
3011
|
+
const result = [];
|
|
3012
|
+
for (const item of raw) {
|
|
3013
|
+
const row = item;
|
|
3014
|
+
const file = safePath(row?.file);
|
|
3015
|
+
const description = typeof row?.description === "string" ? row.description.trim() : "";
|
|
3016
|
+
if (!file || !description) return void 0;
|
|
3017
|
+
result.push({
|
|
3018
|
+
file,
|
|
3019
|
+
...typeof row.symbol === "string" && row.symbol.trim() ? { symbol: row.symbol.trim() } : {},
|
|
3020
|
+
description
|
|
3021
|
+
});
|
|
2539
3022
|
}
|
|
2540
|
-
|
|
2541
|
-
|
|
3023
|
+
return result;
|
|
3024
|
+
};
|
|
3025
|
+
if (!Array.isArray(value.featureAreas) || value.featureAreas.length > 200) return void 0;
|
|
3026
|
+
const featureAreas = [];
|
|
3027
|
+
for (const item of value.featureAreas) {
|
|
3028
|
+
const row = item;
|
|
3029
|
+
const root = safePath(row?.root);
|
|
3030
|
+
const files = stringList(row?.files, 200);
|
|
3031
|
+
if (typeof row?.name !== "string" || !row.name.trim() || !root || !files || files.some((file) => !safePath(file))) return void 0;
|
|
3032
|
+
featureAreas.push({ name: row.name.trim(), root, files: files.map((file) => safePath(file)) });
|
|
3033
|
+
}
|
|
3034
|
+
const identifyAnchors = anchors(value.identifyAnchors, 50);
|
|
3035
|
+
const logoutAnchors = anchors(value.logoutAnchors, 50);
|
|
3036
|
+
const existingAnalytics = anchors(value.existingAnalytics, 100);
|
|
3037
|
+
const baseCallSites = anchors(value.integratableCallSites, 500);
|
|
3038
|
+
if (!identifyAnchors || !logoutAnchors || !existingAnalytics || !baseCallSites || !Array.isArray(value.integratableCallSites)) return void 0;
|
|
3039
|
+
const integratableCallSites = [];
|
|
3040
|
+
for (let index = 0; index < baseCallSites.length; index += 1) {
|
|
3041
|
+
const row = value.integratableCallSites[index];
|
|
3042
|
+
const outcome = typeof row.outcome === "string" ? row.outcome.trim() : "";
|
|
3043
|
+
const payloadFields = stringList(row.payloadFields, 50);
|
|
3044
|
+
const similarPaths = stringList(row.similarPaths, 50);
|
|
3045
|
+
if (!outcome || !payloadFields || !similarPaths || similarPaths.some((file) => !safePath(file))) return void 0;
|
|
3046
|
+
integratableCallSites.push({
|
|
3047
|
+
...baseCallSites[index],
|
|
3048
|
+
outcome,
|
|
3049
|
+
payloadFields,
|
|
3050
|
+
similarPaths: similarPaths.map((file) => safePath(file))
|
|
3051
|
+
});
|
|
2542
3052
|
}
|
|
2543
|
-
return
|
|
3053
|
+
return {
|
|
3054
|
+
stack: {
|
|
3055
|
+
frameworks,
|
|
3056
|
+
...typeof stack.packageManager === "string" && stack.packageManager.trim() ? { packageManager: stack.packageManager.trim() } : {},
|
|
3057
|
+
entryPoints: entryPoints.map((file) => safePath(file))
|
|
3058
|
+
},
|
|
3059
|
+
featureAreas,
|
|
3060
|
+
identifyAnchors,
|
|
3061
|
+
logoutAnchors,
|
|
3062
|
+
existingAnalytics,
|
|
3063
|
+
integratableCallSites
|
|
3064
|
+
};
|
|
2544
3065
|
}
|
|
2545
3066
|
function placementsFor(registered) {
|
|
2546
3067
|
return registered.map((event) => ({
|
|
@@ -2556,7 +3077,7 @@ function placementsFor(registered) {
|
|
|
2556
3077
|
import { spawn as spawn2 } from "child_process";
|
|
2557
3078
|
import { cp, lstat, mkdir, mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
|
|
2558
3079
|
import { tmpdir } from "os";
|
|
2559
|
-
import { dirname, join as
|
|
3080
|
+
import { dirname as dirname2, join as join5, relative, resolve as resolve3, sep as sep3 } from "path";
|
|
2560
3081
|
var WorktreeError = class extends Error {
|
|
2561
3082
|
constructor(code, message) {
|
|
2562
3083
|
super(message);
|
|
@@ -2566,7 +3087,7 @@ var WorktreeError = class extends Error {
|
|
|
2566
3087
|
code;
|
|
2567
3088
|
};
|
|
2568
3089
|
function git(cwd, args) {
|
|
2569
|
-
return new Promise((
|
|
3090
|
+
return new Promise((resolve8) => {
|
|
2570
3091
|
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2571
3092
|
let stdout = "";
|
|
2572
3093
|
let stderr = "";
|
|
@@ -2576,8 +3097,8 @@ function git(cwd, args) {
|
|
|
2576
3097
|
child.stderr.on("data", (chunk) => {
|
|
2577
3098
|
stderr += String(chunk);
|
|
2578
3099
|
});
|
|
2579
|
-
child.on("error", () =>
|
|
2580
|
-
child.on("close", (exitCode) =>
|
|
3100
|
+
child.on("error", () => resolve8({ ok: false, stdout, stderr: "git is not available" }));
|
|
3101
|
+
child.on("close", (exitCode) => resolve8({ ok: exitCode === 0, stdout, stderr }));
|
|
2581
3102
|
});
|
|
2582
3103
|
}
|
|
2583
3104
|
async function must(cwd, args) {
|
|
@@ -2588,8 +3109,8 @@ async function must(cwd, args) {
|
|
|
2588
3109
|
return result.stdout;
|
|
2589
3110
|
}
|
|
2590
3111
|
function safeRepoFile(repoPath, file) {
|
|
2591
|
-
const path =
|
|
2592
|
-
if (path === repoPath || !path.startsWith(repoPath +
|
|
3112
|
+
const path = resolve3(repoPath, file);
|
|
3113
|
+
if (path === repoPath || !path.startsWith(repoPath + sep3) || relative(repoPath, path).startsWith("..")) {
|
|
2593
3114
|
throw new WorktreeError("git_failed", `unsafe progress file path: ${file}`);
|
|
2594
3115
|
}
|
|
2595
3116
|
return path;
|
|
@@ -2605,7 +3126,7 @@ async function seedWorktree(repoPath, worktreePath, files) {
|
|
|
2605
3126
|
const destination = safeRepoFile(worktreePath, file);
|
|
2606
3127
|
try {
|
|
2607
3128
|
await lstat(source);
|
|
2608
|
-
await mkdir(
|
|
3129
|
+
await mkdir(dirname2(destination), { recursive: true });
|
|
2609
3130
|
await cp(source, destination, { recursive: true, force: true });
|
|
2610
3131
|
} catch {
|
|
2611
3132
|
await rm(destination, { recursive: true, force: true });
|
|
@@ -2627,7 +3148,7 @@ async function createWorktree(repoPath, force = false, allowedDirtyFiles = []) {
|
|
|
2627
3148
|
);
|
|
2628
3149
|
}
|
|
2629
3150
|
let baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
|
|
2630
|
-
const path = await mkdtemp(
|
|
3151
|
+
const path = await mkdtemp(join5(tmpdir(), "whisperr-worktree-"));
|
|
2631
3152
|
await must(repoPath, ["worktree", "add", "--detach", "--force", path, "HEAD"]);
|
|
2632
3153
|
const seedFiles = force ? dirty : dirty.filter((file) => allowed.has(file));
|
|
2633
3154
|
if (seedFiles.length > 0) {
|
|
@@ -2652,12 +3173,18 @@ async function worktreePatch(handle) {
|
|
|
2652
3173
|
return must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
|
|
2653
3174
|
}
|
|
2654
3175
|
async function applyPatchToRepo(handle, patch) {
|
|
3176
|
+
return applyPatch(handle.repoPath, patch, false);
|
|
3177
|
+
}
|
|
3178
|
+
async function reversePatchInRepo(repoPath, patch) {
|
|
3179
|
+
return applyPatch(repoPath, patch, true);
|
|
3180
|
+
}
|
|
3181
|
+
async function applyPatch(repoPath, patch, reverse) {
|
|
2655
3182
|
if (patch.trim() === "") {
|
|
2656
3183
|
return;
|
|
2657
3184
|
}
|
|
2658
|
-
await new Promise((
|
|
2659
|
-
const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
|
|
2660
|
-
cwd:
|
|
3185
|
+
await new Promise((resolve8, reject) => {
|
|
3186
|
+
const child = spawn2("git", ["apply", ...reverse ? ["--reverse"] : [], "--whitespace=nowarn"], {
|
|
3187
|
+
cwd: repoPath,
|
|
2661
3188
|
stdio: ["pipe", "ignore", "pipe"]
|
|
2662
3189
|
});
|
|
2663
3190
|
let stderr = "";
|
|
@@ -2667,7 +3194,7 @@ async function applyPatchToRepo(handle, patch) {
|
|
|
2667
3194
|
child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
|
|
2668
3195
|
child.on("close", (exitCode) => {
|
|
2669
3196
|
if (exitCode === 0) {
|
|
2670
|
-
|
|
3197
|
+
resolve8();
|
|
2671
3198
|
} else {
|
|
2672
3199
|
reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
|
|
2673
3200
|
}
|
|
@@ -2675,24 +3202,6 @@ async function applyPatchToRepo(handle, patch) {
|
|
|
2675
3202
|
child.stdin.end(patch);
|
|
2676
3203
|
});
|
|
2677
3204
|
}
|
|
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
3205
|
async function discardWorktreeChanges(handle) {
|
|
2697
3206
|
await must(handle.path, ["reset", "--hard", handle.baseRef]);
|
|
2698
3207
|
await must(handle.path, ["clean", "-fd"]);
|
|
@@ -2705,6 +3214,16 @@ async function removeWorktree(handle) {
|
|
|
2705
3214
|
// src/engine/orchestrator.ts
|
|
2706
3215
|
async function runEngine(input) {
|
|
2707
3216
|
const runs = new RunsClient(input.config, input.session);
|
|
3217
|
+
const phaseTimings = [];
|
|
3218
|
+
let activePhase = "authorizing";
|
|
3219
|
+
let phaseStartedAt = Date.now();
|
|
3220
|
+
let timingsFinished = false;
|
|
3221
|
+
const finishTimings = () => {
|
|
3222
|
+
if (!timingsFinished) {
|
|
3223
|
+
phaseTimings.push({ phase: activePhase, ms: Date.now() - phaseStartedAt });
|
|
3224
|
+
timingsFinished = true;
|
|
3225
|
+
}
|
|
3226
|
+
};
|
|
2708
3227
|
let bootstrap;
|
|
2709
3228
|
try {
|
|
2710
3229
|
bootstrap = await runs.createOrResumeRun({
|
|
@@ -2722,6 +3241,11 @@ async function runEngine(input) {
|
|
|
2722
3241
|
const runId = bootstrap.run.id;
|
|
2723
3242
|
input.provider.onRunReady?.(runId, bootstrap.run.modelConversationId);
|
|
2724
3243
|
const patchPhase = async (phase, message) => {
|
|
3244
|
+
if (phase !== activePhase) {
|
|
3245
|
+
phaseTimings.push({ phase: activePhase, ms: Date.now() - phaseStartedAt });
|
|
3246
|
+
activePhase = phase;
|
|
3247
|
+
phaseStartedAt = Date.now();
|
|
3248
|
+
}
|
|
2725
3249
|
try {
|
|
2726
3250
|
await runs.patchRun(runId, { phase, ...message ? { message } : {} });
|
|
2727
3251
|
} catch {
|
|
@@ -2733,6 +3257,7 @@ async function runEngine(input) {
|
|
|
2733
3257
|
} catch {
|
|
2734
3258
|
}
|
|
2735
3259
|
};
|
|
3260
|
+
let survey = artifactContent(bootstrap, "survey");
|
|
2736
3261
|
let registered;
|
|
2737
3262
|
try {
|
|
2738
3263
|
const projectEvents = bootstrap.snapshot.events.filter(
|
|
@@ -2745,17 +3270,24 @@ async function runEngine(input) {
|
|
|
2745
3270
|
phase: "surveying",
|
|
2746
3271
|
action: projectEvents.length === 0 ? "no saved progress found; starting a fresh survey" : "saved events need placement recovery; verifying the repository"
|
|
2747
3272
|
});
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
3273
|
+
if (!survey) {
|
|
3274
|
+
await patchPhase("surveying");
|
|
3275
|
+
const surveyRun = await runSurvey(
|
|
3276
|
+
input.provider,
|
|
3277
|
+
input.models,
|
|
3278
|
+
input.repoPath,
|
|
3279
|
+
bootstrap,
|
|
3280
|
+
input.sink
|
|
3281
|
+
);
|
|
3282
|
+
if (surveyRun.outcome.kind !== "completed" || !surveyRun.finished || !surveyRun.survey) {
|
|
3283
|
+
await failRun(`survey ${surveyRun.outcome.kind}`);
|
|
3284
|
+
return { kind: "aborted", reason: `survey ${surveyRun.outcome.kind}` };
|
|
3285
|
+
}
|
|
3286
|
+
survey = surveyRun.survey;
|
|
3287
|
+
const config2 = modelConfigForTask(input.models, "survey", "survey");
|
|
3288
|
+
await runs.persistSurveyArtifact(runId, survey, modelMetadata(config2));
|
|
3289
|
+
} else {
|
|
3290
|
+
input.sink.emit({ phase: "surveying", action: "reusing the saved immutable survey" });
|
|
2759
3291
|
}
|
|
2760
3292
|
await patchPhase("designing");
|
|
2761
3293
|
const recoverableCodes = new Set(missingPlacement.map((event) => event.code));
|
|
@@ -2765,13 +3297,14 @@ async function runEngine(input) {
|
|
|
2765
3297
|
input.models,
|
|
2766
3298
|
input.repoPath,
|
|
2767
3299
|
bootstrap,
|
|
2768
|
-
survey
|
|
3300
|
+
survey,
|
|
2769
3301
|
[],
|
|
2770
3302
|
input.sink,
|
|
2771
|
-
existingCodes
|
|
3303
|
+
existingCodes,
|
|
3304
|
+
[...recoverableCodes]
|
|
2772
3305
|
);
|
|
2773
3306
|
if (!selection.finished) {
|
|
2774
|
-
const reason = selection.outcome === "completed" ? "
|
|
3307
|
+
const reason = selection.outcome === "completed" ? "design did not call finish_event_design" : `design ${selection.outcome}`;
|
|
2775
3308
|
await failRun(reason);
|
|
2776
3309
|
return { kind: "aborted", reason };
|
|
2777
3310
|
}
|
|
@@ -2779,18 +3312,29 @@ async function runEngine(input) {
|
|
|
2779
3312
|
await failRun("selection finished with no accepted events");
|
|
2780
3313
|
return { kind: "aborted", reason: "selection finished with no accepted events" };
|
|
2781
3314
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
await failRun("selection rejected by user");
|
|
2785
|
-
return { kind: "aborted", reason: "selection rejected by user" };
|
|
2786
|
-
}
|
|
2787
|
-
if (approved.length === 0) {
|
|
2788
|
-
await failRun("no events accepted for instrumentation");
|
|
2789
|
-
return { kind: "aborted", reason: "no events accepted for instrumentation" };
|
|
3315
|
+
if (recoverableCodes.size > 0 && (selection.proposed.length !== recoverableCodes.size || selection.proposed.some((event) => !recoverableCodes.has(event.code)))) {
|
|
3316
|
+
throw new Error("placement recovery must cover the frozen event set exactly");
|
|
2790
3317
|
}
|
|
2791
|
-
await patchPhase("persisting", `registering ${
|
|
2792
|
-
const
|
|
2793
|
-
|
|
3318
|
+
await patchPhase("persisting", `registering ${selection.proposed.length} events`);
|
|
3319
|
+
const design = {
|
|
3320
|
+
surveyHash: artifactContentHash(survey),
|
|
3321
|
+
summary: selection.summary,
|
|
3322
|
+
events: [...selection.proposed].sort((a, b) => a.code.localeCompare(b.code))
|
|
3323
|
+
};
|
|
3324
|
+
const config = modelConfigForTask(input.models, "design", "design");
|
|
3325
|
+
const committed = await runs.commitDesign(
|
|
3326
|
+
runId,
|
|
3327
|
+
design,
|
|
3328
|
+
design.events.map(eventRequest),
|
|
3329
|
+
modelMetadata(config)
|
|
3330
|
+
);
|
|
3331
|
+
registered = registeredFromSnapshot(
|
|
3332
|
+
committed.events.filter((event) => event.projectId === bootstrap.project.id)
|
|
3333
|
+
);
|
|
3334
|
+
input.sink.emit({
|
|
3335
|
+
phase: "persisting",
|
|
3336
|
+
action: `designed and registered ${selection.proposed.length} events`
|
|
3337
|
+
});
|
|
2794
3338
|
const unresolved = projectEvents.filter((event) => !registered.some((candidate) => candidate.code === event.code)).map((event) => event.code);
|
|
2795
3339
|
if (unresolved.length > 0) {
|
|
2796
3340
|
throw new Error(`could not recover placements for: ${unresolved.join(", ")}`);
|
|
@@ -2815,17 +3359,18 @@ async function runEngine(input) {
|
|
|
2815
3359
|
const changedFiles2 = new Set(prior?.changedFiles ?? []);
|
|
2816
3360
|
const eventStatuses = await reconcileProgress(input.repoPath, registered, prior, input.sink);
|
|
2817
3361
|
const eventFailureReasons = /* @__PURE__ */ new Map();
|
|
3362
|
+
let verificationStatus = "pending";
|
|
2818
3363
|
for (const event of registered) {
|
|
2819
3364
|
const status = eventStatuses.get(event.code) ?? "planned";
|
|
2820
3365
|
if (status !== "planned" && status !== "failed" && status !== "unsupported") {
|
|
2821
3366
|
changedFiles2.add(event.anchorFile);
|
|
2822
3367
|
}
|
|
2823
3368
|
}
|
|
2824
|
-
let identifyWired = prior?.identifyWired === true && await setupLooksPresent(input.repoPath, changedFiles2);
|
|
3369
|
+
let identifyWired = prior?.identifyWired === true && await setupLooksPresent(input.repoPath, changedFiles2, survey);
|
|
2825
3370
|
const progress = () => ({
|
|
2826
3371
|
changedFiles: [...changedFiles2].sort(),
|
|
2827
3372
|
identifyWired,
|
|
2828
|
-
verificationStatus
|
|
3373
|
+
verificationStatus,
|
|
2829
3374
|
events: buildEvidence(registered, eventStatuses)
|
|
2830
3375
|
});
|
|
2831
3376
|
const saveProgress = async () => {
|
|
@@ -2840,6 +3385,7 @@ async function runEngine(input) {
|
|
|
2840
3385
|
}
|
|
2841
3386
|
await patchPhase("binding");
|
|
2842
3387
|
let worktree;
|
|
3388
|
+
const ownedPatches = [];
|
|
2843
3389
|
try {
|
|
2844
3390
|
worktree = await createWorktree(
|
|
2845
3391
|
input.repoPath,
|
|
@@ -2851,8 +3397,9 @@ async function runEngine(input) {
|
|
|
2851
3397
|
await failRun(reason);
|
|
2852
3398
|
return { kind: "aborted", reason };
|
|
2853
3399
|
}
|
|
2854
|
-
const
|
|
2855
|
-
|
|
3400
|
+
const setupWorktree = worktree;
|
|
3401
|
+
const checkpoint = async (handle, kind, eventCodes = []) => {
|
|
3402
|
+
const patch = await worktreePatch(handle);
|
|
2856
3403
|
if (patch.trim() === "") {
|
|
2857
3404
|
return [];
|
|
2858
3405
|
}
|
|
@@ -2861,155 +3408,133 @@ async function runEngine(input) {
|
|
|
2861
3408
|
changedFiles2.add(file);
|
|
2862
3409
|
}
|
|
2863
3410
|
await saveProgress();
|
|
2864
|
-
await applyPatchToRepo(
|
|
2865
|
-
|
|
3411
|
+
await applyPatchToRepo(handle, patch);
|
|
3412
|
+
ownedPatches.push({ patch, files, eventCodes, kind });
|
|
2866
3413
|
return files;
|
|
2867
3414
|
};
|
|
2868
3415
|
try {
|
|
2869
|
-
const bindings = await writeBindingsModule(
|
|
3416
|
+
const bindings = await writeBindingsModule(setupWorktree, input.target, registered);
|
|
2870
3417
|
if (!identifyWired) {
|
|
3418
|
+
const validateSetup = async () => {
|
|
3419
|
+
if (await readFile4(resolve4(setupWorktree.path, bindings.relativePath), "utf8") !== bindings.content) {
|
|
3420
|
+
return false;
|
|
3421
|
+
}
|
|
3422
|
+
const candidatePatch = await worktreePatch(setupWorktree);
|
|
3423
|
+
if (patchAddsProductEvents(candidatePatch, bindings.relativePath, registered)) {
|
|
3424
|
+
return false;
|
|
3425
|
+
}
|
|
3426
|
+
return setupLooksPresent(
|
|
3427
|
+
setupWorktree.path,
|
|
3428
|
+
new Set(patchFiles(candidatePatch)),
|
|
3429
|
+
survey
|
|
3430
|
+
);
|
|
3431
|
+
};
|
|
2871
3432
|
const setupCompleted = await runSdkSetupPass({
|
|
2872
3433
|
provider: input.provider,
|
|
2873
3434
|
models: input.models,
|
|
2874
|
-
worktree,
|
|
3435
|
+
worktree: setupWorktree,
|
|
2875
3436
|
target: input.target,
|
|
2876
3437
|
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
2877
3438
|
registered,
|
|
2878
3439
|
ingestion: bootstrap.ingestion,
|
|
3440
|
+
survey,
|
|
2879
3441
|
sink: input.sink,
|
|
2880
3442
|
saveClusters: () => {
|
|
2881
3443
|
},
|
|
2882
|
-
discardChanges: async () => discardWorktreeChanges(
|
|
3444
|
+
discardChanges: async () => discardWorktreeChanges(setupWorktree),
|
|
2883
3445
|
onClusterIntegrated: async () => {
|
|
2884
3446
|
}
|
|
2885
|
-
}, bindings);
|
|
3447
|
+
}, bindings, validateSetup);
|
|
2886
3448
|
if (!setupCompleted) {
|
|
2887
|
-
throw new Error("SDK setup did not complete");
|
|
3449
|
+
throw new Error("SDK setup did not complete or failed deterministic validation");
|
|
3450
|
+
}
|
|
3451
|
+
const setupPatch = await worktreePatch(setupWorktree);
|
|
3452
|
+
const setupFiles = patchFiles(setupPatch);
|
|
3453
|
+
if (!await setupLooksPresent(setupWorktree.path, new Set(setupFiles), survey)) {
|
|
3454
|
+
throw new Error("SDK initialization, identify(), session restoration, or logout reset() could not be verified");
|
|
2888
3455
|
}
|
|
2889
|
-
await checkpoint();
|
|
2890
|
-
identifyWired = await setupLooksPresent(input.repoPath, changedFiles2);
|
|
3456
|
+
await checkpoint(setupWorktree, "setup");
|
|
3457
|
+
identifyWired = await setupLooksPresent(input.repoPath, changedFiles2, survey);
|
|
2891
3458
|
if (!identifyWired) {
|
|
2892
|
-
throw new Error("SDK initialization, bindings,
|
|
3459
|
+
throw new Error("SDK initialization, bindings, identify(), and reset() could not be verified");
|
|
2893
3460
|
}
|
|
2894
3461
|
await saveProgress();
|
|
2895
3462
|
} else {
|
|
2896
|
-
await checkpoint();
|
|
3463
|
+
await checkpoint(setupWorktree, "setup");
|
|
2897
3464
|
}
|
|
2898
3465
|
const pending = registered.filter((event) => {
|
|
2899
3466
|
const status = eventStatuses.get(event.code) ?? "planned";
|
|
2900
3467
|
return status === "planned" || status === "failed";
|
|
2901
3468
|
});
|
|
2902
|
-
|
|
3469
|
+
const clusters = deriveClusters(placementsFor(pending));
|
|
2903
3470
|
await patchPhase("integrating");
|
|
2904
|
-
await removeWorktree(
|
|
3471
|
+
await removeWorktree(setupWorktree).catch(() => {
|
|
2905
3472
|
});
|
|
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
|
-
}
|
|
3473
|
+
worktree = void 0;
|
|
3474
|
+
const queue = [...clusters];
|
|
3475
|
+
const active = /* @__PURE__ */ new Map();
|
|
3476
|
+
const lockedFiles = /* @__PURE__ */ new Set();
|
|
3477
|
+
let workerSequence = 0;
|
|
3478
|
+
while (queue.length > 0 || active.size > 0) {
|
|
3479
|
+
while (active.size < 4) {
|
|
3480
|
+
const nextIndex = queue.findIndex((cluster2) => !lockedFiles.has(cluster2.files[0]));
|
|
3481
|
+
if (nextIndex < 0) break;
|
|
3482
|
+
const cluster = queue.splice(nextIndex, 1)[0];
|
|
3483
|
+
const file = cluster.files[0];
|
|
3484
|
+
lockedFiles.add(file);
|
|
3485
|
+
const id2 = workerSequence++;
|
|
3486
|
+
active.set(id2, runClusterWorker(input, bootstrap, registered, survey, changedFiles2, cluster));
|
|
2924
3487
|
}
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
3488
|
+
if (active.size === 0) break;
|
|
3489
|
+
const { id, result: worker } = await Promise.race(
|
|
3490
|
+
[...active.entries()].map(async ([id2, task]) => ({ id: id2, result: await task }))
|
|
3491
|
+
);
|
|
3492
|
+
active.delete(id);
|
|
3493
|
+
lockedFiles.delete(worker.cluster.files[0]);
|
|
3494
|
+
const returned = worker.integration?.clusters[0];
|
|
3495
|
+
let applied = false;
|
|
3496
|
+
let failure = worker.error ?? returned?.note ?? "cluster patch could not be merged";
|
|
3497
|
+
if (returned?.status === "verified" && worker.handle) {
|
|
3498
|
+
const files = patchFiles(worker.patch);
|
|
3499
|
+
const unexpected = files.filter((file) => !worker.cluster.files.includes(file));
|
|
3500
|
+
if (unexpected.length === 0) {
|
|
3501
|
+
try {
|
|
3502
|
+
for (const file of files) changedFiles2.add(file);
|
|
3503
|
+
await saveProgress();
|
|
3504
|
+
await applyPatchToRepo(worker.handle, worker.patch);
|
|
3505
|
+
ownedPatches.push({
|
|
3506
|
+
patch: worker.patch,
|
|
3507
|
+
files,
|
|
3508
|
+
eventCodes: worker.cluster.events.map((event) => event.eventCode),
|
|
3509
|
+
kind: "cluster"
|
|
3510
|
+
});
|
|
3511
|
+
applied = true;
|
|
3512
|
+
} catch (error) {
|
|
3513
|
+
failure = `serial patch merge failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
2942
3514
|
}
|
|
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
3515
|
} 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
|
-
}
|
|
3516
|
+
failure = `cluster edited files outside its isolation boundary: ${unexpected.join(", ")}`;
|
|
3002
3517
|
}
|
|
3518
|
+
}
|
|
3519
|
+
if (!applied && worker.cluster.events.length > 1) {
|
|
3520
|
+
const singles = worker.cluster.events.map((event, index) => ({
|
|
3521
|
+
id: clusterIdFor([event.file], [event.eventId || event.eventCode], index),
|
|
3522
|
+
files: [event.file],
|
|
3523
|
+
events: [event],
|
|
3524
|
+
status: "pending",
|
|
3525
|
+
attempts: 0
|
|
3526
|
+
}));
|
|
3527
|
+
queue.push(...singles);
|
|
3528
|
+
input.sink.emit({
|
|
3529
|
+
phase: "integrating",
|
|
3530
|
+
file: worker.cluster.files[0],
|
|
3531
|
+
action: "Sol could not finish the cluster; retrying each event independently"
|
|
3532
|
+
});
|
|
3533
|
+
} else {
|
|
3003
3534
|
for (const placement of worker.cluster.events) {
|
|
3004
|
-
const status = applied ?
|
|
3535
|
+
const status = applied ? "syntax_verified" : "failed";
|
|
3005
3536
|
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
|
-
}
|
|
3537
|
+
if (!applied) eventFailureReasons.set(placement.eventCode, failure);
|
|
3013
3538
|
await saveProgress();
|
|
3014
3539
|
input.sink.emit({
|
|
3015
3540
|
phase: "integrating",
|
|
@@ -3017,78 +3542,216 @@ async function runEngine(input) {
|
|
|
3017
3542
|
action: applied ? `merged and saved ${placement.eventCode}` : `${placement.eventCode} needs manual attention`
|
|
3018
3543
|
});
|
|
3019
3544
|
}
|
|
3020
|
-
if (worker.handle) {
|
|
3021
|
-
await removeWorktree(worker.handle).catch(() => {
|
|
3022
|
-
});
|
|
3023
|
-
}
|
|
3024
3545
|
}
|
|
3546
|
+
if (worker.handle) await removeWorktree(worker.handle).catch(() => {
|
|
3547
|
+
});
|
|
3025
3548
|
}
|
|
3026
3549
|
await saveProgress();
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
if (
|
|
3032
|
-
|
|
3033
|
-
|
|
3550
|
+
await patchPhase("reviewing");
|
|
3551
|
+
const reviewableByFile = groupEventsByFile(registered.filter(
|
|
3552
|
+
(event) => eventStatuses.get(event.code) === "syntax_verified"
|
|
3553
|
+
));
|
|
3554
|
+
if (reviewableByFile.size > 0) {
|
|
3555
|
+
const reviewWorktree = await createWorktree(
|
|
3556
|
+
input.repoPath,
|
|
3557
|
+
input.force ?? false,
|
|
3558
|
+
[...changedFiles2]
|
|
3034
3559
|
);
|
|
3560
|
+
const reviewDeps = integrationDeps(
|
|
3561
|
+
input,
|
|
3562
|
+
bootstrap,
|
|
3563
|
+
registered,
|
|
3564
|
+
survey,
|
|
3565
|
+
reviewWorktree,
|
|
3566
|
+
[...changedFiles2]
|
|
3567
|
+
);
|
|
3568
|
+
const reviews = await mapLimit(
|
|
3569
|
+
[...reviewableByFile.entries()],
|
|
3570
|
+
2,
|
|
3571
|
+
async ([file, events]) => ({
|
|
3572
|
+
file,
|
|
3573
|
+
events,
|
|
3574
|
+
review: await reviewIntegratedFile(reviewDeps, file, events)
|
|
3575
|
+
})
|
|
3576
|
+
);
|
|
3577
|
+
await removeWorktree(reviewWorktree).catch(() => {
|
|
3578
|
+
});
|
|
3579
|
+
for (const reviewed of reviews) {
|
|
3580
|
+
if (reviewed.review.ok) {
|
|
3581
|
+
for (const event of reviewed.events) eventStatuses.set(event.code, "reviewed");
|
|
3582
|
+
await saveProgress();
|
|
3583
|
+
continue;
|
|
3584
|
+
}
|
|
3585
|
+
const repairWorktree = await createWorktree(
|
|
3586
|
+
input.repoPath,
|
|
3587
|
+
input.force ?? false,
|
|
3588
|
+
[...changedFiles2]
|
|
3589
|
+
);
|
|
3590
|
+
const repairDeps = integrationDeps(
|
|
3591
|
+
input,
|
|
3592
|
+
bootstrap,
|
|
3593
|
+
registered,
|
|
3594
|
+
survey,
|
|
3595
|
+
repairWorktree,
|
|
3596
|
+
[...changedFiles2]
|
|
3597
|
+
);
|
|
3598
|
+
const repaired = await repairIntegratedFile(
|
|
3599
|
+
repairDeps,
|
|
3600
|
+
reviewed.file,
|
|
3601
|
+
reviewed.events,
|
|
3602
|
+
reviewed.review
|
|
3603
|
+
);
|
|
3604
|
+
const reviewCluster = {
|
|
3605
|
+
id: clusterIdFor([reviewed.file], reviewed.events.map((event) => event.eventId)),
|
|
3606
|
+
files: [reviewed.file],
|
|
3607
|
+
events: placementsFor(reviewed.events),
|
|
3608
|
+
status: "verified",
|
|
3609
|
+
attempts: 1
|
|
3610
|
+
};
|
|
3611
|
+
const structural = repaired ? await structuralClusterCheck(repairWorktree.path, reviewCluster) : { ok: false, notes: "Sol repair did not complete" };
|
|
3612
|
+
const recheck = structural.ok ? await reviewIntegratedFile(repairDeps, reviewed.file, reviewed.events, ":recheck") : { ok: false, issues: [], notes: structural.notes };
|
|
3613
|
+
const repairPatch = await worktreePatch(repairWorktree);
|
|
3614
|
+
const repairFiles = patchFiles(repairPatch);
|
|
3615
|
+
if (repaired && structural.ok && recheck.ok && repairFiles.every((file) => file === reviewed.file)) {
|
|
3616
|
+
for (const file of repairFiles) changedFiles2.add(file);
|
|
3617
|
+
await saveProgress();
|
|
3618
|
+
await applyPatchToRepo(repairWorktree, repairPatch);
|
|
3619
|
+
ownedPatches.push({
|
|
3620
|
+
patch: repairPatch,
|
|
3621
|
+
files: repairFiles,
|
|
3622
|
+
eventCodes: reviewed.events.map((event) => event.code),
|
|
3623
|
+
kind: "semantic"
|
|
3624
|
+
});
|
|
3625
|
+
for (const event of reviewed.events) eventStatuses.set(event.code, "reviewed");
|
|
3626
|
+
} else {
|
|
3627
|
+
const reasons = reviewed.review.issues.map((issue) => issue.reason).join("; ") || recheck.notes || structural.notes;
|
|
3628
|
+
await rollbackFileEventPatches(
|
|
3629
|
+
input.repoPath,
|
|
3630
|
+
reviewed.file,
|
|
3631
|
+
ownedPatches
|
|
3632
|
+
);
|
|
3633
|
+
for (const event of reviewed.events) {
|
|
3634
|
+
eventStatuses.set(event.code, "failed");
|
|
3635
|
+
eventFailureReasons.set(event.code, reasons.slice(0, 500));
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
await removeWorktree(repairWorktree).catch(() => {
|
|
3639
|
+
});
|
|
3640
|
+
await saveProgress();
|
|
3641
|
+
}
|
|
3035
3642
|
}
|
|
3036
3643
|
await patchPhase("verifying");
|
|
3037
|
-
|
|
3644
|
+
let verificationWorktree = await createWorktree(
|
|
3038
3645
|
input.repoPath,
|
|
3039
3646
|
input.force ?? false,
|
|
3040
3647
|
[...changedFiles2]
|
|
3041
3648
|
);
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
worktree: verificationWorktree,
|
|
3046
|
-
target: input.target,
|
|
3047
|
-
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
3649
|
+
let verificationDeps = integrationDeps(
|
|
3650
|
+
input,
|
|
3651
|
+
bootstrap,
|
|
3048
3652
|
registered,
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3653
|
+
survey,
|
|
3654
|
+
verificationWorktree,
|
|
3655
|
+
[...changedFiles2]
|
|
3656
|
+
);
|
|
3657
|
+
let finalVerify = await finalizeIntegration(verificationDeps, input.repoPath);
|
|
3658
|
+
for (let cycle = 0; finalVerify.status === "failed" && cycle < 2; cycle += 1) {
|
|
3659
|
+
const repaired = await repairVerification(verificationDeps, finalVerify.results, cycle + 1);
|
|
3660
|
+
if (!repaired) break;
|
|
3661
|
+
const repairPatch = await worktreePatch(verificationWorktree);
|
|
3662
|
+
const repairFiles = patchFiles(repairPatch);
|
|
3663
|
+
const allowedRepairFiles = new Set(verificationDeps.allowedRepairFiles ?? []);
|
|
3664
|
+
const structural = await structuralRepairCheck(
|
|
3665
|
+
verificationWorktree.path,
|
|
3666
|
+
registered,
|
|
3667
|
+
repairFiles
|
|
3668
|
+
);
|
|
3669
|
+
if (repairPatch.trim() === "" || repairFiles.some((file) => !allowedRepairFiles.has(file)) || !structural.ok) {
|
|
3670
|
+
break;
|
|
3055
3671
|
}
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3672
|
+
for (const file of repairFiles) changedFiles2.add(file);
|
|
3673
|
+
await saveProgress();
|
|
3674
|
+
await applyPatchToRepo(verificationWorktree, repairPatch);
|
|
3675
|
+
ownedPatches.push({
|
|
3676
|
+
patch: repairPatch,
|
|
3677
|
+
files: repairFiles,
|
|
3678
|
+
eventCodes: registered.map((event) => event.code),
|
|
3679
|
+
kind: "verification"
|
|
3680
|
+
});
|
|
3681
|
+
await removeWorktree(verificationWorktree).catch(() => {
|
|
3682
|
+
});
|
|
3683
|
+
finalVerify = await finalizeIntegration(verificationDeps, input.repoPath);
|
|
3684
|
+
if (finalVerify.status === "failed" && cycle + 1 < 2) {
|
|
3685
|
+
verificationWorktree = await createWorktree(
|
|
3686
|
+
input.repoPath,
|
|
3687
|
+
input.force ?? false,
|
|
3688
|
+
[...changedFiles2]
|
|
3689
|
+
);
|
|
3690
|
+
verificationDeps = integrationDeps(
|
|
3691
|
+
input,
|
|
3692
|
+
bootstrap,
|
|
3693
|
+
registered,
|
|
3694
|
+
survey,
|
|
3695
|
+
verificationWorktree,
|
|
3696
|
+
[...changedFiles2]
|
|
3697
|
+
);
|
|
3069
3698
|
}
|
|
3070
3699
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3700
|
+
verificationStatus = finalVerify.status;
|
|
3701
|
+
if (finalVerify.status !== "passed") {
|
|
3702
|
+
await removeWorktree(verificationWorktree).catch(() => {
|
|
3703
|
+
});
|
|
3704
|
+
await rollbackOwnedPatches(input.repoPath, ownedPatches);
|
|
3705
|
+
changedFiles2.clear();
|
|
3706
|
+
identifyWired = false;
|
|
3074
3707
|
for (const event of registered) {
|
|
3075
3708
|
eventStatuses.set(event.code, "failed");
|
|
3076
3709
|
eventFailureReasons.set(
|
|
3077
3710
|
event.code,
|
|
3078
|
-
`final verification
|
|
3711
|
+
`final verification ${finalVerify.status}${finalVerify.command ? `: ${finalVerify.command}` : ""}`
|
|
3079
3712
|
);
|
|
3080
3713
|
}
|
|
3081
3714
|
await saveProgress();
|
|
3082
|
-
throw new Error(
|
|
3715
|
+
throw new Error(`final verification ${finalVerify.status} after two Sol repair cycles`);
|
|
3083
3716
|
}
|
|
3084
|
-
|
|
3717
|
+
await removeWorktree(verificationWorktree).catch(() => {
|
|
3718
|
+
});
|
|
3719
|
+
identifyWired = await setupLooksPresent(input.repoPath, changedFiles2, survey);
|
|
3085
3720
|
if (!identifyWired) {
|
|
3721
|
+
await rollbackOwnedPatches(input.repoPath, ownedPatches);
|
|
3722
|
+
changedFiles2.clear();
|
|
3086
3723
|
for (const event of registered) {
|
|
3087
3724
|
eventStatuses.set(event.code, "failed");
|
|
3088
3725
|
eventFailureReasons.set(event.code, "SDK initialization or identify() is missing after final repair");
|
|
3089
3726
|
}
|
|
3090
3727
|
await saveProgress();
|
|
3091
|
-
throw new Error("SDK initialization and
|
|
3728
|
+
throw new Error("SDK initialization, identify(), and logout reset() must be present before completion");
|
|
3729
|
+
}
|
|
3730
|
+
for (const event of registered) {
|
|
3731
|
+
if (eventStatuses.get(event.code) === "reviewed") {
|
|
3732
|
+
eventStatuses.set(event.code, "build_verified");
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
await saveProgress();
|
|
3736
|
+
const failures = registered.filter((event) => eventStatuses.get(event.code) !== "build_verified");
|
|
3737
|
+
if (failures.length > 0) {
|
|
3738
|
+
const reason = `${failures.length} event${failures.length === 1 ? "" : "s"} could not be verified`;
|
|
3739
|
+
await failRun(reason);
|
|
3740
|
+
finishTimings();
|
|
3741
|
+
return {
|
|
3742
|
+
kind: "partial",
|
|
3743
|
+
runId,
|
|
3744
|
+
registered,
|
|
3745
|
+
eventStatuses,
|
|
3746
|
+
reportEvents: buildReportEvents(registered, eventStatuses, eventFailureReasons),
|
|
3747
|
+
changedFiles: [...changedFiles2].sort(),
|
|
3748
|
+
identifyWired,
|
|
3749
|
+
applied: changedFiles2.size > 0,
|
|
3750
|
+
summary: summarize(registered, eventStatuses).join("\n"),
|
|
3751
|
+
verificationStatus,
|
|
3752
|
+
phaseTimings,
|
|
3753
|
+
modelRoutes: modelRoutes(input.models)
|
|
3754
|
+
};
|
|
3092
3755
|
}
|
|
3093
3756
|
await patchPhase("reporting");
|
|
3094
3757
|
await runs.completeRun(runId, {
|
|
@@ -3098,12 +3761,12 @@ async function runEngine(input) {
|
|
|
3098
3761
|
...finalVerify.command ? { verificationCommand: finalVerify.command } : {},
|
|
3099
3762
|
events: buildEvidence(registered, eventStatuses)
|
|
3100
3763
|
});
|
|
3101
|
-
await removeWorktree(worktree).catch(() => {
|
|
3764
|
+
if (worktree) await removeWorktree(worktree).catch(() => {
|
|
3102
3765
|
});
|
|
3103
3766
|
const summaryLines = summarize(registered, eventStatuses);
|
|
3104
|
-
|
|
3767
|
+
finishTimings();
|
|
3105
3768
|
return {
|
|
3106
|
-
kind:
|
|
3769
|
+
kind: "completed",
|
|
3107
3770
|
runId,
|
|
3108
3771
|
registered,
|
|
3109
3772
|
eventStatuses,
|
|
@@ -3111,7 +3774,10 @@ async function runEngine(input) {
|
|
|
3111
3774
|
changedFiles: [...changedFiles2].sort(),
|
|
3112
3775
|
identifyWired,
|
|
3113
3776
|
applied: changedFiles2.size > 0,
|
|
3114
|
-
summary: summaryLines.join("\n")
|
|
3777
|
+
summary: summaryLines.join("\n"),
|
|
3778
|
+
verificationStatus,
|
|
3779
|
+
phaseTimings,
|
|
3780
|
+
modelRoutes: modelRoutes(input.models)
|
|
3115
3781
|
};
|
|
3116
3782
|
} catch (error) {
|
|
3117
3783
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -3123,8 +3789,9 @@ async function runEngine(input) {
|
|
|
3123
3789
|
}
|
|
3124
3790
|
}
|
|
3125
3791
|
await failRun(reason);
|
|
3126
|
-
await removeWorktree(worktree).catch(() => {
|
|
3792
|
+
if (worktree) await removeWorktree(worktree).catch(() => {
|
|
3127
3793
|
});
|
|
3794
|
+
finishTimings();
|
|
3128
3795
|
return {
|
|
3129
3796
|
kind: "partial",
|
|
3130
3797
|
runId,
|
|
@@ -3134,9 +3801,154 @@ async function runEngine(input) {
|
|
|
3134
3801
|
changedFiles: [...changedFiles2].sort(),
|
|
3135
3802
|
identifyWired,
|
|
3136
3803
|
applied: changedFiles2.size > 0,
|
|
3137
|
-
summary: `Integration stopped early: ${reason}. Saved event progress will be verified on rerun
|
|
3804
|
+
summary: `Integration stopped early: ${reason}. Saved event progress will be verified on rerun.`,
|
|
3805
|
+
verificationStatus,
|
|
3806
|
+
phaseTimings,
|
|
3807
|
+
modelRoutes: modelRoutes(input.models)
|
|
3808
|
+
};
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
function artifactContent(bootstrap, kind) {
|
|
3812
|
+
return bootstrap.artifacts.find((artifact) => artifact.kind === kind)?.content;
|
|
3813
|
+
}
|
|
3814
|
+
function modelConfigForTask(models, task, role) {
|
|
3815
|
+
return models[task] ?? models[role];
|
|
3816
|
+
}
|
|
3817
|
+
function modelMetadata(config) {
|
|
3818
|
+
return {
|
|
3819
|
+
model: config.model,
|
|
3820
|
+
...config.effort ? { effort: config.effort } : {},
|
|
3821
|
+
...config.serviceTier ? { serviceTier: config.serviceTier } : {},
|
|
3822
|
+
fastMode: config.fastMode === true
|
|
3823
|
+
};
|
|
3824
|
+
}
|
|
3825
|
+
function eventRequest(event) {
|
|
3826
|
+
return {
|
|
3827
|
+
code: event.code,
|
|
3828
|
+
name: event.name,
|
|
3829
|
+
reasoning: event.reasoning,
|
|
3830
|
+
anchorFile: event.anchorFile,
|
|
3831
|
+
...event.anchorSymbol ? { anchorSymbol: event.anchorSymbol } : {},
|
|
3832
|
+
...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
|
|
3833
|
+
};
|
|
3834
|
+
}
|
|
3835
|
+
async function runClusterWorker(input, bootstrap, registered, survey, changedFiles2, cluster) {
|
|
3836
|
+
let handle;
|
|
3837
|
+
try {
|
|
3838
|
+
handle = await createWorktree(input.repoPath, input.force ?? false, [...changedFiles2]);
|
|
3839
|
+
const deps = integrationDeps(input, bootstrap, registered, survey, handle, [...changedFiles2]);
|
|
3840
|
+
const integration = await runClusterIntegration(
|
|
3841
|
+
deps,
|
|
3842
|
+
[cluster],
|
|
3843
|
+
async () => worktreePatch(handle)
|
|
3844
|
+
);
|
|
3845
|
+
return {
|
|
3846
|
+
cluster,
|
|
3847
|
+
handle,
|
|
3848
|
+
integration,
|
|
3849
|
+
patch: await worktreePatch(handle)
|
|
3850
|
+
};
|
|
3851
|
+
} catch (error) {
|
|
3852
|
+
return {
|
|
3853
|
+
cluster,
|
|
3854
|
+
handle,
|
|
3855
|
+
patch: "",
|
|
3856
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
function integrationDeps(input, bootstrap, registered, survey, worktree, allowedRepairFiles) {
|
|
3861
|
+
return {
|
|
3862
|
+
provider: input.provider,
|
|
3863
|
+
models: input.models,
|
|
3864
|
+
worktree,
|
|
3865
|
+
target: input.target,
|
|
3866
|
+
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
3867
|
+
registered,
|
|
3868
|
+
ingestion: bootstrap.ingestion,
|
|
3869
|
+
survey,
|
|
3870
|
+
allowedRepairFiles: allowedRepairFiles.filter(
|
|
3871
|
+
(file) => file !== generateBindings(input.target, registered).relativePath
|
|
3872
|
+
),
|
|
3873
|
+
sink: input.sink,
|
|
3874
|
+
saveClusters: () => {
|
|
3875
|
+
},
|
|
3876
|
+
discardChanges: async () => discardWorktreeChanges(worktree),
|
|
3877
|
+
onClusterIntegrated: async () => {
|
|
3878
|
+
}
|
|
3879
|
+
};
|
|
3880
|
+
}
|
|
3881
|
+
function groupEventsByFile(events) {
|
|
3882
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3883
|
+
for (const event of events) {
|
|
3884
|
+
const current = grouped.get(event.anchorFile) ?? [];
|
|
3885
|
+
current.push(event);
|
|
3886
|
+
grouped.set(event.anchorFile, current);
|
|
3887
|
+
}
|
|
3888
|
+
return grouped;
|
|
3889
|
+
}
|
|
3890
|
+
async function structuralRepairCheck(repoPath, events, files) {
|
|
3891
|
+
const repairedFiles = new Set(files);
|
|
3892
|
+
for (const [file, fileEvents] of groupEventsByFile(
|
|
3893
|
+
events.filter((event) => repairedFiles.has(event.anchorFile))
|
|
3894
|
+
)) {
|
|
3895
|
+
const result = await structuralClusterCheck(repoPath, {
|
|
3896
|
+
id: clusterIdFor([file], fileEvents.map((event) => event.eventId)),
|
|
3897
|
+
files: [file],
|
|
3898
|
+
events: placementsFor(fileEvents),
|
|
3899
|
+
status: "verified",
|
|
3900
|
+
attempts: 1
|
|
3901
|
+
});
|
|
3902
|
+
if (!result.ok) return result;
|
|
3903
|
+
}
|
|
3904
|
+
return { ok: true, notes: "" };
|
|
3905
|
+
}
|
|
3906
|
+
async function mapLimit(values, limit, operation) {
|
|
3907
|
+
const results = new Array(values.length);
|
|
3908
|
+
let nextIndex = 0;
|
|
3909
|
+
const workers = Array.from({ length: Math.min(limit, values.length) }, async () => {
|
|
3910
|
+
for (; ; ) {
|
|
3911
|
+
const index = nextIndex++;
|
|
3912
|
+
if (index >= values.length) return;
|
|
3913
|
+
results[index] = await operation(values[index]);
|
|
3914
|
+
}
|
|
3915
|
+
});
|
|
3916
|
+
await Promise.all(workers);
|
|
3917
|
+
return results;
|
|
3918
|
+
}
|
|
3919
|
+
async function rollbackFileEventPatches(repoPath, file, patches) {
|
|
3920
|
+
for (let index = patches.length - 1; index >= 0; index -= 1) {
|
|
3921
|
+
const patch = patches[index];
|
|
3922
|
+
if (patch.rolledBack || patch.kind !== "cluster" && patch.kind !== "semantic" || !patch.files.includes(file)) {
|
|
3923
|
+
continue;
|
|
3924
|
+
}
|
|
3925
|
+
await reversePatchInRepo(repoPath, patch.patch);
|
|
3926
|
+
patch.rolledBack = true;
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
async function rollbackOwnedPatches(repoPath, patches) {
|
|
3930
|
+
for (let index = patches.length - 1; index >= 0; index -= 1) {
|
|
3931
|
+
const patch = patches[index];
|
|
3932
|
+
if (patch.rolledBack) continue;
|
|
3933
|
+
await reversePatchInRepo(repoPath, patch.patch);
|
|
3934
|
+
patch.rolledBack = true;
|
|
3935
|
+
}
|
|
3936
|
+
}
|
|
3937
|
+
function modelRoutes(models) {
|
|
3938
|
+
const result = {};
|
|
3939
|
+
for (const task of ["survey", "design", "sdk_setup", "cluster_edit", "review", "repair"]) {
|
|
3940
|
+
const role = task === "survey" || task === "design" ? task : task === "review" ? "review" : "edit";
|
|
3941
|
+
const config = models[task] ?? models[role];
|
|
3942
|
+
result[task] = {
|
|
3943
|
+
model: config.model,
|
|
3944
|
+
...config.effort ? { effort: config.effort } : {},
|
|
3945
|
+
serviceTier: config.fastMode ? "fast" : config.serviceTier ?? "default",
|
|
3946
|
+
maxTurns: config.maxTurns,
|
|
3947
|
+
maxMs: config.maxMs,
|
|
3948
|
+
...config.maxOutputTokens ? { maxOutputTokens: config.maxOutputTokens } : {}
|
|
3138
3949
|
};
|
|
3139
3950
|
}
|
|
3951
|
+
return result;
|
|
3140
3952
|
}
|
|
3141
3953
|
function registeredFromSnapshot(events) {
|
|
3142
3954
|
return events.filter((event) => Boolean(event.anchorFile)).map((event) => ({
|
|
@@ -3149,13 +3961,6 @@ function registeredFromSnapshot(events) {
|
|
|
3149
3961
|
payloadSchema: event.payloadSchema ?? {}
|
|
3150
3962
|
}));
|
|
3151
3963
|
}
|
|
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
3964
|
async function reconcileProgress(repoPath, registered, prior, sink) {
|
|
3160
3965
|
const priorById = new Map((prior?.events ?? []).map((event) => [event.eventId, event]));
|
|
3161
3966
|
const statuses = /* @__PURE__ */ new Map();
|
|
@@ -3182,36 +3987,46 @@ async function reconcileProgress(repoPath, registered, prior, sink) {
|
|
|
3182
3987
|
async function fileContainsWrapper(repoPath, file, eventCode) {
|
|
3183
3988
|
try {
|
|
3184
3989
|
const path = safeRepoPath(repoPath, file);
|
|
3185
|
-
const content = await
|
|
3990
|
+
const content = await readFile4(path, "utf8");
|
|
3186
3991
|
return content.includes(eventWrapperName(eventCode));
|
|
3187
3992
|
} catch {
|
|
3188
3993
|
return false;
|
|
3189
3994
|
}
|
|
3190
3995
|
}
|
|
3191
|
-
async function setupLooksPresent(repoPath, changedFiles2) {
|
|
3996
|
+
async function setupLooksPresent(repoPath, changedFiles2, survey) {
|
|
3192
3997
|
let bindingsFound = false;
|
|
3193
3998
|
let bindingCallFound = false;
|
|
3194
3999
|
let identifyFound = false;
|
|
4000
|
+
let resetFound = false;
|
|
3195
4001
|
let sdkInitializationFound = false;
|
|
4002
|
+
const contents = /* @__PURE__ */ new Map();
|
|
3196
4003
|
for (const file of changedFiles2) {
|
|
3197
4004
|
try {
|
|
3198
|
-
const content = await
|
|
4005
|
+
const content = await readFile4(safeRepoPath(repoPath, file), "utf8");
|
|
4006
|
+
contents.set(file, content);
|
|
3199
4007
|
const generated = content.includes("Generated by @whisperr/wizard");
|
|
3200
4008
|
bindingsFound ||= generated;
|
|
3201
4009
|
if (!generated) {
|
|
3202
4010
|
bindingCallFound ||= content.includes("bindWhisperr(") || content.includes("WhisperrEvents.bind(");
|
|
3203
4011
|
identifyFound ||= /\bidentify\s*\(/.test(content);
|
|
4012
|
+
resetFound ||= /\breset\s*\(/.test(content);
|
|
3204
4013
|
sdkInitializationFound ||= /whisperr/i.test(content) && /(initiali[sz]e|configure|client|setup)/i.test(content);
|
|
3205
4014
|
}
|
|
3206
4015
|
} catch {
|
|
3207
4016
|
}
|
|
3208
4017
|
}
|
|
3209
|
-
|
|
4018
|
+
const identifyAnchorsCovered = (survey?.identifyAnchors ?? []).every(
|
|
4019
|
+
(anchor) => /\bidentify\s*\(/.test(contents.get(anchor.file) ?? "")
|
|
4020
|
+
);
|
|
4021
|
+
const logoutAnchorsCovered = (survey?.logoutAnchors ?? []).every(
|
|
4022
|
+
(anchor) => /\breset\s*\(/.test(contents.get(anchor.file) ?? "")
|
|
4023
|
+
);
|
|
4024
|
+
return bindingsFound && bindingCallFound && identifyFound && resetFound && sdkInitializationFound && identifyAnchorsCovered && logoutAnchorsCovered;
|
|
3210
4025
|
}
|
|
3211
4026
|
function safeRepoPath(repoPath, file) {
|
|
3212
|
-
const root =
|
|
3213
|
-
const path =
|
|
3214
|
-
if (path === root || !path.startsWith(root +
|
|
4027
|
+
const root = resolve4(repoPath);
|
|
4028
|
+
const path = resolve4(root, file);
|
|
4029
|
+
if (path === root || !path.startsWith(root + sep4)) {
|
|
3215
4030
|
throw new Error(`unsafe progress file path: ${file}`);
|
|
3216
4031
|
}
|
|
3217
4032
|
return path;
|
|
@@ -3252,6 +4067,21 @@ function patchFiles(patch) {
|
|
|
3252
4067
|
}
|
|
3253
4068
|
return [...files];
|
|
3254
4069
|
}
|
|
4070
|
+
function patchAddsProductEvents(patch, bindingsFile, events) {
|
|
4071
|
+
const wrappers = events.map((event) => eventWrapperName(event.code));
|
|
4072
|
+
let file = "";
|
|
4073
|
+
for (const line of patch.split("\n")) {
|
|
4074
|
+
const header = /^\+\+\+ b\/(.+)$/.exec(line);
|
|
4075
|
+
if (header?.[1]) {
|
|
4076
|
+
file = header[1];
|
|
4077
|
+
continue;
|
|
4078
|
+
}
|
|
4079
|
+
if (file !== bindingsFile && line.startsWith("+") && !line.startsWith("+++") && wrappers.some((wrapper) => line.includes(wrapper))) {
|
|
4080
|
+
return true;
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
return false;
|
|
4084
|
+
}
|
|
3255
4085
|
|
|
3256
4086
|
// src/engine/providers/claude.ts
|
|
3257
4087
|
import { createSdkMcpServer, query as query2, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -3263,9 +4093,9 @@ import {
|
|
|
3263
4093
|
|
|
3264
4094
|
// src/core/git.ts
|
|
3265
4095
|
import { spawn as spawn3 } from "child_process";
|
|
3266
|
-
import { createHash as
|
|
3267
|
-
import { readFile as
|
|
3268
|
-
import { basename as basename2, join as
|
|
4096
|
+
import { createHash as createHash4 } from "crypto";
|
|
4097
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
4098
|
+
import { basename as basename2, join as join6 } from "path";
|
|
3269
4099
|
async function takeCheckpoint(repoPath) {
|
|
3270
4100
|
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
3271
4101
|
if (!isRepo) return { isRepo: false };
|
|
@@ -3314,7 +4144,7 @@ async function revertToCheckpoint(repoPath, checkpoint) {
|
|
|
3314
4144
|
async function repoFingerprint(repoPath) {
|
|
3315
4145
|
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
3316
4146
|
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename2(repoPath)}`;
|
|
3317
|
-
return
|
|
4147
|
+
return createHash4("sha256").update(seed).digest("hex").slice(0, 16);
|
|
3318
4148
|
}
|
|
3319
4149
|
function normalizeRemote(url) {
|
|
3320
4150
|
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
@@ -3331,7 +4161,7 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
|
3331
4161
|
const contents = [];
|
|
3332
4162
|
for (const file of files) {
|
|
3333
4163
|
try {
|
|
3334
|
-
contents.push({ file, content: await
|
|
4164
|
+
contents.push({ file, content: await readFile5(join6(repoPath, file), "utf8") });
|
|
3335
4165
|
} catch {
|
|
3336
4166
|
continue;
|
|
3337
4167
|
}
|
|
@@ -3361,14 +4191,14 @@ function escapeRegExp(s) {
|
|
|
3361
4191
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3362
4192
|
}
|
|
3363
4193
|
function run(cwd, args) {
|
|
3364
|
-
return new Promise((
|
|
4194
|
+
return new Promise((resolve8) => {
|
|
3365
4195
|
const child = spawn3("git", args, { cwd });
|
|
3366
4196
|
let stdout = "";
|
|
3367
4197
|
let stderr = "";
|
|
3368
4198
|
child.stdout.on("data", (d) => stdout += d.toString());
|
|
3369
4199
|
child.stderr.on("data", (d) => stderr += d.toString());
|
|
3370
|
-
child.on("close", (code) =>
|
|
3371
|
-
child.on("error", () =>
|
|
4200
|
+
child.on("close", (code) => resolve8({ ok: code === 0, stdout, stderr }));
|
|
4201
|
+
child.on("error", () => resolve8({ ok: false, stdout, stderr }));
|
|
3372
4202
|
});
|
|
3373
4203
|
}
|
|
3374
4204
|
|
|
@@ -3607,7 +4437,7 @@ function coverageNote(coverage) {
|
|
|
3607
4437
|
}
|
|
3608
4438
|
|
|
3609
4439
|
// src/core/toolPolicy.ts
|
|
3610
|
-
import { isAbsolute, relative as relative2, resolve as
|
|
4440
|
+
import { isAbsolute, relative as relative2, resolve as resolve5 } from "path";
|
|
3611
4441
|
var SECRET_MATERIAL_DENIAL = "blocked: secret material must not be accessed";
|
|
3612
4442
|
var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
|
|
3613
4443
|
var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
|
|
@@ -3703,8 +4533,8 @@ function isSecretPathLike(value) {
|
|
|
3703
4533
|
}
|
|
3704
4534
|
function isPathInsideRepo(pathValue, repoPath) {
|
|
3705
4535
|
if (pathValue.includes("..")) return false;
|
|
3706
|
-
const repoRoot =
|
|
3707
|
-
const candidate = isAbsolute(pathValue) ?
|
|
4536
|
+
const repoRoot = resolve5(repoPath);
|
|
4537
|
+
const candidate = isAbsolute(pathValue) ? resolve5(pathValue) : resolve5(repoRoot, pathValue);
|
|
3708
4538
|
const rel = relative2(repoRoot, candidate);
|
|
3709
4539
|
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
3710
4540
|
}
|
|
@@ -3721,6 +4551,12 @@ function evaluateReadLike(secretValues, repoPathValues, context) {
|
|
|
3721
4551
|
message: "blocked: reads must stay inside the target repository"
|
|
3722
4552
|
};
|
|
3723
4553
|
}
|
|
4554
|
+
if (context.allowedFiles && !allowedPath(value, context)) {
|
|
4555
|
+
return {
|
|
4556
|
+
behavior: "deny",
|
|
4557
|
+
message: "blocked: this task may only access its explicitly supplied repository files"
|
|
4558
|
+
};
|
|
4559
|
+
}
|
|
3724
4560
|
}
|
|
3725
4561
|
return { behavior: "allow" };
|
|
3726
4562
|
}
|
|
@@ -3738,8 +4574,19 @@ function evaluateWrite(input, context) {
|
|
|
3738
4574
|
if (isSensitiveWritePath(pathValue, context.repoPath)) {
|
|
3739
4575
|
return { behavior: "deny", message: SENSITIVE_WRITE_DENIAL };
|
|
3740
4576
|
}
|
|
4577
|
+
if (context.allowedFiles && !allowedPath(pathValue, context)) {
|
|
4578
|
+
return {
|
|
4579
|
+
behavior: "deny",
|
|
4580
|
+
message: "blocked: this task may only edit its explicitly supplied repository files"
|
|
4581
|
+
};
|
|
4582
|
+
}
|
|
3741
4583
|
return { behavior: "allow" };
|
|
3742
4584
|
}
|
|
4585
|
+
function allowedPath(pathValue, context) {
|
|
4586
|
+
if (!context.allowedFiles) return true;
|
|
4587
|
+
const relativePath = repoRelativePath(pathValue, context.repoPath);
|
|
4588
|
+
return context.allowedFiles.has(relativePath);
|
|
4589
|
+
}
|
|
3743
4590
|
function evaluateBash(input) {
|
|
3744
4591
|
const command = firstString(input, ["command"]);
|
|
3745
4592
|
if (!command) {
|
|
@@ -3828,8 +4675,8 @@ function normalizePathLike(value) {
|
|
|
3828
4675
|
return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
|
|
3829
4676
|
}
|
|
3830
4677
|
function repoRelativePath(pathValue, repoPath) {
|
|
3831
|
-
const repoRoot =
|
|
3832
|
-
const candidate = isAbsolute(pathValue) ?
|
|
4678
|
+
const repoRoot = resolve5(repoPath);
|
|
4679
|
+
const candidate = isAbsolute(pathValue) ? resolve5(pathValue) : resolve5(repoRoot, pathValue);
|
|
3833
4680
|
return normalizePathLike(relative2(repoRoot, candidate));
|
|
3834
4681
|
}
|
|
3835
4682
|
function isSensitiveWritePath(pathValue, repoPath) {
|
|
@@ -4696,7 +5543,7 @@ async function runPass(opts) {
|
|
|
4696
5543
|
}
|
|
4697
5544
|
return { summary, costUsd, ok, maxedOut };
|
|
4698
5545
|
}
|
|
4699
|
-
function buildToolPolicyHooks(repoPath, trustedTools) {
|
|
5546
|
+
function buildToolPolicyHooks(repoPath, trustedTools, allowedFiles) {
|
|
4700
5547
|
return {
|
|
4701
5548
|
PreToolUse: [
|
|
4702
5549
|
{
|
|
@@ -4705,7 +5552,8 @@ function buildToolPolicyHooks(repoPath, trustedTools) {
|
|
|
4705
5552
|
if (input.hook_event_name !== "PreToolUse") return { continue: true };
|
|
4706
5553
|
const decision = evaluateToolUse(input.tool_name, toolInputRecord(input.tool_input), {
|
|
4707
5554
|
repoPath,
|
|
4708
|
-
trustedTools
|
|
5555
|
+
trustedTools,
|
|
5556
|
+
allowedFiles
|
|
4709
5557
|
});
|
|
4710
5558
|
if (decision.behavior === "allow") {
|
|
4711
5559
|
return { continue: true };
|
|
@@ -4723,9 +5571,9 @@ function buildToolPolicyHooks(repoPath, trustedTools) {
|
|
|
4723
5571
|
]
|
|
4724
5572
|
};
|
|
4725
5573
|
}
|
|
4726
|
-
function createToolPermissionCallback(repoPath, trustedTools) {
|
|
5574
|
+
function createToolPermissionCallback(repoPath, trustedTools, allowedFiles) {
|
|
4727
5575
|
return async (toolName, input, options) => {
|
|
4728
|
-
const decision = evaluateToolUse(toolName, input, { repoPath, trustedTools });
|
|
5576
|
+
const decision = evaluateToolUse(toolName, input, { repoPath, trustedTools, allowedFiles });
|
|
4729
5577
|
return decision.behavior === "allow" ? { behavior: "allow", toolUseID: options.toolUseID } : {
|
|
4730
5578
|
behavior: "deny",
|
|
4731
5579
|
message: decision.message,
|
|
@@ -4792,7 +5640,8 @@ function short(s, max = 60) {
|
|
|
4792
5640
|
|
|
4793
5641
|
// src/engine/providers/claude.ts
|
|
4794
5642
|
var READ_ONLY_TOOLS2 = ["Read", "Glob", "Grep"];
|
|
4795
|
-
var
|
|
5643
|
+
var CONSTRAINED_EDIT_TOOLS = ["Read", "Edit", "Write"];
|
|
5644
|
+
var SETUP_TOOLS = ["Read", "Edit", "Write", "Bash"];
|
|
4796
5645
|
var FAST_MODE_MODELS = /* @__PURE__ */ new Set(["claude-opus-5", "claude-opus-4-8"]);
|
|
4797
5646
|
function supportsClaudeFastMode(model) {
|
|
4798
5647
|
return FAST_MODE_MODELS.has(model);
|
|
@@ -4813,10 +5662,10 @@ function createClaudeProvider(config, session) {
|
|
|
4813
5662
|
})
|
|
4814
5663
|
);
|
|
4815
5664
|
const trustedTools = trustedEngineMcpTools(invocation.tools);
|
|
4816
|
-
const
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
5665
|
+
const accessMode = invocation.repositoryAccess?.mode ?? (invocation.allowRepoWrite ? "repair" : "survey");
|
|
5666
|
+
const repositoryTools = accessMode === "none" || accessMode === "review" ? [] : accessMode === "survey" ? READ_ONLY_TOOLS2 : accessMode === "sdk_setup" ? SETUP_TOOLS : CONSTRAINED_EDIT_TOOLS;
|
|
5667
|
+
const allowedTools = [...repositoryTools, ...trustedTools];
|
|
5668
|
+
const allowedFiles = invocation.repositoryAccess?.allowedFiles ? new Set(invocation.repositoryAccess.allowedFiles.map((file) => file.replaceAll("\\", "/"))) : void 0;
|
|
4820
5669
|
let sessionId = invocation.resumeSessionId;
|
|
4821
5670
|
let costUsd = 0;
|
|
4822
5671
|
let turns = 0;
|
|
@@ -4832,7 +5681,8 @@ function createClaudeProvider(config, session) {
|
|
|
4832
5681
|
`X-Whisperr-Wizard-Session-Key: ${headerValue(sessionKey)}`,
|
|
4833
5682
|
`X-Whisperr-Wizard-Model: ${headerValue(roleConfig.model)}`,
|
|
4834
5683
|
`X-Whisperr-Wizard-Effort: ${headerValue(roleConfig.effort ?? "")}`,
|
|
4835
|
-
`X-Whisperr-Wizard-Service-Tier: ${roleConfig.fastMode ? "fast" : "default"}
|
|
5684
|
+
`X-Whisperr-Wizard-Service-Tier: ${roleConfig.fastMode ? "fast" : "default"}`,
|
|
5685
|
+
`X-Whisperr-Wizard-Fallback: ${invocation.fallback ? "true" : "false"}`
|
|
4836
5686
|
].filter(Boolean).join("\n");
|
|
4837
5687
|
const response = query2({
|
|
4838
5688
|
prompt: invocation.userPrompt,
|
|
@@ -4844,9 +5694,10 @@ function createClaudeProvider(config, session) {
|
|
|
4844
5694
|
...roleConfig.effort ? { effort: roleConfig.effort } : {},
|
|
4845
5695
|
tools: [...allowedTools],
|
|
4846
5696
|
...hostTools.length > 0 ? { mcpServers: { engine: createSdkMcpServer({ name: "engine", tools: hostTools }) } } : {},
|
|
4847
|
-
hooks: buildToolPolicyHooks(invocation.cwd, trustedTools),
|
|
4848
|
-
canUseTool: createToolPermissionCallback(invocation.cwd, trustedTools),
|
|
5697
|
+
hooks: buildToolPolicyHooks(invocation.cwd, trustedTools, allowedFiles),
|
|
5698
|
+
canUseTool: createToolPermissionCallback(invocation.cwd, trustedTools, allowedFiles),
|
|
4849
5699
|
maxTurns: roleConfig.maxTurns,
|
|
5700
|
+
...roleConfig.maxOutputTokens ? { taskBudget: { total: roleConfig.maxOutputTokens } } : {},
|
|
4850
5701
|
...roleConfig.fastMode ?? supportsClaudeFastMode(roleConfig.model) ? { settings: { fastMode: true } } : {},
|
|
4851
5702
|
...invocation.resumeSessionId ? { resume: invocation.resumeSessionId } : {},
|
|
4852
5703
|
settingSources: [],
|
|
@@ -4906,28 +5757,32 @@ function headerValue(value) {
|
|
|
4906
5757
|
}
|
|
4907
5758
|
|
|
4908
5759
|
// src/engine/providers/openai.ts
|
|
4909
|
-
import { readdirSync, readFileSync as
|
|
4910
|
-
import { join as
|
|
5760
|
+
import { readdirSync, readFileSync as readFileSync3, statSync, writeFileSync } from "fs";
|
|
5761
|
+
import { join as join7, resolve as resolve6, sep as sep5 } from "path";
|
|
4911
5762
|
function jailedPath(cwd, candidate) {
|
|
4912
|
-
const resolved =
|
|
4913
|
-
if (resolved !== cwd && !resolved.startsWith(cwd +
|
|
5763
|
+
const resolved = resolve6(cwd, candidate);
|
|
5764
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + sep5)) {
|
|
4914
5765
|
throw new Error(`path ${candidate} escapes the repository`);
|
|
4915
5766
|
}
|
|
4916
5767
|
return resolved;
|
|
4917
5768
|
}
|
|
4918
|
-
function guardedPath(cwd, toolName, candidate) {
|
|
5769
|
+
function guardedPath(cwd, toolName, candidate, allowedFiles) {
|
|
4919
5770
|
const path = jailedPath(cwd, candidate);
|
|
4920
5771
|
const decision = evaluateToolUse(
|
|
4921
5772
|
toolName,
|
|
4922
5773
|
toolName === "Glob" ? { path: candidate, pattern: "*" } : { file_path: candidate },
|
|
4923
|
-
{ repoPath: cwd }
|
|
5774
|
+
{ repoPath: cwd, allowedFiles }
|
|
4924
5775
|
);
|
|
4925
5776
|
if (decision.behavior === "deny") {
|
|
4926
5777
|
throw new Error(decision.message);
|
|
4927
5778
|
}
|
|
4928
5779
|
return path;
|
|
4929
5780
|
}
|
|
4930
|
-
function fileTools(cwd, allowWrite) {
|
|
5781
|
+
function fileTools(cwd, allowWrite, access = { mode: allowWrite ? "repair" : "survey" }) {
|
|
5782
|
+
if (access.mode === "none" || access.mode === "review") {
|
|
5783
|
+
return [];
|
|
5784
|
+
}
|
|
5785
|
+
const allowedFiles = access.allowedFiles ? new Set(access.allowedFiles.map((file) => file.replaceAll("\\", "/"))) : void 0;
|
|
4931
5786
|
const tools = [
|
|
4932
5787
|
{
|
|
4933
5788
|
name: "read_file",
|
|
@@ -4939,8 +5794,8 @@ function fileTools(cwd, allowWrite) {
|
|
|
4939
5794
|
required: ["path"]
|
|
4940
5795
|
},
|
|
4941
5796
|
handler: async (input) => {
|
|
4942
|
-
const path = guardedPath(cwd, "Read", String(input.path ?? ""));
|
|
4943
|
-
const content =
|
|
5797
|
+
const path = guardedPath(cwd, "Read", String(input.path ?? ""), allowedFiles);
|
|
5798
|
+
const content = readFileSync3(path, "utf8");
|
|
4944
5799
|
return { content: content.slice(0, 4e4) };
|
|
4945
5800
|
}
|
|
4946
5801
|
},
|
|
@@ -4954,9 +5809,9 @@ function fileTools(cwd, allowWrite) {
|
|
|
4954
5809
|
required: ["path"]
|
|
4955
5810
|
},
|
|
4956
5811
|
handler: async (input) => {
|
|
4957
|
-
const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."));
|
|
5812
|
+
const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."), allowedFiles);
|
|
4958
5813
|
const entries = readdirSync(target9).filter((entry) => !isSecretPathLike(entry)).map((entry) => {
|
|
4959
|
-
const kind = statSync(
|
|
5814
|
+
const kind = statSync(join7(target9, entry)).isDirectory() ? "dir" : "file";
|
|
4960
5815
|
return `${kind}:${entry}`;
|
|
4961
5816
|
});
|
|
4962
5817
|
return { entries: entries.slice(0, 500) };
|
|
@@ -4975,7 +5830,7 @@ function fileTools(cwd, allowWrite) {
|
|
|
4975
5830
|
required: ["path", "content"]
|
|
4976
5831
|
},
|
|
4977
5832
|
handler: async (input) => {
|
|
4978
|
-
const path = guardedPath(cwd, "Write", String(input.path ?? ""));
|
|
5833
|
+
const path = guardedPath(cwd, "Write", String(input.path ?? ""), allowedFiles);
|
|
4979
5834
|
writeFileSync(path, String(input.content ?? ""));
|
|
4980
5835
|
return { ok: true };
|
|
4981
5836
|
}
|
|
@@ -4994,8 +5849,8 @@ function fileTools(cwd, allowWrite) {
|
|
|
4994
5849
|
required: ["path", "oldText", "newText"]
|
|
4995
5850
|
},
|
|
4996
5851
|
handler: async (input) => {
|
|
4997
|
-
const path = guardedPath(cwd, "Edit", String(input.path ?? ""));
|
|
4998
|
-
const content =
|
|
5852
|
+
const path = guardedPath(cwd, "Edit", String(input.path ?? ""), allowedFiles);
|
|
5853
|
+
const content = readFileSync3(path, "utf8");
|
|
4999
5854
|
const oldText = String(input.oldText ?? "");
|
|
5000
5855
|
if (!content.includes(oldText)) {
|
|
5001
5856
|
return { ok: false, reason: "oldText not found" };
|
|
@@ -5025,7 +5880,8 @@ function createOpenAIProvider(config, session) {
|
|
|
5025
5880
|
Authorization: `Bearer ${session.token}`,
|
|
5026
5881
|
...sessionKey === "legacy" ? {} : {
|
|
5027
5882
|
"X-Whisperr-Wizard-Session-Key": sessionKey,
|
|
5028
|
-
"X-Whisperr-Wizard-Task": task
|
|
5883
|
+
"X-Whisperr-Wizard-Task": task,
|
|
5884
|
+
"X-Whisperr-Wizard-Fallback": sessionKey.startsWith("fallback:") ? "true" : "false"
|
|
5029
5885
|
}
|
|
5030
5886
|
},
|
|
5031
5887
|
body: JSON.stringify(body)
|
|
@@ -5094,7 +5950,10 @@ function createOpenAIProvider(config, session) {
|
|
|
5094
5950
|
const sessionKey = invocation.sessionKey?.trim() || "legacy";
|
|
5095
5951
|
let activeConversationId = sessionKey === "legacy" ? legacyConversationId : conversationIds.get(sessionKey);
|
|
5096
5952
|
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 = [
|
|
5953
|
+
const allTools = [
|
|
5954
|
+
...invocation.tools,
|
|
5955
|
+
...fileTools(invocation.cwd, invocation.allowRepoWrite, invocation.repositoryAccess)
|
|
5956
|
+
];
|
|
5098
5957
|
const toolsByName = new Map(allTools.map((tool2) => [tool2.name, tool2]));
|
|
5099
5958
|
const toolDefs = allTools.map((tool2) => ({
|
|
5100
5959
|
type: "function",
|
|
@@ -5125,7 +5984,8 @@ function createOpenAIProvider(config, session) {
|
|
|
5125
5984
|
input,
|
|
5126
5985
|
tools: toolDefs,
|
|
5127
5986
|
...roleConfig.effort ? { reasoning: { effort: roleConfig.effort } } : {},
|
|
5128
|
-
...roleConfig.serviceTier ? { service_tier: roleConfig.serviceTier } : {}
|
|
5987
|
+
...roleConfig.serviceTier ? { service_tier: roleConfig.serviceTier } : {},
|
|
5988
|
+
...roleConfig.maxOutputTokens ? { max_output_tokens: roleConfig.maxOutputTokens } : {}
|
|
5129
5989
|
}, sessionKey, task);
|
|
5130
5990
|
const items = response.output ?? [];
|
|
5131
5991
|
const calls = items.filter(
|
|
@@ -5181,7 +6041,7 @@ var Semaphore = class {
|
|
|
5181
6041
|
waiting = [];
|
|
5182
6042
|
async run(operation) {
|
|
5183
6043
|
if (this.active >= this.limit) {
|
|
5184
|
-
await new Promise((
|
|
6044
|
+
await new Promise((resolve8) => this.waiting.push(resolve8));
|
|
5185
6045
|
}
|
|
5186
6046
|
this.active += 1;
|
|
5187
6047
|
try {
|
|
@@ -5193,10 +6053,7 @@ var Semaphore = class {
|
|
|
5193
6053
|
}
|
|
5194
6054
|
};
|
|
5195
6055
|
function shouldFallback(outcome) {
|
|
5196
|
-
|
|
5197
|
-
return true;
|
|
5198
|
-
}
|
|
5199
|
-
return outcome.kind === "failed" && classifyFailure(outcome.error) !== "fatal";
|
|
6056
|
+
return outcome.kind !== "completed";
|
|
5200
6057
|
}
|
|
5201
6058
|
function shouldDisableProvider(outcome) {
|
|
5202
6059
|
return outcome.kind === "failed" && classifyFailure(outcome.error) === "transient";
|
|
@@ -5214,21 +6071,27 @@ function createRoutedProvider(routes, fallback, fallbackConcurrency = 2) {
|
|
|
5214
6071
|
...[...normalized.values()].map((route) => route.provider)
|
|
5215
6072
|
]);
|
|
5216
6073
|
const semaphores = /* @__PURE__ */ new Map();
|
|
5217
|
-
const
|
|
5218
|
-
const semaphoreFor = (
|
|
5219
|
-
let semaphore = semaphores.get(
|
|
6074
|
+
const disabledRoutes = /* @__PURE__ */ new Set();
|
|
6075
|
+
const semaphoreFor = (key, concurrency) => {
|
|
6076
|
+
let semaphore = semaphores.get(key);
|
|
5220
6077
|
if (!semaphore) {
|
|
5221
|
-
semaphore = new Semaphore(concurrency ??
|
|
5222
|
-
semaphores.set(
|
|
6078
|
+
semaphore = new Semaphore(concurrency ?? fallbackConcurrency);
|
|
6079
|
+
semaphores.set(key, semaphore);
|
|
5223
6080
|
}
|
|
5224
6081
|
return semaphore;
|
|
5225
6082
|
};
|
|
6083
|
+
const invocationRouteKey = (invocation, config) => `${invocation.task ?? invocation.role}:${config.model}`;
|
|
5226
6084
|
const routeFor = (invocation) => normalized.get(invocation.task ?? invocation.role) ?? normalized.get(invocation.role);
|
|
5227
|
-
const call = (provider, concurrency, invocation, config, onProgress) => semaphoreFor(
|
|
6085
|
+
const call = (provider, concurrency, invocation, config, onProgress, key = invocationRouteKey(invocation, config)) => semaphoreFor(key, concurrency).run(() => provider.invoke(invocation, config, onProgress));
|
|
6086
|
+
const fallbackInvocation = (invocation) => ({
|
|
6087
|
+
...invocation,
|
|
6088
|
+
fallback: true,
|
|
6089
|
+
sessionKey: `fallback:${invocation.sessionKey ?? invocation.task ?? invocation.role}`
|
|
6090
|
+
});
|
|
5228
6091
|
return {
|
|
5229
6092
|
name: "routed",
|
|
5230
6093
|
onRunReady(runId, resumeSessionId) {
|
|
5231
|
-
|
|
6094
|
+
disabledRoutes.clear();
|
|
5232
6095
|
for (const provider of providers) {
|
|
5233
6096
|
provider.onRunReady?.(runId, resumeSessionId);
|
|
5234
6097
|
}
|
|
@@ -5238,28 +6101,31 @@ function createRoutedProvider(routes, fallback, fallbackConcurrency = 2) {
|
|
|
5238
6101
|
if (!route) {
|
|
5239
6102
|
return call(fallback, fallbackConcurrency, invocation, config, onProgress);
|
|
5240
6103
|
}
|
|
5241
|
-
|
|
6104
|
+
const routeKey = invocationRouteKey(invocation, config);
|
|
6105
|
+
if (disabledRoutes.has(routeKey)) {
|
|
5242
6106
|
return call(
|
|
5243
6107
|
fallback,
|
|
5244
6108
|
fallbackConcurrency,
|
|
5245
|
-
invocation,
|
|
6109
|
+
fallbackInvocation(invocation),
|
|
5246
6110
|
route.fallbackConfig ?? config,
|
|
5247
|
-
onProgress
|
|
6111
|
+
onProgress,
|
|
6112
|
+
`fallback:${routeKey}`
|
|
5248
6113
|
);
|
|
5249
6114
|
}
|
|
5250
6115
|
return call(route.provider, route.concurrency, invocation, config, onProgress).then(
|
|
5251
6116
|
async (outcome) => {
|
|
5252
6117
|
if (shouldDisableProvider(outcome)) {
|
|
5253
|
-
|
|
6118
|
+
disabledRoutes.add(routeKey);
|
|
5254
6119
|
}
|
|
5255
6120
|
if (!invocation.allowRepoWrite && shouldFallback(outcome)) {
|
|
5256
6121
|
onProgress(`${route.provider.name} unavailable; continuing with ${fallback.name}`);
|
|
5257
6122
|
return call(
|
|
5258
6123
|
fallback,
|
|
5259
6124
|
fallbackConcurrency,
|
|
5260
|
-
invocation,
|
|
6125
|
+
fallbackInvocation(invocation),
|
|
5261
6126
|
route.fallbackConfig ?? config,
|
|
5262
|
-
onProgress
|
|
6127
|
+
onProgress,
|
|
6128
|
+
`fallback:${routeKey}`
|
|
5263
6129
|
);
|
|
5264
6130
|
}
|
|
5265
6131
|
return outcome;
|
|
@@ -5267,7 +6133,14 @@ function createRoutedProvider(routes, fallback, fallbackConcurrency = 2) {
|
|
|
5267
6133
|
);
|
|
5268
6134
|
},
|
|
5269
6135
|
invokeFallback(invocation, config, onProgress) {
|
|
5270
|
-
return call(
|
|
6136
|
+
return call(
|
|
6137
|
+
fallback,
|
|
6138
|
+
fallbackConcurrency,
|
|
6139
|
+
fallbackInvocation(invocation),
|
|
6140
|
+
config,
|
|
6141
|
+
onProgress,
|
|
6142
|
+
`fallback:${invocationRouteKey(invocation, config)}`
|
|
6143
|
+
);
|
|
5271
6144
|
}
|
|
5272
6145
|
};
|
|
5273
6146
|
}
|
|
@@ -5277,14 +6150,53 @@ var MINUTE = 6e4;
|
|
|
5277
6150
|
function engineModelConfig(config) {
|
|
5278
6151
|
const model = (task, fallback) => process.env[`WHISPERR_WIZARD_ENGINE_${task.toUpperCase()}_MODEL`]?.trim() || fallback;
|
|
5279
6152
|
const terra = "gpt-5.6-terra";
|
|
6153
|
+
const sol = "gpt-5.6-sol";
|
|
5280
6154
|
const opus = "claude-opus-5";
|
|
5281
6155
|
const opusOnly = process.env.WHISPERR_WIZARD_PRESET?.trim() === "claude-default" || config.offline === true;
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
6156
|
+
if (opusOnly) {
|
|
6157
|
+
const survey2 = { model: model("survey", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 5 * MINUTE };
|
|
6158
|
+
const design2 = { model: model("design", opus), effort: "medium", fastMode: true, maxTurns: 80, maxMs: 20 * MINUTE };
|
|
6159
|
+
const sdkSetup2 = { model: model("sdk_setup", opus), effort: "medium", fastMode: true, maxTurns: 60, maxMs: 12 * MINUTE };
|
|
6160
|
+
const clusterEdit2 = { model: model("cluster_edit", opus), effort: "medium", fastMode: true, maxTurns: 60, maxMs: 12 * MINUTE };
|
|
6161
|
+
const repair2 = { model: model("repair", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 8 * MINUTE };
|
|
6162
|
+
const review2 = { model: model("review", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 8 * MINUTE };
|
|
6163
|
+
return {
|
|
6164
|
+
survey: survey2,
|
|
6165
|
+
design: design2,
|
|
6166
|
+
edit: sdkSetup2,
|
|
6167
|
+
review: review2,
|
|
6168
|
+
sdk_setup: sdkSetup2,
|
|
6169
|
+
cluster_edit: clusterEdit2,
|
|
6170
|
+
repair: repair2
|
|
6171
|
+
};
|
|
6172
|
+
}
|
|
6173
|
+
const survey = { model: model("survey", opus), effort: "low", fastMode: true, maxTurns: 30, maxMs: 5 * MINUTE };
|
|
6174
|
+
const design = { model: model("design", opus), effort: "low", fastMode: true, maxTurns: 20, maxMs: 5 * MINUTE };
|
|
6175
|
+
const sdkSetup = { model: model("sdk_setup", opus), effort: "low", fastMode: true, maxTurns: 30, maxMs: 6 * MINUTE };
|
|
6176
|
+
const clusterEdit = opusOnly ? { model: model("cluster_edit", opus), effort: "medium", fastMode: true, maxTurns: 60, maxMs: 12 * MINUTE } : {
|
|
6177
|
+
model: model("cluster_edit", terra),
|
|
6178
|
+
effort: "medium",
|
|
6179
|
+
serviceTier: "priority",
|
|
6180
|
+
maxTurns: 20,
|
|
6181
|
+
maxMs: 4 * MINUTE,
|
|
6182
|
+
maxOutputTokens: 8e3
|
|
6183
|
+
};
|
|
6184
|
+
const repair = {
|
|
6185
|
+
model: model("repair", sol),
|
|
6186
|
+
effort: "medium",
|
|
6187
|
+
serviceTier: "priority",
|
|
6188
|
+
maxTurns: 30,
|
|
6189
|
+
maxMs: 8 * MINUTE,
|
|
6190
|
+
maxOutputTokens: 12e3
|
|
6191
|
+
};
|
|
6192
|
+
const review = {
|
|
6193
|
+
model: model("review", sol),
|
|
6194
|
+
effort: "medium",
|
|
6195
|
+
serviceTier: "priority",
|
|
6196
|
+
maxTurns: 8,
|
|
6197
|
+
maxMs: 3 * MINUTE,
|
|
6198
|
+
maxOutputTokens: 4e3
|
|
6199
|
+
};
|
|
5288
6200
|
return {
|
|
5289
6201
|
survey,
|
|
5290
6202
|
design,
|
|
@@ -5303,21 +6215,19 @@ function engineProvider(config, session) {
|
|
|
5303
6215
|
return createRoutedProvider({}, claude, 2);
|
|
5304
6216
|
}
|
|
5305
6217
|
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
6218
|
return createRoutedProvider({
|
|
5314
|
-
survey: { provider:
|
|
5315
|
-
|
|
5316
|
-
|
|
6219
|
+
survey: { provider: claude, concurrency: 3, fallbackConfig: models.repair },
|
|
6220
|
+
design: { provider: claude, concurrency: 3, fallbackConfig: models.repair },
|
|
6221
|
+
sdk_setup: { provider: claude, concurrency: 2, fallbackConfig: models.repair },
|
|
6222
|
+
cluster_edit: { provider: openai, concurrency: 4, fallbackConfig: models.repair },
|
|
6223
|
+
review: { provider: openai, concurrency: 2, fallbackConfig: models.review },
|
|
6224
|
+
repair: { provider: openai, concurrency: 2, fallbackConfig: models.repair }
|
|
6225
|
+
}, openai, 2);
|
|
5317
6226
|
}
|
|
5318
6227
|
async function tryEngineFlow(options) {
|
|
5319
6228
|
const { repoPath, fingerprint, config, session, playbook, theme: theme2 } = options;
|
|
5320
6229
|
const startedAt = Date.now();
|
|
6230
|
+
const models = engineModelConfig(config);
|
|
5321
6231
|
const sink = createProgressSink({
|
|
5322
6232
|
json: false,
|
|
5323
6233
|
isTTY: Boolean(process.stdout.isTTY),
|
|
@@ -5329,16 +6239,13 @@ async function tryEngineFlow(options) {
|
|
|
5329
6239
|
config,
|
|
5330
6240
|
session,
|
|
5331
6241
|
provider: engineProvider(config, session),
|
|
5332
|
-
models
|
|
6242
|
+
models,
|
|
5333
6243
|
target: playbook.target.id,
|
|
5334
6244
|
projectKind: ["node", "python", "php", "go", "express", "django", "fastapi", "laravel", "spring-boot"].includes(
|
|
5335
6245
|
playbook.target.id
|
|
5336
6246
|
) ? "backend" : "frontend",
|
|
5337
6247
|
playbookSystemPrompt: playbook.systemPrompt,
|
|
5338
|
-
sink
|
|
5339
|
-
callbacks: {
|
|
5340
|
-
reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2)
|
|
5341
|
-
}
|
|
6248
|
+
sink
|
|
5342
6249
|
});
|
|
5343
6250
|
if (result.kind === "wrong_mode") {
|
|
5344
6251
|
return { handled: false, exitCode: 0 };
|
|
@@ -5355,11 +6262,18 @@ async function tryEngineFlow(options) {
|
|
|
5355
6262
|
target: playbook.target.id,
|
|
5356
6263
|
repo_fingerprint: fingerprint,
|
|
5357
6264
|
identify_wired: result.identifyWired,
|
|
5358
|
-
verified: null,
|
|
6265
|
+
verified: result.verificationStatus === "passed" ? true : result.verificationStatus === "failed" ? false : null,
|
|
6266
|
+
verification_status: result.verificationStatus,
|
|
5359
6267
|
cost_usd: 0,
|
|
5360
6268
|
duration_ms: Date.now() - startedAt,
|
|
5361
6269
|
summary: result.summary.slice(0, 2e3),
|
|
5362
6270
|
events: result.reportEvents,
|
|
6271
|
+
wizard_version: packageVersion(),
|
|
6272
|
+
planner_model: models.survey.model,
|
|
6273
|
+
editor_model: models.cluster_edit?.model ?? models.edit.model,
|
|
6274
|
+
effort: models.cluster_edit?.effort ?? models.edit.effort,
|
|
6275
|
+
phase_timings: result.phaseTimings,
|
|
6276
|
+
model_routes: result.modelRoutes,
|
|
5363
6277
|
exit_class: result.kind === "completed" ? "engine_completed" : "engine_partial"
|
|
5364
6278
|
});
|
|
5365
6279
|
if (result.applied && result.registered.length > 0) {
|
|
@@ -5372,32 +6286,6 @@ async function tryEngineFlow(options) {
|
|
|
5372
6286
|
}
|
|
5373
6287
|
return { handled: true, exitCode: result.kind === "completed" ? 0 : 1 };
|
|
5374
6288
|
}
|
|
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
6289
|
|
|
5402
6290
|
// src/core/opportunities.ts
|
|
5403
6291
|
function normalizeCode(value) {
|
|
@@ -5630,7 +6518,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5630
6518
|
return Promise.resolve({ ran: false, ok: true, output: "" });
|
|
5631
6519
|
}
|
|
5632
6520
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
5633
|
-
return new Promise((
|
|
6521
|
+
return new Promise((resolve8) => {
|
|
5634
6522
|
const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
|
|
5635
6523
|
let out = "";
|
|
5636
6524
|
const append = (d) => {
|
|
@@ -5641,11 +6529,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5641
6529
|
child.stderr?.on("data", append);
|
|
5642
6530
|
const timer = setTimeout(() => {
|
|
5643
6531
|
child.kill("SIGKILL");
|
|
5644
|
-
|
|
6532
|
+
resolve8({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
|
|
5645
6533
|
}, timeoutMs);
|
|
5646
6534
|
child.on("close", (code) => {
|
|
5647
6535
|
clearTimeout(timer);
|
|
5648
|
-
|
|
6536
|
+
resolve8({
|
|
5649
6537
|
ran: true,
|
|
5650
6538
|
ok: code === 0,
|
|
5651
6539
|
command,
|
|
@@ -5657,7 +6545,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5657
6545
|
});
|
|
5658
6546
|
child.on("error", () => {
|
|
5659
6547
|
clearTimeout(timer);
|
|
5660
|
-
|
|
6548
|
+
resolve8({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
|
|
5661
6549
|
});
|
|
5662
6550
|
});
|
|
5663
6551
|
}
|
|
@@ -5666,25 +6554,6 @@ function tail2(s) {
|
|
|
5666
6554
|
return t.length > MAX_OUTPUT ? `\u2026${t.slice(-MAX_OUTPUT)}` : t;
|
|
5667
6555
|
}
|
|
5668
6556
|
|
|
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
6557
|
// src/core/gapreport.ts
|
|
5689
6558
|
function buildGapReport(input) {
|
|
5690
6559
|
const outcomes = new Map(
|
|
@@ -5814,7 +6683,7 @@ function banner() {
|
|
|
5814
6683
|
|
|
5815
6684
|
// src/cli.ts
|
|
5816
6685
|
async function run2(options) {
|
|
5817
|
-
const repoPath =
|
|
6686
|
+
const repoPath = resolve7(options.path ?? process.cwd());
|
|
5818
6687
|
const config = resolveConfig(options);
|
|
5819
6688
|
console.log(banner());
|
|
5820
6689
|
p2.intro(theme.signal("Let's wire Whisperr into your app."));
|
|
@@ -5926,40 +6795,23 @@ Flutter is live today; ${theme.bright(
|
|
|
5926
6795
|
}
|
|
5927
6796
|
const tick = useIntegrationSpinner ? setInterval(updateIntegrationMessage, 1e3) : void 0;
|
|
5928
6797
|
const additions = { proposed: 0, applied: [] };
|
|
5929
|
-
const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
5930
6798
|
const outcome = await runIntegrationAgent({
|
|
5931
6799
|
repoPath,
|
|
5932
6800
|
config,
|
|
5933
6801
|
session,
|
|
5934
6802
|
playbook: chosen.playbook,
|
|
5935
6803
|
manifest,
|
|
5936
|
-
// The plan is
|
|
5937
|
-
//
|
|
5938
|
-
// default run reads it out and keeps going.
|
|
6804
|
+
// The plan is informational. Event selection is frozen automatically, so
|
|
6805
|
+
// integration never pauses at a terminal approval prompt.
|
|
5939
6806
|
async onPlanReady(plan) {
|
|
5940
6807
|
if (useIntegrationSpinner) {
|
|
5941
6808
|
spin.stop(theme.success("Placement plan ready"));
|
|
5942
6809
|
}
|
|
5943
6810
|
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
6811
|
if (useIntegrationSpinner) {
|
|
5960
6812
|
spin.start(theme.bright("Continuing integration"));
|
|
5961
6813
|
}
|
|
5962
|
-
return
|
|
6814
|
+
return plan;
|
|
5963
6815
|
},
|
|
5964
6816
|
async onOpportunitiesReady(raw) {
|
|
5965
6817
|
if (useIntegrationSpinner) spin.stop(theme.success("Planning done"));
|
|
@@ -6527,12 +7379,6 @@ function formatDuration(ms) {
|
|
|
6527
7379
|
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
6528
7380
|
return `${minutes}m${seconds}s`;
|
|
6529
7381
|
}
|
|
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
7382
|
function renderPlacementPlan(plan) {
|
|
6537
7383
|
return plan.map((entry) => {
|
|
6538
7384
|
if (entry.decision === "place") {
|
|
@@ -6658,7 +7504,6 @@ function printHelp() {
|
|
|
6658
7504
|
` ${theme.bright("Options")}`,
|
|
6659
7505
|
" --offline Use a demo manifest, no account/browser needed",
|
|
6660
7506
|
" --force Proceed without a clean git tree (no safe undo)",
|
|
6661
|
-
" --review-plan Confirm the placement plan before anything is wired",
|
|
6662
7507
|
" --propose-only Send new events/interventions for dashboard approval",
|
|
6663
7508
|
" instead of adding them to your universe now",
|
|
6664
7509
|
" --strict-review Review the finished diff with the deeper planner model",
|