@whisperr/wizard 0.7.4 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -18
- package/dist/index.js +1809 -546
- 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) {
|
|
@@ -1515,11 +1534,13 @@ function eventWrapperName(code) {
|
|
|
1515
1534
|
|
|
1516
1535
|
// src/engine/clusters.ts
|
|
1517
1536
|
import { createHash } from "crypto";
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1537
|
+
var MAX_CLUSTER_EVENTS = 4;
|
|
1538
|
+
function clusterIdFor(files, eventIds = [], slicePosition = 0) {
|
|
1539
|
+
const digest = createHash("sha256").update(JSON.stringify({
|
|
1540
|
+
files: [...files].sort(),
|
|
1541
|
+
events: [...eventIds].sort(),
|
|
1542
|
+
slicePosition
|
|
1543
|
+
})).digest("hex");
|
|
1523
1544
|
return `cluster_${digest.slice(0, 12)}`;
|
|
1524
1545
|
}
|
|
1525
1546
|
function deriveClusters(placements) {
|
|
@@ -1529,47 +1550,35 @@ function deriveClusters(placements) {
|
|
|
1529
1550
|
existing.push(placement);
|
|
1530
1551
|
byFile.set(placement.file, existing);
|
|
1531
1552
|
}
|
|
1532
|
-
const
|
|
1533
|
-
for (const
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
bucket.events.push(...events);
|
|
1538
|
-
byDirectory.set(directory, bucket);
|
|
1539
|
-
}
|
|
1540
|
-
const clusters = [];
|
|
1541
|
-
const pending = [];
|
|
1542
|
-
for (const directory of [...byDirectory.keys()].sort()) {
|
|
1543
|
-
const bucket = byDirectory.get(directory);
|
|
1544
|
-
if (bucket.events.length >= MIN_CLUSTER_EVENTS) {
|
|
1545
|
-
pending.push(bucket);
|
|
1546
|
-
continue;
|
|
1547
|
-
}
|
|
1548
|
-
const last = pending[pending.length - 1];
|
|
1549
|
-
if (last && last.events.length + bucket.events.length <= MAX_CLUSTER_EVENTS) {
|
|
1550
|
-
for (const file of bucket.files) last.files.add(file);
|
|
1551
|
-
last.events.push(...bucket.events);
|
|
1552
|
-
} else {
|
|
1553
|
-
pending.push(bucket);
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
for (const bucket of pending) {
|
|
1557
|
-
const events = [...bucket.events].sort((a, b) => a.eventCode.localeCompare(b.eventCode));
|
|
1553
|
+
const fileSlices = [];
|
|
1554
|
+
for (const file of [...byFile.keys()].sort()) {
|
|
1555
|
+
const events = [...byFile.get(file)].sort(
|
|
1556
|
+
(a, b) => a.eventCode.localeCompare(b.eventCode) || a.eventId.localeCompare(b.eventId)
|
|
1557
|
+
);
|
|
1558
1558
|
for (let start = 0; start < events.length; start += MAX_CLUSTER_EVENTS) {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
files,
|
|
1564
|
-
events: slice,
|
|
1565
|
-
status: "pending",
|
|
1566
|
-
attempts: 0
|
|
1559
|
+
fileSlices.push({
|
|
1560
|
+
file,
|
|
1561
|
+
position: start / MAX_CLUSTER_EVENTS,
|
|
1562
|
+
events: events.slice(start, start + MAX_CLUSTER_EVENTS)
|
|
1567
1563
|
});
|
|
1568
1564
|
}
|
|
1569
1565
|
}
|
|
1570
|
-
return
|
|
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
|
+
});
|
|
1571
1580
|
}
|
|
1572
|
-
var TERMINAL_STATUSES = ["reviewed", "failed_resumable", "blocked"];
|
|
1581
|
+
var TERMINAL_STATUSES = ["verified", "reviewed", "failed_resumable", "blocked"];
|
|
1573
1582
|
function nextPendingCluster(clusters) {
|
|
1574
1583
|
return clusters.find((cluster) => cluster.status === "pending" || cluster.status === "editing");
|
|
1575
1584
|
}
|
|
@@ -1587,8 +1596,10 @@ function transitionCluster(clusters, clusterId, status, note3) {
|
|
|
1587
1596
|
}
|
|
1588
1597
|
|
|
1589
1598
|
// src/engine/integrate.ts
|
|
1590
|
-
import {
|
|
1591
|
-
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";
|
|
1592
1603
|
|
|
1593
1604
|
// src/engine/budgets.ts
|
|
1594
1605
|
var OVERFLOW_PATTERNS = [
|
|
@@ -1600,6 +1611,8 @@ var OVERFLOW_PATTERNS = [
|
|
|
1600
1611
|
];
|
|
1601
1612
|
var TRANSIENT_PATTERNS = [
|
|
1602
1613
|
/rate.?limit/i,
|
|
1614
|
+
/quota/i,
|
|
1615
|
+
/\b429\b/,
|
|
1603
1616
|
/overloaded/i,
|
|
1604
1617
|
/timeout/i,
|
|
1605
1618
|
/timed out/i,
|
|
@@ -1629,10 +1642,15 @@ function nextClusterStatusAfterFailure(failure, attempts) {
|
|
|
1629
1642
|
return "failed_resumable";
|
|
1630
1643
|
}
|
|
1631
1644
|
|
|
1645
|
+
// src/engine/types.ts
|
|
1646
|
+
function modelConfigFor(models, task, role) {
|
|
1647
|
+
return models[task] ?? models[role];
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1632
1650
|
// src/engine/verify.ts
|
|
1633
1651
|
import { spawn } from "child_process";
|
|
1634
|
-
import { readFileSync } from "fs";
|
|
1635
|
-
import { join as
|
|
1652
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1653
|
+
import { join as join3 } from "path";
|
|
1636
1654
|
var JS_TARGETS = /* @__PURE__ */ new Set(["web-js", "nextjs", "node", "react-native"]);
|
|
1637
1655
|
function resolveVerifySteps(repoPath, target9) {
|
|
1638
1656
|
if (target9 === "flutter") {
|
|
@@ -1646,7 +1664,10 @@ function resolveVerifySteps(repoPath, target9) {
|
|
|
1646
1664
|
} else if (hasTsconfig(repoPath)) {
|
|
1647
1665
|
steps.push({ kind: "typecheck", command: "npx", args: ["--no-install", "tsc", "--noEmit"] });
|
|
1648
1666
|
}
|
|
1649
|
-
if (
|
|
1667
|
+
if (scripts.lint) {
|
|
1668
|
+
steps.push({ kind: "lint", command: npmCommand(), args: ["run", "lint"] });
|
|
1669
|
+
}
|
|
1670
|
+
if (scripts.build) {
|
|
1650
1671
|
steps.push({ kind: "build", command: npmCommand(), args: ["run", "build"] });
|
|
1651
1672
|
}
|
|
1652
1673
|
return steps;
|
|
@@ -1655,7 +1676,7 @@ function resolveVerifySteps(repoPath, target9) {
|
|
|
1655
1676
|
}
|
|
1656
1677
|
function packageScripts(repoPath) {
|
|
1657
1678
|
try {
|
|
1658
|
-
const parsed = JSON.parse(
|
|
1679
|
+
const parsed = JSON.parse(readFileSync2(join3(repoPath, "package.json"), "utf8"));
|
|
1659
1680
|
return parsed.scripts ?? {};
|
|
1660
1681
|
} catch {
|
|
1661
1682
|
return {};
|
|
@@ -1663,7 +1684,7 @@ function packageScripts(repoPath) {
|
|
|
1663
1684
|
}
|
|
1664
1685
|
function hasTsconfig(repoPath) {
|
|
1665
1686
|
try {
|
|
1666
|
-
|
|
1687
|
+
readFileSync2(join3(repoPath, "tsconfig.json"), "utf8");
|
|
1667
1688
|
return true;
|
|
1668
1689
|
} catch {
|
|
1669
1690
|
return false;
|
|
@@ -1673,7 +1694,7 @@ function npmCommand() {
|
|
|
1673
1694
|
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
1674
1695
|
}
|
|
1675
1696
|
function runVerifyStep(cwd, step, timeoutMs = 3e5) {
|
|
1676
|
-
return new Promise((
|
|
1697
|
+
return new Promise((resolve8) => {
|
|
1677
1698
|
const child = spawn(step.command, step.args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
1678
1699
|
let output = "";
|
|
1679
1700
|
const capture = (chunk) => {
|
|
@@ -1688,11 +1709,11 @@ function runVerifyStep(cwd, step, timeoutMs = 3e5) {
|
|
|
1688
1709
|
}, timeoutMs);
|
|
1689
1710
|
child.on("error", (error) => {
|
|
1690
1711
|
clearTimeout(timer);
|
|
1691
|
-
|
|
1712
|
+
resolve8({ ...step, ok: false, output: String(error) });
|
|
1692
1713
|
});
|
|
1693
1714
|
child.on("close", (exitCode) => {
|
|
1694
1715
|
clearTimeout(timer);
|
|
1695
|
-
|
|
1716
|
+
resolve8({ ...step, ok: exitCode === 0, output });
|
|
1696
1717
|
});
|
|
1697
1718
|
});
|
|
1698
1719
|
}
|
|
@@ -1703,21 +1724,6 @@ async function runVerifySteps(cwd, steps) {
|
|
|
1703
1724
|
}
|
|
1704
1725
|
return results;
|
|
1705
1726
|
}
|
|
1706
|
-
function verifiedStatus(results) {
|
|
1707
|
-
if (results.length === 0) {
|
|
1708
|
-
return "applied";
|
|
1709
|
-
}
|
|
1710
|
-
if (results.some((result) => !result.ok)) {
|
|
1711
|
-
return "applied";
|
|
1712
|
-
}
|
|
1713
|
-
if (results.some((result) => result.kind === "build")) {
|
|
1714
|
-
return "build_verified";
|
|
1715
|
-
}
|
|
1716
|
-
if (results.some((result) => result.kind === "typecheck" || result.kind === "analyze")) {
|
|
1717
|
-
return "type_verified";
|
|
1718
|
-
}
|
|
1719
|
-
return "syntax_verified";
|
|
1720
|
-
}
|
|
1721
1727
|
function runVerificationStatus(results) {
|
|
1722
1728
|
if (results.length === 0) {
|
|
1723
1729
|
return "unavailable";
|
|
@@ -1740,34 +1746,116 @@ var REVIEW_CHECKLIST = [
|
|
|
1740
1746
|
].join("\n");
|
|
1741
1747
|
async function writeBindingsModule(worktree, target9, registered) {
|
|
1742
1748
|
const bindings = generateBindings(target9, registered);
|
|
1743
|
-
await writeFile(
|
|
1749
|
+
await writeFile(join4(worktree.path, bindings.relativePath), bindings.content);
|
|
1744
1750
|
return bindings;
|
|
1745
1751
|
}
|
|
1746
|
-
async function runSdkSetupPass(deps, bindings) {
|
|
1752
|
+
async function runSdkSetupPass(deps, bindings, validate) {
|
|
1747
1753
|
deps.sink.emit({ phase: "binding", action: "installing SDK and wiring identify()" });
|
|
1748
|
-
const
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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,
|
|
1788
|
+
modelConfigFor(deps.models, "sdk_setup", "edit"),
|
|
1768
1789
|
(action) => deps.sink.emit({ phase: "binding", action })
|
|
1769
1790
|
);
|
|
1770
|
-
|
|
1791
|
+
const passesValidation = async () => {
|
|
1792
|
+
if (!validate) return true;
|
|
1793
|
+
try {
|
|
1794
|
+
return await validate();
|
|
1795
|
+
} catch {
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
};
|
|
1799
|
+
let valid = outcome.kind === "completed" && await passesValidation();
|
|
1800
|
+
if (!valid && deps.provider.invokeFallback) {
|
|
1801
|
+
await deps.discardChanges();
|
|
1802
|
+
await writeFile(join4(deps.worktree.path, bindings.relativePath), bindings.content);
|
|
1803
|
+
deps.sink.emit({ phase: "binding", action: "Opus setup rolled back; retrying with Sol" });
|
|
1804
|
+
outcome = await deps.provider.invokeFallback(
|
|
1805
|
+
{ ...invocation, sessionKey: "sdk_setup:sol" },
|
|
1806
|
+
modelConfigFor(deps.models, "repair", "edit"),
|
|
1807
|
+
(action) => deps.sink.emit({ phase: "binding", action })
|
|
1808
|
+
);
|
|
1809
|
+
valid = outcome.kind === "completed" && await passesValidation();
|
|
1810
|
+
}
|
|
1811
|
+
return valid;
|
|
1812
|
+
}
|
|
1813
|
+
async function sdkSetupFiles(repoPath, survey, bindingsFile) {
|
|
1814
|
+
const candidates = /* @__PURE__ */ new Set([
|
|
1815
|
+
bindingsFile,
|
|
1816
|
+
...survey?.stack.entryPoints ?? [],
|
|
1817
|
+
...survey?.identifyAnchors.map((anchor) => anchor.file) ?? [],
|
|
1818
|
+
...survey?.logoutAnchors.map((anchor) => anchor.file) ?? [],
|
|
1819
|
+
...survey?.featureAreas.flatMap(
|
|
1820
|
+
(area) => area.files.filter(isSdkConfigurationFile)
|
|
1821
|
+
) ?? [],
|
|
1822
|
+
"package.json",
|
|
1823
|
+
"package-lock.json",
|
|
1824
|
+
"pnpm-lock.yaml",
|
|
1825
|
+
"yarn.lock",
|
|
1826
|
+
"bun.lock",
|
|
1827
|
+
"bun.lockb",
|
|
1828
|
+
"pyproject.toml",
|
|
1829
|
+
"requirements.txt",
|
|
1830
|
+
"Pipfile",
|
|
1831
|
+
"Package.swift",
|
|
1832
|
+
"Podfile",
|
|
1833
|
+
"pubspec.yaml",
|
|
1834
|
+
"go.mod",
|
|
1835
|
+
"composer.json",
|
|
1836
|
+
"tsconfig.json",
|
|
1837
|
+
"next.config.js",
|
|
1838
|
+
"next.config.mjs",
|
|
1839
|
+
"next.config.ts",
|
|
1840
|
+
"vite.config.js",
|
|
1841
|
+
"vite.config.ts",
|
|
1842
|
+
"app.json",
|
|
1843
|
+
"Info.plist"
|
|
1844
|
+
]);
|
|
1845
|
+
const available = [];
|
|
1846
|
+
for (const file of candidates) {
|
|
1847
|
+
try {
|
|
1848
|
+
if ((await stat2(join4(repoPath, file))).isFile()) available.push(file.replaceAll("\\", "/"));
|
|
1849
|
+
} catch {
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return [...new Set(available)].sort();
|
|
1853
|
+
}
|
|
1854
|
+
function isSdkConfigurationFile(file) {
|
|
1855
|
+
const normalized = file.replaceAll("\\", "/");
|
|
1856
|
+
return /(^|\/)(?:[^/]*config[^/]*|settings[^/]*|Info\.plist|AndroidManifest\.xml|Podfile|pubspec\.yaml)$/.test(
|
|
1857
|
+
normalized
|
|
1858
|
+
);
|
|
1771
1859
|
}
|
|
1772
1860
|
function clusterPrompt(cluster, registered, bindings) {
|
|
1773
1861
|
const byCode = new Map(registered.map((event) => [event.code, event]));
|
|
@@ -1794,41 +1882,10 @@ function clusterPrompt(cluster, registered, bindings) {
|
|
|
1794
1882
|
"Only edit the listed files (plus imports of the bindings module). Then stop."
|
|
1795
1883
|
].join("\n");
|
|
1796
1884
|
}
|
|
1797
|
-
async function
|
|
1798
|
-
const outcome = await deps.provider.invoke(
|
|
1799
|
-
{
|
|
1800
|
-
role: "review",
|
|
1801
|
-
systemPrompt: [
|
|
1802
|
-
"You review an instrumentation diff against a strict checklist. Answer with a verdict line",
|
|
1803
|
-
"`VERDICT: pass` or `VERDICT: fail`, then bullet notes for anything wrong."
|
|
1804
|
-
].join("\n"),
|
|
1805
|
-
userPrompt: [
|
|
1806
|
-
"Checklist:",
|
|
1807
|
-
REVIEW_CHECKLIST,
|
|
1808
|
-
"",
|
|
1809
|
-
`Events expected: ${cluster.events.map((event) => event.eventCode).join(", ")}`,
|
|
1810
|
-
"",
|
|
1811
|
-
"Diff:",
|
|
1812
|
-
diff.length > 6e4 ? diff.slice(0, 6e4) + "\n\u2026(truncated)" : diff
|
|
1813
|
-
].join("\n"),
|
|
1814
|
-
tools: [],
|
|
1815
|
-
allowRepoWrite: false,
|
|
1816
|
-
cwd: deps.worktree.path
|
|
1817
|
-
},
|
|
1818
|
-
deps.models.review,
|
|
1819
|
-
(action) => deps.sink.emit({ phase: "reviewing", action })
|
|
1820
|
-
);
|
|
1821
|
-
if (outcome.kind !== "completed") {
|
|
1822
|
-
return { ok: false, notes: `review pass did not complete (${outcome.kind})` };
|
|
1823
|
-
}
|
|
1824
|
-
const ok = /verdict:\s*pass/i.test(outcome.text);
|
|
1825
|
-
return { ok, notes: outcome.text.trim() };
|
|
1826
|
-
}
|
|
1827
|
-
async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
1885
|
+
async function runClusterIntegration(deps, initialClusters, _clusterDiff) {
|
|
1828
1886
|
let clusters = initialClusters;
|
|
1829
1887
|
const eventStatuses = /* @__PURE__ */ new Map();
|
|
1830
1888
|
const bindings = generateBindings(deps.target, deps.registered);
|
|
1831
|
-
const steps = resolveVerifySteps(deps.worktree.path, deps.target);
|
|
1832
1889
|
const total = clusters.length;
|
|
1833
1890
|
for (; ; ) {
|
|
1834
1891
|
const cluster = nextPendingCluster(clusters);
|
|
@@ -1844,22 +1901,46 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
|
1844
1901
|
file: cluster.files[0],
|
|
1845
1902
|
action: "placing events"
|
|
1846
1903
|
});
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
deps.
|
|
1904
|
+
const invocation = {
|
|
1905
|
+
role: "edit",
|
|
1906
|
+
task: "cluster_edit",
|
|
1907
|
+
sessionKey: cluster.id,
|
|
1908
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
1909
|
+
userPrompt: clusterPrompt(cluster, deps.registered, bindings),
|
|
1910
|
+
tools: [],
|
|
1911
|
+
repositoryAccess: { mode: "cluster", allowedFiles: cluster.files },
|
|
1912
|
+
allowRepoWrite: true,
|
|
1913
|
+
cwd: deps.worktree.path
|
|
1914
|
+
};
|
|
1915
|
+
let edit = await deps.provider.invoke(
|
|
1916
|
+
invocation,
|
|
1917
|
+
modelConfigFor(deps.models, "cluster_edit", "edit"),
|
|
1857
1918
|
(action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
|
|
1858
1919
|
);
|
|
1859
|
-
|
|
1920
|
+
let structural = edit.kind === "completed" ? await structuralClusterCheck(deps.worktree.path, cluster) : { ok: false, notes: `edit ${edit.kind}` };
|
|
1921
|
+
if ((!structural.ok || edit.kind !== "completed") && deps.provider.invokeFallback) {
|
|
1922
|
+
await deps.discardChanges();
|
|
1923
|
+
deps.sink.emit({
|
|
1924
|
+
phase: "integrating",
|
|
1925
|
+
cluster: { index, total },
|
|
1926
|
+
action: "Terra attempt rolled back; retrying the clean cluster with Sol"
|
|
1927
|
+
});
|
|
1928
|
+
edit = await deps.provider.invokeFallback(
|
|
1929
|
+
{ ...invocation, task: "repair", sessionKey: `cluster_sol:${cluster.id}` },
|
|
1930
|
+
modelConfigFor(deps.models, "repair", "edit"),
|
|
1931
|
+
(action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
|
|
1932
|
+
);
|
|
1933
|
+
structural = edit.kind === "completed" ? await structuralClusterCheck(deps.worktree.path, cluster) : { ok: false, notes: `Sol escalation ${edit.kind}` };
|
|
1934
|
+
}
|
|
1935
|
+
if (edit.kind !== "completed" || !structural.ok) {
|
|
1860
1936
|
const failure = edit.kind === "context_overflow" ? "context_overflow" : edit.kind === "budget_exhausted" ? "context_overflow" : classifyFailure(edit.kind === "failed" ? edit.error : "");
|
|
1861
1937
|
const nextStatus = nextClusterStatusAfterFailure(failure, cluster.attempts);
|
|
1862
|
-
clusters = transitionCluster(
|
|
1938
|
+
clusters = transitionCluster(
|
|
1939
|
+
clusters,
|
|
1940
|
+
cluster.id,
|
|
1941
|
+
nextStatus,
|
|
1942
|
+
structural.notes || `edit ${edit.kind}`
|
|
1943
|
+
);
|
|
1863
1944
|
deps.saveClusters(clusters);
|
|
1864
1945
|
for (const placement of cluster.events) {
|
|
1865
1946
|
eventStatuses.set(placement.eventCode, "failed");
|
|
@@ -1867,40 +1948,9 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
|
1867
1948
|
await deps.discardChanges();
|
|
1868
1949
|
continue;
|
|
1869
1950
|
}
|
|
1870
|
-
|
|
1871
|
-
if (steps.length > 0) {
|
|
1872
|
-
deps.sink.emit({ phase: "verifying", cluster: { index, total }, action: "running verifier" });
|
|
1873
|
-
const results = await runVerifySteps(deps.worktree.path, steps);
|
|
1874
|
-
statuses = verifiedStatus(results);
|
|
1875
|
-
if (results.some((result) => !result.ok) && cluster.attempts < MAX_CLUSTER_ATTEMPTS) {
|
|
1876
|
-
deps.sink.emit({ phase: "verifying", cluster: { index, total }, action: "repairing verification failure" });
|
|
1877
|
-
await deps.provider.invoke(
|
|
1878
|
-
{
|
|
1879
|
-
role: "edit",
|
|
1880
|
-
systemPrompt: deps.playbookSystemPrompt,
|
|
1881
|
-
userPrompt: [
|
|
1882
|
-
"The verification command failed after your edits. Fix ONLY the failure below, then stop.",
|
|
1883
|
-
"",
|
|
1884
|
-
results.map((result) => result.output).join("\n").slice(0, 2e4)
|
|
1885
|
-
].join("\n"),
|
|
1886
|
-
tools: [],
|
|
1887
|
-
allowRepoWrite: true,
|
|
1888
|
-
cwd: deps.worktree.path
|
|
1889
|
-
},
|
|
1890
|
-
deps.models.edit,
|
|
1891
|
-
(action) => deps.sink.emit({ phase: "verifying", cluster: { index, total }, action })
|
|
1892
|
-
);
|
|
1893
|
-
const retried = await runVerifySteps(deps.worktree.path, steps);
|
|
1894
|
-
statuses = verifiedStatus(retried);
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1951
|
+
const finalStatus = "syntax_verified";
|
|
1897
1952
|
clusters = transitionCluster(clusters, cluster.id, "verified");
|
|
1898
1953
|
deps.saveClusters(clusters);
|
|
1899
|
-
const diff = await clusterDiff(cluster.files);
|
|
1900
|
-
const review = await reviewCluster(deps, cluster, diff);
|
|
1901
|
-
const finalStatus = review.ok ? statuses === "applied" ? "applied" : "reviewed" : statuses;
|
|
1902
|
-
clusters = transitionCluster(clusters, cluster.id, "reviewed", review.ok ? void 0 : review.notes.slice(0, 500));
|
|
1903
|
-
deps.saveClusters(clusters);
|
|
1904
1954
|
for (const placement of cluster.events) {
|
|
1905
1955
|
eventStatuses.set(placement.eventCode, finalStatus);
|
|
1906
1956
|
}
|
|
@@ -1911,19 +1961,281 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
|
1911
1961
|
}
|
|
1912
1962
|
return { clusters, eventStatuses };
|
|
1913
1963
|
}
|
|
1914
|
-
async function
|
|
1915
|
-
const
|
|
1964
|
+
async function structuralClusterCheck(repoPath, cluster) {
|
|
1965
|
+
const allowed = new Set(cluster.files);
|
|
1966
|
+
const missing = [];
|
|
1967
|
+
for (const placement of cluster.events) {
|
|
1968
|
+
if (!allowed.has(placement.file)) {
|
|
1969
|
+
missing.push(`${placement.eventCode}: placement escaped cluster`);
|
|
1970
|
+
continue;
|
|
1971
|
+
}
|
|
1972
|
+
try {
|
|
1973
|
+
const root = resolve(repoPath);
|
|
1974
|
+
const path = resolve(root, placement.file);
|
|
1975
|
+
if (!path.startsWith(root + sep)) {
|
|
1976
|
+
missing.push(`${placement.eventCode}: unsafe file`);
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
const content = await readFile2(path, "utf8");
|
|
1980
|
+
const wrapper = eventWrapperName(placement.eventCode);
|
|
1981
|
+
if (!content.includes(wrapper)) {
|
|
1982
|
+
missing.push(`${placement.eventCode}: wrapper call missing from ${placement.file}`);
|
|
1983
|
+
}
|
|
1984
|
+
if (content.includes("<<<<<<<") || content.includes(">>>>>>>")) {
|
|
1985
|
+
missing.push(`${placement.eventCode}: merge conflict marker in ${placement.file}`);
|
|
1986
|
+
}
|
|
1987
|
+
if (/\.(?:ts|tsx|js|jsx|swift|dart|kt|java|go|py|php)$/.test(placement.file) && !balancedSourceDelimiters(content)) {
|
|
1988
|
+
missing.push(`${placement.eventCode}: basic syntax delimiters are unbalanced in ${placement.file}`);
|
|
1989
|
+
}
|
|
1990
|
+
if (/\.(?:ts|tsx|js|jsx)$/.test(placement.file) && content.includes("WhisperrEvents.") && !/import[\s\S]{0,300}\bWhisperrEvents\b/.test(content)) {
|
|
1991
|
+
missing.push(`${placement.eventCode}: WhisperrEvents import is missing from ${placement.file}`);
|
|
1992
|
+
}
|
|
1993
|
+
} catch {
|
|
1994
|
+
missing.push(`${placement.eventCode}: cannot read ${placement.file}`);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return {
|
|
1998
|
+
ok: missing.length === 0,
|
|
1999
|
+
notes: missing.join("; ")
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
function balancedSourceDelimiters(content) {
|
|
2003
|
+
const stack = [];
|
|
2004
|
+
const pairs = { ")": "(", "]": "[", "}": "{" };
|
|
2005
|
+
let quote = "";
|
|
2006
|
+
let escaped = false;
|
|
2007
|
+
let lineComment = false;
|
|
2008
|
+
let blockComment = false;
|
|
2009
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
2010
|
+
const char = content[index];
|
|
2011
|
+
const next = content[index + 1] ?? "";
|
|
2012
|
+
if (lineComment) {
|
|
2013
|
+
if (char === "\n") lineComment = false;
|
|
2014
|
+
continue;
|
|
2015
|
+
}
|
|
2016
|
+
if (blockComment) {
|
|
2017
|
+
if (char === "*" && next === "/") {
|
|
2018
|
+
blockComment = false;
|
|
2019
|
+
index += 1;
|
|
2020
|
+
}
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
if (quote) {
|
|
2024
|
+
if (escaped) {
|
|
2025
|
+
escaped = false;
|
|
2026
|
+
} else if (char === "\\") {
|
|
2027
|
+
escaped = true;
|
|
2028
|
+
} else if (char === quote) {
|
|
2029
|
+
quote = "";
|
|
2030
|
+
}
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
if (char === "/" && next === "/") {
|
|
2034
|
+
lineComment = true;
|
|
2035
|
+
index += 1;
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
if (char === "/" && next === "*") {
|
|
2039
|
+
blockComment = true;
|
|
2040
|
+
index += 1;
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
2044
|
+
quote = char;
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2047
|
+
if (char === "(" || char === "[" || char === "{") stack.push(char);
|
|
2048
|
+
if (pairs[char] && stack.pop() !== pairs[char]) return false;
|
|
2049
|
+
}
|
|
2050
|
+
return stack.length === 0 && !quote && !blockComment;
|
|
2051
|
+
}
|
|
2052
|
+
async function reviewIntegratedFile(deps, file, events, sessionSuffix = "") {
|
|
2053
|
+
let verdict;
|
|
2054
|
+
let issues = [];
|
|
2055
|
+
let notes = "";
|
|
2056
|
+
const expectedCodes = new Set(events.map((event) => event.code));
|
|
2057
|
+
const finishTool = {
|
|
2058
|
+
name: "finish_review",
|
|
2059
|
+
description: "Return the semantic verdict for this one file and its exact event contracts.",
|
|
2060
|
+
inputSchema: {
|
|
2061
|
+
verdict: z.enum(["pass", "fail"]),
|
|
2062
|
+
issues: z.array(z.object({
|
|
2063
|
+
eventCode: z.string(),
|
|
2064
|
+
reason: z.string()
|
|
2065
|
+
})).max(100),
|
|
2066
|
+
notes: z.string().optional()
|
|
2067
|
+
},
|
|
2068
|
+
jsonSchema: {
|
|
2069
|
+
type: "object",
|
|
2070
|
+
properties: {
|
|
2071
|
+
verdict: { type: "string", enum: ["pass", "fail"] },
|
|
2072
|
+
issues: {
|
|
2073
|
+
type: "array",
|
|
2074
|
+
maxItems: 100,
|
|
2075
|
+
items: {
|
|
2076
|
+
type: "object",
|
|
2077
|
+
properties: {
|
|
2078
|
+
eventCode: { type: "string" },
|
|
2079
|
+
reason: { type: "string" }
|
|
2080
|
+
},
|
|
2081
|
+
required: ["eventCode", "reason"]
|
|
2082
|
+
}
|
|
2083
|
+
},
|
|
2084
|
+
notes: { type: "string" }
|
|
2085
|
+
},
|
|
2086
|
+
required: ["verdict", "issues"]
|
|
2087
|
+
},
|
|
2088
|
+
handler: async (input) => {
|
|
2089
|
+
const rawIssues = Array.isArray(input.issues) ? input.issues : [];
|
|
2090
|
+
const normalized = [];
|
|
2091
|
+
for (const candidate of rawIssues) {
|
|
2092
|
+
const issue = candidate;
|
|
2093
|
+
const eventCode = String(issue.eventCode ?? "").trim();
|
|
2094
|
+
const reason = String(issue.reason ?? "").trim();
|
|
2095
|
+
if (!expectedCodes.has(eventCode) || !reason) {
|
|
2096
|
+
return { ok: false, reason: "issues must name an expected event and a concrete reason" };
|
|
2097
|
+
}
|
|
2098
|
+
normalized.push({ eventCode, reason });
|
|
2099
|
+
}
|
|
2100
|
+
verdict = input.verdict === "pass" ? "pass" : "fail";
|
|
2101
|
+
issues = normalized;
|
|
2102
|
+
notes = String(input.notes ?? "").trim();
|
|
2103
|
+
if (verdict === "pass" && issues.length > 0) {
|
|
2104
|
+
return { ok: false, reason: "a passing verdict cannot include issues" };
|
|
2105
|
+
}
|
|
2106
|
+
return { ok: true };
|
|
2107
|
+
}
|
|
2108
|
+
};
|
|
2109
|
+
const content = await readFile2(resolve(deps.worktree.path, file), "utf8");
|
|
2110
|
+
const contracts = events.map((event) => ({
|
|
2111
|
+
code: event.code,
|
|
2112
|
+
reasoning: event.reasoning,
|
|
2113
|
+
anchorFile: event.anchorFile,
|
|
2114
|
+
anchorSymbol: event.anchorSymbol,
|
|
2115
|
+
payloadSchema: event.payloadSchema
|
|
2116
|
+
}));
|
|
2117
|
+
const surveyAnchors = deps.survey?.integratableCallSites.filter((anchor) => anchor.file === file) ?? [];
|
|
2118
|
+
const key = createHash2("sha256").update(file).digest("hex").slice(0, 12);
|
|
2119
|
+
const outcome = await deps.provider.invoke(
|
|
2120
|
+
{
|
|
2121
|
+
role: "review",
|
|
2122
|
+
task: "review",
|
|
2123
|
+
sessionKey: `review:${key}${sessionSuffix}`,
|
|
2124
|
+
systemPrompt: [
|
|
2125
|
+
"You are the file-isolated semantic review pass for product analytics instrumentation.",
|
|
2126
|
+
"Use finish_review exactly once. Do not browse the repository and do not edit files."
|
|
2127
|
+
].join("\n"),
|
|
2128
|
+
userPrompt: [
|
|
2129
|
+
"Validate every expected wrapper call for:",
|
|
2130
|
+
"- placement after the confirmed outcome rather than intent",
|
|
2131
|
+
"- deduplication, especially view/render/navigation events",
|
|
2132
|
+
"- payload correctness against the exact contract",
|
|
2133
|
+
"- absence of personal identifiers",
|
|
2134
|
+
"- generated wrapper usage rather than raw track calls",
|
|
2135
|
+
"",
|
|
2136
|
+
`Event contracts:
|
|
2137
|
+
${JSON.stringify(contracts)}`,
|
|
2138
|
+
`Saved survey anchors:
|
|
2139
|
+
${JSON.stringify(surveyAnchors)}`,
|
|
2140
|
+
"",
|
|
2141
|
+
`Expanded context for ${file}:
|
|
2142
|
+
${content.slice(0, 6e4)}`
|
|
2143
|
+
].join("\n"),
|
|
2144
|
+
tools: [finishTool],
|
|
2145
|
+
repositoryAccess: { mode: "review" },
|
|
2146
|
+
allowRepoWrite: false,
|
|
2147
|
+
cwd: deps.worktree.path
|
|
2148
|
+
},
|
|
2149
|
+
modelConfigFor(deps.models, "review", "review"),
|
|
2150
|
+
(action) => deps.sink.emit({ phase: "reviewing", file, action })
|
|
2151
|
+
);
|
|
2152
|
+
if (outcome.kind !== "completed" || verdict === void 0) {
|
|
2153
|
+
return {
|
|
2154
|
+
ok: false,
|
|
2155
|
+
issues: events.map((event) => ({
|
|
2156
|
+
eventCode: event.code,
|
|
2157
|
+
reason: `semantic review did not finish (${outcome.kind})`
|
|
2158
|
+
})),
|
|
2159
|
+
notes: `semantic review did not finish (${outcome.kind})`
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
return { ok: verdict === "pass" && issues.length === 0, issues, notes };
|
|
2163
|
+
}
|
|
2164
|
+
async function repairIntegratedFile(deps, file, events, review) {
|
|
2165
|
+
const key = createHash2("sha256").update(file).digest("hex").slice(0, 12);
|
|
2166
|
+
deps.sink.emit({ phase: "reviewing", file, action: "Sol is repairing semantic review failures" });
|
|
2167
|
+
const outcome = await deps.provider.invoke(
|
|
2168
|
+
{
|
|
2169
|
+
role: "edit",
|
|
2170
|
+
task: "repair",
|
|
2171
|
+
sessionKey: `semantic_repair:${key}`,
|
|
2172
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
2173
|
+
userPrompt: [
|
|
2174
|
+
`Repair only ${file}.`,
|
|
2175
|
+
"Preserve the exact event set and generated wrappers. Do not add events or edit other files.",
|
|
2176
|
+
"Correct placement timing, deduplication, payloads, PII safety, and wrapper usage as reported.",
|
|
2177
|
+
`Issues:
|
|
2178
|
+
${JSON.stringify(review.issues)}`,
|
|
2179
|
+
`Contracts:
|
|
2180
|
+
${JSON.stringify(events.map((event) => ({
|
|
2181
|
+
code: event.code,
|
|
2182
|
+
anchorSymbol: event.anchorSymbol,
|
|
2183
|
+
payloadSchema: event.payloadSchema,
|
|
2184
|
+
reasoning: event.reasoning
|
|
2185
|
+
})))}`
|
|
2186
|
+
].join("\n"),
|
|
2187
|
+
tools: [],
|
|
2188
|
+
repositoryAccess: { mode: "repair", allowedFiles: [file] },
|
|
2189
|
+
allowRepoWrite: true,
|
|
2190
|
+
cwd: deps.worktree.path
|
|
2191
|
+
},
|
|
2192
|
+
modelConfigFor(deps.models, "repair", "edit"),
|
|
2193
|
+
(action) => deps.sink.emit({ phase: "reviewing", file, action })
|
|
2194
|
+
);
|
|
2195
|
+
return outcome.kind === "completed";
|
|
2196
|
+
}
|
|
2197
|
+
async function finalizeIntegration(deps, verifyCwd = deps.worktree.path) {
|
|
2198
|
+
const steps = resolveVerifySteps(verifyCwd, deps.target);
|
|
1916
2199
|
deps.sink.emit({ phase: "verifying", action: "final verification pass" });
|
|
1917
|
-
const results = await runVerifySteps(
|
|
2200
|
+
const results = await runVerifySteps(verifyCwd, steps);
|
|
1918
2201
|
return {
|
|
1919
2202
|
results,
|
|
1920
2203
|
status: runVerificationStatus(results),
|
|
1921
2204
|
command: verificationCommand(results)
|
|
1922
2205
|
};
|
|
1923
2206
|
}
|
|
2207
|
+
async function repairVerification(deps, results, cycle = 1) {
|
|
2208
|
+
const failed = results.filter((result) => !result.ok);
|
|
2209
|
+
if (failed.length === 0) return true;
|
|
2210
|
+
deps.sink.emit({ phase: "verifying", action: "Sol is repairing final verification failures" });
|
|
2211
|
+
const allowedFiles = deps.allowedRepairFiles ?? deps.registered.map((event) => event.anchorFile);
|
|
2212
|
+
const outcome = await deps.provider.invoke(
|
|
2213
|
+
{
|
|
2214
|
+
role: "edit",
|
|
2215
|
+
task: "repair",
|
|
2216
|
+
sessionKey: `final_repair:${cycle}`,
|
|
2217
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
2218
|
+
userPrompt: [
|
|
2219
|
+
"The one final repository verification failed after all instrumentation patches were merged.",
|
|
2220
|
+
"Repair only the affected integration files and their imports/configuration. Preserve every generated event wrapper call.",
|
|
2221
|
+
"Do not redesign events. Stop after the repair; the wizard will rerun verification.",
|
|
2222
|
+
"",
|
|
2223
|
+
failed.map((result) => `$ ${result.command} ${result.args.join(" ")}
|
|
2224
|
+
${result.output}`).join("\n\n").slice(0, 3e4)
|
|
2225
|
+
].join("\n"),
|
|
2226
|
+
tools: [],
|
|
2227
|
+
repositoryAccess: { mode: "repair", allowedFiles },
|
|
2228
|
+
allowRepoWrite: true,
|
|
2229
|
+
cwd: deps.worktree.path
|
|
2230
|
+
},
|
|
2231
|
+
modelConfigFor(deps.models, "repair", "edit"),
|
|
2232
|
+
(action) => deps.sink.emit({ phase: "verifying", action })
|
|
2233
|
+
);
|
|
2234
|
+
return outcome.kind === "completed";
|
|
2235
|
+
}
|
|
1924
2236
|
|
|
1925
2237
|
// src/engine/runs.ts
|
|
1926
|
-
import { createHash as
|
|
2238
|
+
import { createHash as createHash3 } from "crypto";
|
|
1927
2239
|
var RunsApiError = class extends Error {
|
|
1928
2240
|
constructor(message, status, code) {
|
|
1929
2241
|
super(message);
|
|
@@ -1935,7 +2247,7 @@ var RunsApiError = class extends Error {
|
|
|
1935
2247
|
code;
|
|
1936
2248
|
};
|
|
1937
2249
|
function itemIdempotencyKey(kind, code, extra = "") {
|
|
1938
|
-
return
|
|
2250
|
+
return createHash3("sha256").update(`${kind}
|
|
1939
2251
|
${code}
|
|
1940
2252
|
${extra}`).digest("hex").slice(0, 32);
|
|
1941
2253
|
}
|
|
@@ -1997,6 +2309,20 @@ var RunsClient = class {
|
|
|
1997
2309
|
payload
|
|
1998
2310
|
}).then(normalizeItemWriteResult);
|
|
1999
2311
|
}
|
|
2312
|
+
persistSurveyArtifact(runId, content, metadata) {
|
|
2313
|
+
return this.request(
|
|
2314
|
+
"POST",
|
|
2315
|
+
`/wizard/runs/${runId}/artifacts/survey`,
|
|
2316
|
+
artifactRequest(content, metadata)
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
commitDesign(runId, content, events, metadata) {
|
|
2320
|
+
return this.request(
|
|
2321
|
+
"POST",
|
|
2322
|
+
`/wizard/runs/${runId}/design/commit`,
|
|
2323
|
+
{ ...artifactRequest(content, metadata), events }
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2000
2326
|
completeRun(runId, completion) {
|
|
2001
2327
|
return this.request(
|
|
2002
2328
|
"POST",
|
|
@@ -2056,9 +2382,59 @@ function normalizeRunBootstrap(payload) {
|
|
|
2056
2382
|
setupStatus: stringValue(snapshotSource.setupStatus),
|
|
2057
2383
|
events
|
|
2058
2384
|
},
|
|
2059
|
-
ingestion: { apiKey: ingestion.apiKey, baseUrl: ingestion.baseUrl }
|
|
2385
|
+
ingestion: { apiKey: ingestion.apiKey, baseUrl: ingestion.baseUrl },
|
|
2386
|
+
artifacts: normalizeArtifacts(value.artifacts ?? snapshotSource.artifacts)
|
|
2060
2387
|
};
|
|
2061
2388
|
}
|
|
2389
|
+
function normalizeArtifacts(payload) {
|
|
2390
|
+
if (!Array.isArray(payload)) return [];
|
|
2391
|
+
return payload.flatMap((candidate) => {
|
|
2392
|
+
const value = record(candidate);
|
|
2393
|
+
const kind = value?.kind;
|
|
2394
|
+
if (kind !== "survey" && kind !== "event_design" || typeof value?.contentHash !== "string" || !/^[0-9a-f]{64}$/.test(value.contentHash) || typeof value.model !== "string" || typeof value.createdAt !== "string") {
|
|
2395
|
+
return [];
|
|
2396
|
+
}
|
|
2397
|
+
return [{
|
|
2398
|
+
kind,
|
|
2399
|
+
content: value.content,
|
|
2400
|
+
contentHash: value.contentHash,
|
|
2401
|
+
model: value.model,
|
|
2402
|
+
...typeof value.effort === "string" ? { effort: value.effort } : {},
|
|
2403
|
+
...typeof value.serviceTier === "string" ? { serviceTier: value.serviceTier } : {},
|
|
2404
|
+
fastMode: value.fastMode === true,
|
|
2405
|
+
...typeof value.frozenAt === "string" ? { frozenAt: value.frozenAt } : {},
|
|
2406
|
+
createdAt: value.createdAt
|
|
2407
|
+
}];
|
|
2408
|
+
});
|
|
2409
|
+
}
|
|
2410
|
+
function artifactRequest(content, metadata) {
|
|
2411
|
+
return {
|
|
2412
|
+
content,
|
|
2413
|
+
contentHash: artifactContentHash(content),
|
|
2414
|
+
model: metadata.model,
|
|
2415
|
+
...metadata.effort ? { effort: metadata.effort } : {},
|
|
2416
|
+
...metadata.serviceTier ? { serviceTier: metadata.serviceTier } : {},
|
|
2417
|
+
fastMode: metadata.fastMode === true
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
function artifactContentHash(content) {
|
|
2421
|
+
return createHash3("sha256").update(stableJSONStringify(content)).digest("hex");
|
|
2422
|
+
}
|
|
2423
|
+
function stableJSONStringify(value) {
|
|
2424
|
+
if (Array.isArray(value)) {
|
|
2425
|
+
return `[${value.map(stableJSONStringify).join(",")}]`;
|
|
2426
|
+
}
|
|
2427
|
+
if (value && typeof value === "object") {
|
|
2428
|
+
const entries = Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right));
|
|
2429
|
+
return `{${entries.map(
|
|
2430
|
+
([key, item]) => `${canonicalJSONString(key)}:${stableJSONStringify(item)}`
|
|
2431
|
+
).join(",")}}`;
|
|
2432
|
+
}
|
|
2433
|
+
return canonicalJSONString(value);
|
|
2434
|
+
}
|
|
2435
|
+
function canonicalJSONString(value) {
|
|
2436
|
+
return JSON.stringify(value).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
2437
|
+
}
|
|
2062
2438
|
function normalizeIntegrationProgress(payload) {
|
|
2063
2439
|
const value = record(payload);
|
|
2064
2440
|
if (!value || !Array.isArray(value.changedFiles) || !Array.isArray(value.events)) {
|
|
@@ -2096,7 +2472,9 @@ function isSnapshotEvent(value) {
|
|
|
2096
2472
|
}
|
|
2097
2473
|
|
|
2098
2474
|
// src/engine/selection.ts
|
|
2099
|
-
import {
|
|
2475
|
+
import { readFile as readFile3, stat as stat3 } from "fs/promises";
|
|
2476
|
+
import { resolve as resolve2, sep as sep2 } from "path";
|
|
2477
|
+
import { z as z2 } from "zod";
|
|
2100
2478
|
|
|
2101
2479
|
// src/engine/prompts.ts
|
|
2102
2480
|
var SURVEY_SYSTEM_PROMPT = [
|
|
@@ -2104,8 +2482,8 @@ var SURVEY_SYSTEM_PROMPT = [
|
|
|
2104
2482
|
"Map the repository so a later pass can pick the product's important lifecycle events.",
|
|
2105
2483
|
"You cannot edit files. Be fast and decisive; read entry points and feature roots, not every file.",
|
|
2106
2484
|
"",
|
|
2107
|
-
"
|
|
2108
|
-
"1. Stack: frameworks, package manager, app entry point
|
|
2485
|
+
"Finish by calling finish_survey exactly once. Its structured fields must contain:",
|
|
2486
|
+
"1. Stack: frameworks, package manager, and app entry point files.",
|
|
2109
2487
|
"2. Feature areas: name, root directory, and every relevant file containing",
|
|
2110
2488
|
" committed end-user actions or meaningful lifecycle transitions.",
|
|
2111
2489
|
"3. The end-user identify() anchor: the login/signup success and session-restore call sites for the",
|
|
@@ -2114,7 +2492,8 @@ var SURVEY_SYSTEM_PROMPT = [
|
|
|
2114
2492
|
"5. Exhaustive inventory of integratable call sites: file, function/symbol,",
|
|
2115
2493
|
" confirmed outcome or state transition, available non-PII payload fields,",
|
|
2116
2494
|
" and whether similar paths exist elsewhere.",
|
|
2117
|
-
"Cite concrete
|
|
2495
|
+
"Cite concrete repository-relative paths for every claim. Do not propose event names yet.",
|
|
2496
|
+
"Never copy raw source code into the result."
|
|
2118
2497
|
].join("\n");
|
|
2119
2498
|
function surveyUserPrompt(repoPath, onboardingContext) {
|
|
2120
2499
|
return [
|
|
@@ -2122,13 +2501,13 @@ function surveyUserPrompt(repoPath, onboardingContext) {
|
|
|
2122
2501
|
"",
|
|
2123
2502
|
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
2124
2503
|
"",
|
|
2125
|
-
"
|
|
2504
|
+
"Complete the structured survey now."
|
|
2126
2505
|
].join("\n");
|
|
2127
2506
|
}
|
|
2128
2507
|
var SELECTION_SYSTEM_PROMPT = [
|
|
2129
2508
|
"You are the event-selection pass of the Whisperr integration wizard.",
|
|
2130
2509
|
"From the survey report and business context, choose the IMPORTANT product events this codebase",
|
|
2131
|
-
"can actually emit, and propose
|
|
2510
|
+
"can actually emit, and propose them with propose_event_batch calls.",
|
|
2132
2511
|
"",
|
|
2133
2512
|
"Rules \u2014 these are hard constraints:",
|
|
2134
2513
|
"- Only propose an event with a concrete call site: a real file and function where the committed",
|
|
@@ -2156,11 +2535,11 @@ var SELECTION_SYSTEM_PROMPT = [
|
|
|
2156
2535
|
" government ids, usernames, passwords, device ids, or IP addresses. Domain metrics are welcome.",
|
|
2157
2536
|
"- The end user is the paying customer/consumer. Never instrument admin, staff, or operator actions.",
|
|
2158
2537
|
"",
|
|
2159
|
-
"
|
|
2538
|
+
"Use only propose_event_batch for proposals. When done, call finish_event_design with a one-line summary."
|
|
2160
2539
|
].join("\n");
|
|
2161
2540
|
function selectionUserPrompt(surveyReport, onboardingContext, existingEventCodes, rejectedCodes) {
|
|
2162
2541
|
return [
|
|
2163
|
-
"
|
|
2542
|
+
"Saved structured survey artifact:",
|
|
2164
2543
|
surveyReport,
|
|
2165
2544
|
"",
|
|
2166
2545
|
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
@@ -2271,38 +2650,108 @@ function validateProposedEvent(input, seen, rejected) {
|
|
|
2271
2650
|
};
|
|
2272
2651
|
}
|
|
2273
2652
|
async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSessionId) {
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2653
|
+
let survey;
|
|
2654
|
+
let finished = false;
|
|
2655
|
+
const finishSurvey = {
|
|
2656
|
+
name: "finish_survey",
|
|
2657
|
+
description: "Return the complete structured repository survey without raw source code.",
|
|
2658
|
+
inputSchema: {
|
|
2659
|
+
stack: z2.object({
|
|
2660
|
+
frameworks: z2.array(z2.string()).max(30),
|
|
2661
|
+
packageManager: z2.string().optional(),
|
|
2662
|
+
entryPoints: z2.array(z2.string()).max(50)
|
|
2663
|
+
}),
|
|
2664
|
+
featureAreas: z2.array(z2.object({
|
|
2665
|
+
name: z2.string(),
|
|
2666
|
+
root: z2.string(),
|
|
2667
|
+
files: z2.array(z2.string()).max(200)
|
|
2668
|
+
})).max(200),
|
|
2669
|
+
identifyAnchors: z2.array(z2.object({
|
|
2670
|
+
file: z2.string(),
|
|
2671
|
+
symbol: z2.string().optional(),
|
|
2672
|
+
description: z2.string()
|
|
2673
|
+
})).max(50),
|
|
2674
|
+
logoutAnchors: z2.array(z2.object({
|
|
2675
|
+
file: z2.string(),
|
|
2676
|
+
symbol: z2.string().optional(),
|
|
2677
|
+
description: z2.string()
|
|
2678
|
+
})).max(50),
|
|
2679
|
+
existingAnalytics: z2.array(z2.object({
|
|
2680
|
+
file: z2.string(),
|
|
2681
|
+
symbol: z2.string().optional(),
|
|
2682
|
+
description: z2.string()
|
|
2683
|
+
})).max(100),
|
|
2684
|
+
integratableCallSites: z2.array(z2.object({
|
|
2685
|
+
file: z2.string(),
|
|
2686
|
+
symbol: z2.string().optional(),
|
|
2687
|
+
description: z2.string(),
|
|
2688
|
+
outcome: z2.string(),
|
|
2689
|
+
payloadFields: z2.array(z2.string()).max(50),
|
|
2690
|
+
similarPaths: z2.array(z2.string()).max(50)
|
|
2691
|
+
})).max(500)
|
|
2284
2692
|
},
|
|
2285
|
-
|
|
2693
|
+
jsonSchema: surveyJSONSchema(),
|
|
2694
|
+
handler: async (input) => {
|
|
2695
|
+
if (finished) {
|
|
2696
|
+
return { ok: false, reason: "survey is already frozen" };
|
|
2697
|
+
}
|
|
2698
|
+
const normalized = normalizeSurveyArtifact(input);
|
|
2699
|
+
if (!normalized) {
|
|
2700
|
+
return { ok: false, reason: "survey fields or repository-relative paths are invalid" };
|
|
2701
|
+
}
|
|
2702
|
+
survey = normalized;
|
|
2703
|
+
finished = true;
|
|
2704
|
+
return { ok: true, callSites: normalized.integratableCallSites.length };
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
sink.emit({ phase: "surveying", action: "mapping the repository" });
|
|
2708
|
+
const invocation = {
|
|
2709
|
+
role: "survey",
|
|
2710
|
+
task: "survey",
|
|
2711
|
+
sessionKey: "survey",
|
|
2712
|
+
systemPrompt: SURVEY_SYSTEM_PROMPT,
|
|
2713
|
+
userPrompt: surveyUserPrompt(repoPath, renderOnboardingContext(bootstrap.onboardingContext)),
|
|
2714
|
+
tools: [finishSurvey],
|
|
2715
|
+
repositoryAccess: { mode: "survey" },
|
|
2716
|
+
allowRepoWrite: false,
|
|
2717
|
+
cwd: repoPath,
|
|
2718
|
+
resumeSessionId
|
|
2719
|
+
};
|
|
2720
|
+
let outcome = await provider.invoke(
|
|
2721
|
+
invocation,
|
|
2722
|
+
modelConfigFor(models, "survey", "survey"),
|
|
2286
2723
|
(action) => sink.emit({ phase: "surveying", action })
|
|
2287
2724
|
);
|
|
2288
|
-
|
|
2725
|
+
if (outcome.kind === "completed" && !finished && provider.invokeFallback) {
|
|
2726
|
+
survey = void 0;
|
|
2727
|
+
finished = false;
|
|
2728
|
+
sink.emit({ phase: "surveying", action: "Opus survey was invalid; retrying once with Sol" });
|
|
2729
|
+
outcome = await provider.invokeFallback(
|
|
2730
|
+
{ ...invocation, sessionKey: "survey:sol_retry" },
|
|
2731
|
+
modelConfigFor(models, "repair", "survey"),
|
|
2732
|
+
(action) => sink.emit({ phase: "surveying", action })
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
return { survey, finished, outcome };
|
|
2289
2736
|
}
|
|
2290
|
-
async function runSelection(provider, models, repoPath, bootstrap,
|
|
2737
|
+
async function runSelection(provider, models, repoPath, bootstrap, survey, rejectedCodes, sink, existingEventCodes = bootstrap.snapshot.events.map((event) => event.code), recoveryCodes = []) {
|
|
2291
2738
|
const proposed = [];
|
|
2292
2739
|
const seen = /* @__PURE__ */ new Set();
|
|
2293
2740
|
const rejected = new Set(rejectedCodes);
|
|
2741
|
+
const existing = new Set(existingEventCodes.map(normalizeEventCode));
|
|
2742
|
+
const recovery = new Set(recoveryCodes.map(normalizeEventCode));
|
|
2294
2743
|
let summary = "";
|
|
2295
2744
|
let finished = false;
|
|
2296
2745
|
const proposeTool = {
|
|
2297
2746
|
name: "propose_event",
|
|
2298
2747
|
description: "Propose one important, concretely-anchored product event. Returns ok:true when accepted.",
|
|
2299
2748
|
inputSchema: {
|
|
2300
|
-
code:
|
|
2301
|
-
name:
|
|
2302
|
-
reasoning:
|
|
2303
|
-
anchorFile:
|
|
2304
|
-
anchorSymbol:
|
|
2305
|
-
payloadSchema:
|
|
2749
|
+
code: z2.string().describe("lowercase snake_case event code"),
|
|
2750
|
+
name: z2.string().describe("short human-readable name"),
|
|
2751
|
+
reasoning: z2.string().describe("why this event matters for this business"),
|
|
2752
|
+
anchorFile: z2.string().describe("repository file where the committed action happens"),
|
|
2753
|
+
anchorSymbol: z2.string().optional().describe("function/symbol at the anchor"),
|
|
2754
|
+
payloadSchema: z2.record(z2.string(), z2.string()).optional().describe("important payload fields: name -> short description; no personal identifiers")
|
|
2306
2755
|
},
|
|
2307
2756
|
jsonSchema: {
|
|
2308
2757
|
type: "object",
|
|
@@ -2321,6 +2770,9 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2321
2770
|
required: ["code", "name", "reasoning", "anchorFile"]
|
|
2322
2771
|
},
|
|
2323
2772
|
handler: async (input) => {
|
|
2773
|
+
if (finished) {
|
|
2774
|
+
return { ok: false, reason: "event design is already frozen" };
|
|
2775
|
+
}
|
|
2324
2776
|
const candidate = {
|
|
2325
2777
|
code: String(input.code ?? ""),
|
|
2326
2778
|
name: String(input.name ?? ""),
|
|
@@ -2333,6 +2785,16 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2333
2785
|
if (!validated.ok) {
|
|
2334
2786
|
return { ok: false, reason: validated.reason };
|
|
2335
2787
|
}
|
|
2788
|
+
if (recovery.size > 0 && !recovery.has(validated.event.code)) {
|
|
2789
|
+
return { ok: false, reason: `placement recovery cannot create event ${validated.event.code}` };
|
|
2790
|
+
}
|
|
2791
|
+
if (existing.has(validated.event.code) && !recovery.has(validated.event.code)) {
|
|
2792
|
+
return { ok: false, reason: `event ${validated.event.code} is already registered` };
|
|
2793
|
+
}
|
|
2794
|
+
const anchorError = await validateAnchor(repoPath, validated.event);
|
|
2795
|
+
if (anchorError) {
|
|
2796
|
+
return { ok: false, reason: anchorError };
|
|
2797
|
+
}
|
|
2336
2798
|
seen.add(validated.event.code);
|
|
2337
2799
|
proposed.push(validated.event);
|
|
2338
2800
|
sink.emit({
|
|
@@ -2343,10 +2805,41 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2343
2805
|
return { ok: true, code: validated.event.code };
|
|
2344
2806
|
}
|
|
2345
2807
|
};
|
|
2808
|
+
const proposeBatchTool = {
|
|
2809
|
+
name: "propose_event_batch",
|
|
2810
|
+
description: "Propose a batch of important, concretely-anchored product events in one tool call.",
|
|
2811
|
+
inputSchema: {
|
|
2812
|
+
events: z2.array(z2.object(proposeTool.inputSchema)).min(1).max(200)
|
|
2813
|
+
},
|
|
2814
|
+
jsonSchema: {
|
|
2815
|
+
type: "object",
|
|
2816
|
+
properties: {
|
|
2817
|
+
events: {
|
|
2818
|
+
type: "array",
|
|
2819
|
+
minItems: 1,
|
|
2820
|
+
maxItems: 200,
|
|
2821
|
+
items: proposeTool.jsonSchema
|
|
2822
|
+
}
|
|
2823
|
+
},
|
|
2824
|
+
required: ["events"]
|
|
2825
|
+
},
|
|
2826
|
+
handler: async (input) => {
|
|
2827
|
+
const events = Array.isArray(input.events) ? input.events : [];
|
|
2828
|
+
const results = [];
|
|
2829
|
+
for (const event of events) {
|
|
2830
|
+
results.push(await proposeTool.handler(event ?? {}));
|
|
2831
|
+
}
|
|
2832
|
+
return {
|
|
2833
|
+
ok: results.every((result) => result.ok === true),
|
|
2834
|
+
accepted: results.filter((result) => result.ok === true).length,
|
|
2835
|
+
results
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
};
|
|
2346
2839
|
const finishTool = {
|
|
2347
|
-
name: "
|
|
2348
|
-
description: "
|
|
2349
|
-
inputSchema: { summary:
|
|
2840
|
+
name: "finish_event_design",
|
|
2841
|
+
description: "Freeze the complete batch after every important event has been proposed.",
|
|
2842
|
+
inputSchema: { summary: z2.string().describe("one line on what was selected and why") },
|
|
2350
2843
|
jsonSchema: {
|
|
2351
2844
|
type: "object",
|
|
2352
2845
|
properties: { summary: { type: "string", description: "one line on what was selected and why" } },
|
|
@@ -2359,60 +2852,204 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2359
2852
|
}
|
|
2360
2853
|
};
|
|
2361
2854
|
sink.emit({ phase: "designing", action: "selecting important events" });
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2855
|
+
const invocation = {
|
|
2856
|
+
role: "design",
|
|
2857
|
+
task: "design",
|
|
2858
|
+
sessionKey: "design",
|
|
2859
|
+
systemPrompt: SELECTION_SYSTEM_PROMPT,
|
|
2860
|
+
userPrompt: selectionUserPrompt(
|
|
2861
|
+
JSON.stringify(survey),
|
|
2862
|
+
renderOnboardingContext(bootstrap.onboardingContext),
|
|
2863
|
+
existingEventCodes,
|
|
2864
|
+
rejectedCodes
|
|
2865
|
+
),
|
|
2866
|
+
tools: [proposeBatchTool, finishTool],
|
|
2867
|
+
repositoryAccess: { mode: "none" },
|
|
2868
|
+
allowRepoWrite: false,
|
|
2869
|
+
cwd: repoPath
|
|
2870
|
+
};
|
|
2871
|
+
let outcome = await provider.invoke(
|
|
2872
|
+
invocation,
|
|
2873
|
+
modelConfigFor(models, "design", "design"),
|
|
2377
2874
|
(action) => sink.emit({ phase: "designing", action })
|
|
2378
2875
|
);
|
|
2876
|
+
if (outcome.kind === "completed" && (!finished || proposed.length === 0) && provider.invokeFallback) {
|
|
2877
|
+
proposed.length = 0;
|
|
2878
|
+
seen.clear();
|
|
2879
|
+
summary = "";
|
|
2880
|
+
finished = false;
|
|
2881
|
+
sink.emit({ phase: "designing", action: "Opus design was invalid; retrying once with Sol" });
|
|
2882
|
+
outcome = await provider.invokeFallback(
|
|
2883
|
+
{ ...invocation, sessionKey: "design:sol_retry" },
|
|
2884
|
+
modelConfigFor(models, "repair", "design"),
|
|
2885
|
+
(action) => sink.emit({ phase: "designing", action })
|
|
2886
|
+
);
|
|
2887
|
+
}
|
|
2379
2888
|
return {
|
|
2380
2889
|
outcome: outcome.kind,
|
|
2381
2890
|
finished,
|
|
2382
2891
|
proposed,
|
|
2383
2892
|
summary,
|
|
2384
|
-
|
|
2893
|
+
survey,
|
|
2385
2894
|
sessionId: outcome.sessionId
|
|
2386
2895
|
};
|
|
2387
2896
|
}
|
|
2388
|
-
async function
|
|
2389
|
-
const
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
} catch (error) {
|
|
2402
|
-
if (error instanceof RunsApiError && error.code === "wizard_project_conflict") {
|
|
2403
|
-
sink.emit({
|
|
2404
|
-
phase: "persisting",
|
|
2405
|
-
file: event.anchorFile,
|
|
2406
|
-
action: `skipped ${event.code} \u2014 already registered by another project`
|
|
2407
|
-
});
|
|
2408
|
-
continue;
|
|
2897
|
+
async function validateAnchor(repoPath, event) {
|
|
2898
|
+
const root = resolve2(repoPath);
|
|
2899
|
+
const path = resolve2(root, event.anchorFile);
|
|
2900
|
+
if (path === root || !path.startsWith(root + sep2)) {
|
|
2901
|
+
return `${event.code}: anchor escapes the repository`;
|
|
2902
|
+
}
|
|
2903
|
+
try {
|
|
2904
|
+
const info = await stat3(path);
|
|
2905
|
+
if (!info.isFile()) return `${event.code}: anchor is not a file`;
|
|
2906
|
+
if (event.anchorSymbol) {
|
|
2907
|
+
const content = await readFile3(path, "utf8");
|
|
2908
|
+
if (!content.includes(event.anchorSymbol)) {
|
|
2909
|
+
return `${event.code}: anchor symbol ${event.anchorSymbol} is not present in ${event.anchorFile}`;
|
|
2409
2910
|
}
|
|
2410
|
-
throw error;
|
|
2411
2911
|
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2912
|
+
} catch {
|
|
2913
|
+
return `${event.code}: anchor file ${event.anchorFile} does not exist`;
|
|
2914
|
+
}
|
|
2915
|
+
return void 0;
|
|
2916
|
+
}
|
|
2917
|
+
function surveyJSONSchema() {
|
|
2918
|
+
const anchor = {
|
|
2919
|
+
type: "object",
|
|
2920
|
+
properties: {
|
|
2921
|
+
file: { type: "string" },
|
|
2922
|
+
symbol: { type: "string" },
|
|
2923
|
+
description: { type: "string" }
|
|
2924
|
+
},
|
|
2925
|
+
required: ["file", "description"]
|
|
2926
|
+
};
|
|
2927
|
+
return {
|
|
2928
|
+
type: "object",
|
|
2929
|
+
properties: {
|
|
2930
|
+
stack: {
|
|
2931
|
+
type: "object",
|
|
2932
|
+
properties: {
|
|
2933
|
+
frameworks: { type: "array", items: { type: "string" }, maxItems: 30 },
|
|
2934
|
+
packageManager: { type: "string" },
|
|
2935
|
+
entryPoints: { type: "array", items: { type: "string" }, maxItems: 50 }
|
|
2936
|
+
},
|
|
2937
|
+
required: ["frameworks", "entryPoints"]
|
|
2938
|
+
},
|
|
2939
|
+
featureAreas: {
|
|
2940
|
+
type: "array",
|
|
2941
|
+
maxItems: 200,
|
|
2942
|
+
items: {
|
|
2943
|
+
type: "object",
|
|
2944
|
+
properties: {
|
|
2945
|
+
name: { type: "string" },
|
|
2946
|
+
root: { type: "string" },
|
|
2947
|
+
files: { type: "array", items: { type: "string" }, maxItems: 200 }
|
|
2948
|
+
},
|
|
2949
|
+
required: ["name", "root", "files"]
|
|
2950
|
+
}
|
|
2951
|
+
},
|
|
2952
|
+
identifyAnchors: { type: "array", items: anchor, maxItems: 50 },
|
|
2953
|
+
logoutAnchors: { type: "array", items: anchor, maxItems: 50 },
|
|
2954
|
+
existingAnalytics: { type: "array", items: anchor, maxItems: 100 },
|
|
2955
|
+
integratableCallSites: {
|
|
2956
|
+
type: "array",
|
|
2957
|
+
maxItems: 500,
|
|
2958
|
+
items: {
|
|
2959
|
+
...anchor,
|
|
2960
|
+
properties: {
|
|
2961
|
+
...anchor.properties,
|
|
2962
|
+
outcome: { type: "string" },
|
|
2963
|
+
payloadFields: { type: "array", items: { type: "string" }, maxItems: 50 },
|
|
2964
|
+
similarPaths: { type: "array", items: { type: "string" }, maxItems: 50 }
|
|
2965
|
+
},
|
|
2966
|
+
required: ["file", "description", "outcome", "payloadFields", "similarPaths"]
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
},
|
|
2970
|
+
required: [
|
|
2971
|
+
"stack",
|
|
2972
|
+
"featureAreas",
|
|
2973
|
+
"identifyAnchors",
|
|
2974
|
+
"logoutAnchors",
|
|
2975
|
+
"existingAnalytics",
|
|
2976
|
+
"integratableCallSites"
|
|
2977
|
+
]
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
function normalizeSurveyArtifact(input) {
|
|
2981
|
+
const safePath = (value2) => {
|
|
2982
|
+
if (typeof value2 !== "string") return void 0;
|
|
2983
|
+
const normalized = value2.trim().replaceAll("\\", "/");
|
|
2984
|
+
if (!normalized || normalized.startsWith("/") || /^[a-z]:/i.test(normalized) || normalized.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
2985
|
+
return void 0;
|
|
2986
|
+
}
|
|
2987
|
+
return normalized;
|
|
2988
|
+
};
|
|
2989
|
+
const stringList = (value2, max) => Array.isArray(value2) && value2.length <= max && value2.every((item) => typeof item === "string" && item.trim()) ? [...new Set(value2.map((item) => String(item).trim()))] : void 0;
|
|
2990
|
+
const value = input;
|
|
2991
|
+
const stack = value.stack;
|
|
2992
|
+
const frameworks = stringList(stack?.frameworks, 30);
|
|
2993
|
+
const entryPoints = stringList(stack?.entryPoints, 50);
|
|
2994
|
+
if (!stack || !frameworks || !entryPoints || entryPoints.some((file) => !safePath(file))) {
|
|
2995
|
+
return void 0;
|
|
2414
2996
|
}
|
|
2415
|
-
|
|
2997
|
+
const anchors = (raw, max) => {
|
|
2998
|
+
if (!Array.isArray(raw) || raw.length > max) return void 0;
|
|
2999
|
+
const result = [];
|
|
3000
|
+
for (const item of raw) {
|
|
3001
|
+
const row = item;
|
|
3002
|
+
const file = safePath(row?.file);
|
|
3003
|
+
const description = typeof row?.description === "string" ? row.description.trim() : "";
|
|
3004
|
+
if (!file || !description) return void 0;
|
|
3005
|
+
result.push({
|
|
3006
|
+
file,
|
|
3007
|
+
...typeof row.symbol === "string" && row.symbol.trim() ? { symbol: row.symbol.trim() } : {},
|
|
3008
|
+
description
|
|
3009
|
+
});
|
|
3010
|
+
}
|
|
3011
|
+
return result;
|
|
3012
|
+
};
|
|
3013
|
+
if (!Array.isArray(value.featureAreas) || value.featureAreas.length > 200) return void 0;
|
|
3014
|
+
const featureAreas = [];
|
|
3015
|
+
for (const item of value.featureAreas) {
|
|
3016
|
+
const row = item;
|
|
3017
|
+
const root = safePath(row?.root);
|
|
3018
|
+
const files = stringList(row?.files, 200);
|
|
3019
|
+
if (typeof row?.name !== "string" || !row.name.trim() || !root || !files || files.some((file) => !safePath(file))) return void 0;
|
|
3020
|
+
featureAreas.push({ name: row.name.trim(), root, files: files.map((file) => safePath(file)) });
|
|
3021
|
+
}
|
|
3022
|
+
const identifyAnchors = anchors(value.identifyAnchors, 50);
|
|
3023
|
+
const logoutAnchors = anchors(value.logoutAnchors, 50);
|
|
3024
|
+
const existingAnalytics = anchors(value.existingAnalytics, 100);
|
|
3025
|
+
const baseCallSites = anchors(value.integratableCallSites, 500);
|
|
3026
|
+
if (!identifyAnchors || !logoutAnchors || !existingAnalytics || !baseCallSites || !Array.isArray(value.integratableCallSites)) return void 0;
|
|
3027
|
+
const integratableCallSites = [];
|
|
3028
|
+
for (let index = 0; index < baseCallSites.length; index += 1) {
|
|
3029
|
+
const row = value.integratableCallSites[index];
|
|
3030
|
+
const outcome = typeof row.outcome === "string" ? row.outcome.trim() : "";
|
|
3031
|
+
const payloadFields = stringList(row.payloadFields, 50);
|
|
3032
|
+
const similarPaths = stringList(row.similarPaths, 50);
|
|
3033
|
+
if (!outcome || !payloadFields || !similarPaths || similarPaths.some((file) => !safePath(file))) return void 0;
|
|
3034
|
+
integratableCallSites.push({
|
|
3035
|
+
...baseCallSites[index],
|
|
3036
|
+
outcome,
|
|
3037
|
+
payloadFields,
|
|
3038
|
+
similarPaths: similarPaths.map((file) => safePath(file))
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
return {
|
|
3042
|
+
stack: {
|
|
3043
|
+
frameworks,
|
|
3044
|
+
...typeof stack.packageManager === "string" && stack.packageManager.trim() ? { packageManager: stack.packageManager.trim() } : {},
|
|
3045
|
+
entryPoints: entryPoints.map((file) => safePath(file))
|
|
3046
|
+
},
|
|
3047
|
+
featureAreas,
|
|
3048
|
+
identifyAnchors,
|
|
3049
|
+
logoutAnchors,
|
|
3050
|
+
existingAnalytics,
|
|
3051
|
+
integratableCallSites
|
|
3052
|
+
};
|
|
2416
3053
|
}
|
|
2417
3054
|
function placementsFor(registered) {
|
|
2418
3055
|
return registered.map((event) => ({
|
|
@@ -2428,7 +3065,7 @@ function placementsFor(registered) {
|
|
|
2428
3065
|
import { spawn as spawn2 } from "child_process";
|
|
2429
3066
|
import { cp, lstat, mkdir, mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
|
|
2430
3067
|
import { tmpdir } from "os";
|
|
2431
|
-
import { dirname as dirname2, join as
|
|
3068
|
+
import { dirname as dirname2, join as join5, relative, resolve as resolve3, sep as sep3 } from "path";
|
|
2432
3069
|
var WorktreeError = class extends Error {
|
|
2433
3070
|
constructor(code, message) {
|
|
2434
3071
|
super(message);
|
|
@@ -2438,7 +3075,7 @@ var WorktreeError = class extends Error {
|
|
|
2438
3075
|
code;
|
|
2439
3076
|
};
|
|
2440
3077
|
function git(cwd, args) {
|
|
2441
|
-
return new Promise((
|
|
3078
|
+
return new Promise((resolve8) => {
|
|
2442
3079
|
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2443
3080
|
let stdout = "";
|
|
2444
3081
|
let stderr = "";
|
|
@@ -2448,8 +3085,8 @@ function git(cwd, args) {
|
|
|
2448
3085
|
child.stderr.on("data", (chunk) => {
|
|
2449
3086
|
stderr += String(chunk);
|
|
2450
3087
|
});
|
|
2451
|
-
child.on("error", () =>
|
|
2452
|
-
child.on("close", (exitCode) =>
|
|
3088
|
+
child.on("error", () => resolve8({ ok: false, stdout, stderr: "git is not available" }));
|
|
3089
|
+
child.on("close", (exitCode) => resolve8({ ok: exitCode === 0, stdout, stderr }));
|
|
2453
3090
|
});
|
|
2454
3091
|
}
|
|
2455
3092
|
async function must(cwd, args) {
|
|
@@ -2460,8 +3097,8 @@ async function must(cwd, args) {
|
|
|
2460
3097
|
return result.stdout;
|
|
2461
3098
|
}
|
|
2462
3099
|
function safeRepoFile(repoPath, file) {
|
|
2463
|
-
const path =
|
|
2464
|
-
if (path === repoPath || !path.startsWith(repoPath +
|
|
3100
|
+
const path = resolve3(repoPath, file);
|
|
3101
|
+
if (path === repoPath || !path.startsWith(repoPath + sep3) || relative(repoPath, path).startsWith("..")) {
|
|
2465
3102
|
throw new WorktreeError("git_failed", `unsafe progress file path: ${file}`);
|
|
2466
3103
|
}
|
|
2467
3104
|
return path;
|
|
@@ -2499,7 +3136,7 @@ async function createWorktree(repoPath, force = false, allowedDirtyFiles = []) {
|
|
|
2499
3136
|
);
|
|
2500
3137
|
}
|
|
2501
3138
|
let baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
|
|
2502
|
-
const path = await mkdtemp(
|
|
3139
|
+
const path = await mkdtemp(join5(tmpdir(), "whisperr-worktree-"));
|
|
2503
3140
|
await must(repoPath, ["worktree", "add", "--detach", "--force", path, "HEAD"]);
|
|
2504
3141
|
const seedFiles = force ? dirty : dirty.filter((file) => allowed.has(file));
|
|
2505
3142
|
if (seedFiles.length > 0) {
|
|
@@ -2524,12 +3161,18 @@ async function worktreePatch(handle) {
|
|
|
2524
3161
|
return must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
|
|
2525
3162
|
}
|
|
2526
3163
|
async function applyPatchToRepo(handle, patch) {
|
|
3164
|
+
return applyPatch(handle.repoPath, patch, false);
|
|
3165
|
+
}
|
|
3166
|
+
async function reversePatchInRepo(repoPath, patch) {
|
|
3167
|
+
return applyPatch(repoPath, patch, true);
|
|
3168
|
+
}
|
|
3169
|
+
async function applyPatch(repoPath, patch, reverse) {
|
|
2527
3170
|
if (patch.trim() === "") {
|
|
2528
3171
|
return;
|
|
2529
3172
|
}
|
|
2530
|
-
await new Promise((
|
|
2531
|
-
const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
|
|
2532
|
-
cwd:
|
|
3173
|
+
await new Promise((resolve8, reject) => {
|
|
3174
|
+
const child = spawn2("git", ["apply", ...reverse ? ["--reverse"] : [], "--whitespace=nowarn"], {
|
|
3175
|
+
cwd: repoPath,
|
|
2533
3176
|
stdio: ["pipe", "ignore", "pipe"]
|
|
2534
3177
|
});
|
|
2535
3178
|
let stderr = "";
|
|
@@ -2539,31 +3182,13 @@ async function applyPatchToRepo(handle, patch) {
|
|
|
2539
3182
|
child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
|
|
2540
3183
|
child.on("close", (exitCode) => {
|
|
2541
3184
|
if (exitCode === 0) {
|
|
2542
|
-
|
|
3185
|
+
resolve8();
|
|
2543
3186
|
} else {
|
|
2544
|
-
reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
|
|
2545
|
-
}
|
|
2546
|
-
});
|
|
2547
|
-
child.stdin.end(patch);
|
|
2548
|
-
});
|
|
2549
|
-
}
|
|
2550
|
-
async function commitWorktreeCheckpoint(handle) {
|
|
2551
|
-
await must(handle.path, ["add", "-A"]);
|
|
2552
|
-
const patch = await must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
|
|
2553
|
-
if (patch.trim() === "") {
|
|
2554
|
-
return;
|
|
2555
|
-
}
|
|
2556
|
-
await must(handle.path, [
|
|
2557
|
-
"-c",
|
|
2558
|
-
"user.name=Whisperr Wizard",
|
|
2559
|
-
"-c",
|
|
2560
|
-
"user.email=wizard@whisperr.net",
|
|
2561
|
-
"commit",
|
|
2562
|
-
"--no-gpg-sign",
|
|
2563
|
-
"-m",
|
|
2564
|
-
"Whisperr integration checkpoint"
|
|
2565
|
-
]);
|
|
2566
|
-
handle.baseRef = (await must(handle.path, ["rev-parse", "HEAD"])).trim();
|
|
3187
|
+
reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
|
|
3188
|
+
}
|
|
3189
|
+
});
|
|
3190
|
+
child.stdin.end(patch);
|
|
3191
|
+
});
|
|
2567
3192
|
}
|
|
2568
3193
|
async function discardWorktreeChanges(handle) {
|
|
2569
3194
|
await must(handle.path, ["reset", "--hard", handle.baseRef]);
|
|
@@ -2577,6 +3202,16 @@ async function removeWorktree(handle) {
|
|
|
2577
3202
|
// src/engine/orchestrator.ts
|
|
2578
3203
|
async function runEngine(input) {
|
|
2579
3204
|
const runs = new RunsClient(input.config, input.session);
|
|
3205
|
+
const phaseTimings = [];
|
|
3206
|
+
let activePhase = "authorizing";
|
|
3207
|
+
let phaseStartedAt = Date.now();
|
|
3208
|
+
let timingsFinished = false;
|
|
3209
|
+
const finishTimings = () => {
|
|
3210
|
+
if (!timingsFinished) {
|
|
3211
|
+
phaseTimings.push({ phase: activePhase, ms: Date.now() - phaseStartedAt });
|
|
3212
|
+
timingsFinished = true;
|
|
3213
|
+
}
|
|
3214
|
+
};
|
|
2580
3215
|
let bootstrap;
|
|
2581
3216
|
try {
|
|
2582
3217
|
bootstrap = await runs.createOrResumeRun({
|
|
@@ -2594,6 +3229,11 @@ async function runEngine(input) {
|
|
|
2594
3229
|
const runId = bootstrap.run.id;
|
|
2595
3230
|
input.provider.onRunReady?.(runId, bootstrap.run.modelConversationId);
|
|
2596
3231
|
const patchPhase = async (phase, message) => {
|
|
3232
|
+
if (phase !== activePhase) {
|
|
3233
|
+
phaseTimings.push({ phase: activePhase, ms: Date.now() - phaseStartedAt });
|
|
3234
|
+
activePhase = phase;
|
|
3235
|
+
phaseStartedAt = Date.now();
|
|
3236
|
+
}
|
|
2597
3237
|
try {
|
|
2598
3238
|
await runs.patchRun(runId, { phase, ...message ? { message } : {} });
|
|
2599
3239
|
} catch {
|
|
@@ -2605,6 +3245,7 @@ async function runEngine(input) {
|
|
|
2605
3245
|
} catch {
|
|
2606
3246
|
}
|
|
2607
3247
|
};
|
|
3248
|
+
let survey = artifactContent(bootstrap, "survey");
|
|
2608
3249
|
let registered;
|
|
2609
3250
|
try {
|
|
2610
3251
|
const projectEvents = bootstrap.snapshot.events.filter(
|
|
@@ -2617,17 +3258,24 @@ async function runEngine(input) {
|
|
|
2617
3258
|
phase: "surveying",
|
|
2618
3259
|
action: projectEvents.length === 0 ? "no saved progress found; starting a fresh survey" : "saved events need placement recovery; verifying the repository"
|
|
2619
3260
|
});
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
3261
|
+
if (!survey) {
|
|
3262
|
+
await patchPhase("surveying");
|
|
3263
|
+
const surveyRun = await runSurvey(
|
|
3264
|
+
input.provider,
|
|
3265
|
+
input.models,
|
|
3266
|
+
input.repoPath,
|
|
3267
|
+
bootstrap,
|
|
3268
|
+
input.sink
|
|
3269
|
+
);
|
|
3270
|
+
if (surveyRun.outcome.kind !== "completed" || !surveyRun.finished || !surveyRun.survey) {
|
|
3271
|
+
await failRun(`survey ${surveyRun.outcome.kind}`);
|
|
3272
|
+
return { kind: "aborted", reason: `survey ${surveyRun.outcome.kind}` };
|
|
3273
|
+
}
|
|
3274
|
+
survey = surveyRun.survey;
|
|
3275
|
+
const config2 = modelConfigForTask(input.models, "survey", "survey");
|
|
3276
|
+
await runs.persistSurveyArtifact(runId, survey, modelMetadata(config2));
|
|
3277
|
+
} else {
|
|
3278
|
+
input.sink.emit({ phase: "surveying", action: "reusing the saved immutable survey" });
|
|
2631
3279
|
}
|
|
2632
3280
|
await patchPhase("designing");
|
|
2633
3281
|
const recoverableCodes = new Set(missingPlacement.map((event) => event.code));
|
|
@@ -2637,13 +3285,14 @@ async function runEngine(input) {
|
|
|
2637
3285
|
input.models,
|
|
2638
3286
|
input.repoPath,
|
|
2639
3287
|
bootstrap,
|
|
2640
|
-
survey
|
|
3288
|
+
survey,
|
|
2641
3289
|
[],
|
|
2642
3290
|
input.sink,
|
|
2643
|
-
existingCodes
|
|
3291
|
+
existingCodes,
|
|
3292
|
+
[...recoverableCodes]
|
|
2644
3293
|
);
|
|
2645
3294
|
if (!selection.finished) {
|
|
2646
|
-
const reason = selection.outcome === "completed" ? "
|
|
3295
|
+
const reason = selection.outcome === "completed" ? "design did not call finish_event_design" : `design ${selection.outcome}`;
|
|
2647
3296
|
await failRun(reason);
|
|
2648
3297
|
return { kind: "aborted", reason };
|
|
2649
3298
|
}
|
|
@@ -2651,18 +3300,29 @@ async function runEngine(input) {
|
|
|
2651
3300
|
await failRun("selection finished with no accepted events");
|
|
2652
3301
|
return { kind: "aborted", reason: "selection finished with no accepted events" };
|
|
2653
3302
|
}
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
await failRun("selection rejected by user");
|
|
2657
|
-
return { kind: "aborted", reason: "selection rejected by user" };
|
|
2658
|
-
}
|
|
2659
|
-
if (approved.length === 0) {
|
|
2660
|
-
await failRun("no events accepted for instrumentation");
|
|
2661
|
-
return { kind: "aborted", reason: "no events accepted for instrumentation" };
|
|
3303
|
+
if (recoverableCodes.size > 0 && (selection.proposed.length !== recoverableCodes.size || selection.proposed.some((event) => !recoverableCodes.has(event.code)))) {
|
|
3304
|
+
throw new Error("placement recovery must cover the frozen event set exactly");
|
|
2662
3305
|
}
|
|
2663
|
-
await patchPhase("persisting", `registering ${
|
|
2664
|
-
const
|
|
2665
|
-
|
|
3306
|
+
await patchPhase("persisting", `registering ${selection.proposed.length} events`);
|
|
3307
|
+
const design = {
|
|
3308
|
+
surveyHash: artifactContentHash(survey),
|
|
3309
|
+
summary: selection.summary,
|
|
3310
|
+
events: [...selection.proposed].sort((a, b) => a.code.localeCompare(b.code))
|
|
3311
|
+
};
|
|
3312
|
+
const config = modelConfigForTask(input.models, "design", "design");
|
|
3313
|
+
const committed = await runs.commitDesign(
|
|
3314
|
+
runId,
|
|
3315
|
+
design,
|
|
3316
|
+
design.events.map(eventRequest),
|
|
3317
|
+
modelMetadata(config)
|
|
3318
|
+
);
|
|
3319
|
+
registered = registeredFromSnapshot(
|
|
3320
|
+
committed.events.filter((event) => event.projectId === bootstrap.project.id)
|
|
3321
|
+
);
|
|
3322
|
+
input.sink.emit({
|
|
3323
|
+
phase: "persisting",
|
|
3324
|
+
action: `designed and registered ${selection.proposed.length} events`
|
|
3325
|
+
});
|
|
2666
3326
|
const unresolved = projectEvents.filter((event) => !registered.some((candidate) => candidate.code === event.code)).map((event) => event.code);
|
|
2667
3327
|
if (unresolved.length > 0) {
|
|
2668
3328
|
throw new Error(`could not recover placements for: ${unresolved.join(", ")}`);
|
|
@@ -2686,17 +3346,19 @@ async function runEngine(input) {
|
|
|
2686
3346
|
const prior = bootstrap.run.integrationEvidence;
|
|
2687
3347
|
const changedFiles2 = new Set(prior?.changedFiles ?? []);
|
|
2688
3348
|
const eventStatuses = await reconcileProgress(input.repoPath, registered, prior, input.sink);
|
|
3349
|
+
const eventFailureReasons = /* @__PURE__ */ new Map();
|
|
3350
|
+
let verificationStatus = "pending";
|
|
2689
3351
|
for (const event of registered) {
|
|
2690
3352
|
const status = eventStatuses.get(event.code) ?? "planned";
|
|
2691
3353
|
if (status !== "planned" && status !== "failed" && status !== "unsupported") {
|
|
2692
3354
|
changedFiles2.add(event.anchorFile);
|
|
2693
3355
|
}
|
|
2694
3356
|
}
|
|
2695
|
-
let identifyWired = prior?.identifyWired === true && await setupLooksPresent(input.repoPath, changedFiles2);
|
|
3357
|
+
let identifyWired = prior?.identifyWired === true && await setupLooksPresent(input.repoPath, changedFiles2, survey);
|
|
2696
3358
|
const progress = () => ({
|
|
2697
3359
|
changedFiles: [...changedFiles2].sort(),
|
|
2698
3360
|
identifyWired,
|
|
2699
|
-
verificationStatus
|
|
3361
|
+
verificationStatus,
|
|
2700
3362
|
events: buildEvidence(registered, eventStatuses)
|
|
2701
3363
|
});
|
|
2702
3364
|
const saveProgress = async () => {
|
|
@@ -2711,6 +3373,7 @@ async function runEngine(input) {
|
|
|
2711
3373
|
}
|
|
2712
3374
|
await patchPhase("binding");
|
|
2713
3375
|
let worktree;
|
|
3376
|
+
const ownedPatches = [];
|
|
2714
3377
|
try {
|
|
2715
3378
|
worktree = await createWorktree(
|
|
2716
3379
|
input.repoPath,
|
|
@@ -2722,8 +3385,9 @@ async function runEngine(input) {
|
|
|
2722
3385
|
await failRun(reason);
|
|
2723
3386
|
return { kind: "aborted", reason };
|
|
2724
3387
|
}
|
|
2725
|
-
const
|
|
2726
|
-
|
|
3388
|
+
const setupWorktree = worktree;
|
|
3389
|
+
const checkpoint = async (handle, kind, eventCodes = []) => {
|
|
3390
|
+
const patch = await worktreePatch(handle);
|
|
2727
3391
|
if (patch.trim() === "") {
|
|
2728
3392
|
return [];
|
|
2729
3393
|
}
|
|
@@ -2732,102 +3396,351 @@ async function runEngine(input) {
|
|
|
2732
3396
|
changedFiles2.add(file);
|
|
2733
3397
|
}
|
|
2734
3398
|
await saveProgress();
|
|
2735
|
-
await applyPatchToRepo(
|
|
2736
|
-
|
|
3399
|
+
await applyPatchToRepo(handle, patch);
|
|
3400
|
+
ownedPatches.push({ patch, files, eventCodes, kind });
|
|
2737
3401
|
return files;
|
|
2738
3402
|
};
|
|
2739
3403
|
try {
|
|
2740
|
-
const bindings = await writeBindingsModule(
|
|
3404
|
+
const bindings = await writeBindingsModule(setupWorktree, input.target, registered);
|
|
2741
3405
|
if (!identifyWired) {
|
|
3406
|
+
const validateSetup = async () => {
|
|
3407
|
+
if (await readFile4(resolve4(setupWorktree.path, bindings.relativePath), "utf8") !== bindings.content) {
|
|
3408
|
+
return false;
|
|
3409
|
+
}
|
|
3410
|
+
const candidatePatch = await worktreePatch(setupWorktree);
|
|
3411
|
+
if (patchAddsProductEvents(candidatePatch, bindings.relativePath, registered)) {
|
|
3412
|
+
return false;
|
|
3413
|
+
}
|
|
3414
|
+
return setupLooksPresent(
|
|
3415
|
+
setupWorktree.path,
|
|
3416
|
+
new Set(patchFiles(candidatePatch)),
|
|
3417
|
+
survey
|
|
3418
|
+
);
|
|
3419
|
+
};
|
|
2742
3420
|
const setupCompleted = await runSdkSetupPass({
|
|
2743
3421
|
provider: input.provider,
|
|
2744
3422
|
models: input.models,
|
|
2745
|
-
worktree,
|
|
3423
|
+
worktree: setupWorktree,
|
|
2746
3424
|
target: input.target,
|
|
2747
3425
|
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
2748
3426
|
registered,
|
|
2749
3427
|
ingestion: bootstrap.ingestion,
|
|
3428
|
+
survey,
|
|
2750
3429
|
sink: input.sink,
|
|
2751
3430
|
saveClusters: () => {
|
|
2752
3431
|
},
|
|
2753
|
-
discardChanges: async () => discardWorktreeChanges(
|
|
3432
|
+
discardChanges: async () => discardWorktreeChanges(setupWorktree),
|
|
2754
3433
|
onClusterIntegrated: async () => {
|
|
2755
3434
|
}
|
|
2756
|
-
}, bindings);
|
|
3435
|
+
}, bindings, validateSetup);
|
|
2757
3436
|
if (!setupCompleted) {
|
|
2758
|
-
throw new Error("SDK setup did not complete");
|
|
3437
|
+
throw new Error("SDK setup did not complete or failed deterministic validation");
|
|
3438
|
+
}
|
|
3439
|
+
const setupPatch = await worktreePatch(setupWorktree);
|
|
3440
|
+
const setupFiles = patchFiles(setupPatch);
|
|
3441
|
+
if (!await setupLooksPresent(setupWorktree.path, new Set(setupFiles), survey)) {
|
|
3442
|
+
throw new Error("SDK initialization, identify(), session restoration, or logout reset() could not be verified");
|
|
3443
|
+
}
|
|
3444
|
+
await checkpoint(setupWorktree, "setup");
|
|
3445
|
+
identifyWired = await setupLooksPresent(input.repoPath, changedFiles2, survey);
|
|
3446
|
+
if (!identifyWired) {
|
|
3447
|
+
throw new Error("SDK initialization, bindings, identify(), and reset() could not be verified");
|
|
2759
3448
|
}
|
|
2760
|
-
await checkpoint();
|
|
2761
|
-
identifyWired = true;
|
|
2762
3449
|
await saveProgress();
|
|
2763
3450
|
} else {
|
|
2764
|
-
await checkpoint();
|
|
3451
|
+
await checkpoint(setupWorktree, "setup");
|
|
2765
3452
|
}
|
|
2766
3453
|
const pending = registered.filter((event) => {
|
|
2767
3454
|
const status = eventStatuses.get(event.code) ?? "planned";
|
|
2768
3455
|
return status === "planned" || status === "failed";
|
|
2769
3456
|
});
|
|
2770
3457
|
const clusters = deriveClusters(placementsFor(pending));
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
const
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
3458
|
+
await patchPhase("integrating");
|
|
3459
|
+
await removeWorktree(setupWorktree).catch(() => {
|
|
3460
|
+
});
|
|
3461
|
+
worktree = void 0;
|
|
3462
|
+
const queue = [...clusters];
|
|
3463
|
+
const active = /* @__PURE__ */ new Map();
|
|
3464
|
+
const lockedFiles = /* @__PURE__ */ new Set();
|
|
3465
|
+
let workerSequence = 0;
|
|
3466
|
+
while (queue.length > 0 || active.size > 0) {
|
|
3467
|
+
while (active.size < 4) {
|
|
3468
|
+
const nextIndex = queue.findIndex((cluster2) => !lockedFiles.has(cluster2.files[0]));
|
|
3469
|
+
if (nextIndex < 0) break;
|
|
3470
|
+
const cluster = queue.splice(nextIndex, 1)[0];
|
|
3471
|
+
const file = cluster.files[0];
|
|
3472
|
+
lockedFiles.add(file);
|
|
3473
|
+
const id2 = workerSequence++;
|
|
3474
|
+
active.set(id2, runClusterWorker(input, bootstrap, registered, survey, changedFiles2, cluster));
|
|
3475
|
+
}
|
|
3476
|
+
if (active.size === 0) break;
|
|
3477
|
+
const { id, result: worker } = await Promise.race(
|
|
3478
|
+
[...active.entries()].map(async ([id2, task]) => ({ id: id2, result: await task }))
|
|
3479
|
+
);
|
|
3480
|
+
active.delete(id);
|
|
3481
|
+
lockedFiles.delete(worker.cluster.files[0]);
|
|
3482
|
+
const returned = worker.integration?.clusters[0];
|
|
3483
|
+
let applied = false;
|
|
3484
|
+
let failure = worker.error ?? returned?.note ?? "cluster patch could not be merged";
|
|
3485
|
+
if (returned?.status === "verified" && worker.handle) {
|
|
3486
|
+
const files = patchFiles(worker.patch);
|
|
3487
|
+
const unexpected = files.filter((file) => !worker.cluster.files.includes(file));
|
|
3488
|
+
if (unexpected.length === 0) {
|
|
3489
|
+
try {
|
|
3490
|
+
for (const file of files) changedFiles2.add(file);
|
|
3491
|
+
await saveProgress();
|
|
3492
|
+
await applyPatchToRepo(worker.handle, worker.patch);
|
|
3493
|
+
ownedPatches.push({
|
|
3494
|
+
patch: worker.patch,
|
|
3495
|
+
files,
|
|
3496
|
+
eventCodes: worker.cluster.events.map((event) => event.eventCode),
|
|
3497
|
+
kind: "cluster"
|
|
3498
|
+
});
|
|
3499
|
+
applied = true;
|
|
3500
|
+
} catch (error) {
|
|
3501
|
+
failure = `serial patch merge failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
3502
|
+
}
|
|
3503
|
+
} else {
|
|
3504
|
+
failure = `cluster edited files outside its isolation boundary: ${unexpected.join(", ")}`;
|
|
2792
3505
|
}
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
3506
|
+
}
|
|
3507
|
+
if (!applied && worker.cluster.events.length > 1) {
|
|
3508
|
+
const singles = worker.cluster.events.map((event, index) => ({
|
|
3509
|
+
id: clusterIdFor([event.file], [event.eventId || event.eventCode], index),
|
|
3510
|
+
files: [event.file],
|
|
3511
|
+
events: [event],
|
|
3512
|
+
status: "pending",
|
|
3513
|
+
attempts: 0
|
|
3514
|
+
}));
|
|
3515
|
+
queue.push(...singles);
|
|
3516
|
+
input.sink.emit({
|
|
3517
|
+
phase: "integrating",
|
|
3518
|
+
file: worker.cluster.files[0],
|
|
3519
|
+
action: "Sol could not finish the cluster; retrying each event independently"
|
|
3520
|
+
});
|
|
3521
|
+
} else {
|
|
3522
|
+
for (const placement of worker.cluster.events) {
|
|
3523
|
+
const status = applied ? "syntax_verified" : "failed";
|
|
3524
|
+
eventStatuses.set(placement.eventCode, status);
|
|
3525
|
+
if (!applied) eventFailureReasons.set(placement.eventCode, failure);
|
|
2799
3526
|
await saveProgress();
|
|
2800
3527
|
input.sink.emit({
|
|
2801
3528
|
phase: "integrating",
|
|
2802
3529
|
file: placement.file,
|
|
2803
|
-
action: `saved ${placement.eventCode}
|
|
3530
|
+
action: applied ? `merged and saved ${placement.eventCode}` : `${placement.eventCode} needs manual attention`
|
|
2804
3531
|
});
|
|
2805
3532
|
}
|
|
2806
3533
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
const integration = await runClusterIntegration(
|
|
2810
|
-
deps,
|
|
2811
|
-
clusters,
|
|
2812
|
-
async () => worktreePatch(worktree)
|
|
2813
|
-
);
|
|
2814
|
-
for (const [code, status] of integration.eventStatuses) {
|
|
2815
|
-
if ((eventStatuses.get(code) ?? "planned") === "planned") {
|
|
2816
|
-
eventStatuses.set(code, status);
|
|
2817
|
-
}
|
|
3534
|
+
if (worker.handle) await removeWorktree(worker.handle).catch(() => {
|
|
3535
|
+
});
|
|
2818
3536
|
}
|
|
2819
3537
|
await saveProgress();
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
if (
|
|
2825
|
-
|
|
2826
|
-
|
|
3538
|
+
await patchPhase("reviewing");
|
|
3539
|
+
const reviewableByFile = groupEventsByFile(registered.filter(
|
|
3540
|
+
(event) => eventStatuses.get(event.code) === "syntax_verified"
|
|
3541
|
+
));
|
|
3542
|
+
if (reviewableByFile.size > 0) {
|
|
3543
|
+
const reviewWorktree = await createWorktree(
|
|
3544
|
+
input.repoPath,
|
|
3545
|
+
input.force ?? false,
|
|
3546
|
+
[...changedFiles2]
|
|
2827
3547
|
);
|
|
3548
|
+
const reviewDeps = integrationDeps(
|
|
3549
|
+
input,
|
|
3550
|
+
bootstrap,
|
|
3551
|
+
registered,
|
|
3552
|
+
survey,
|
|
3553
|
+
reviewWorktree,
|
|
3554
|
+
[...changedFiles2]
|
|
3555
|
+
);
|
|
3556
|
+
const reviews = await mapLimit(
|
|
3557
|
+
[...reviewableByFile.entries()],
|
|
3558
|
+
2,
|
|
3559
|
+
async ([file, events]) => ({
|
|
3560
|
+
file,
|
|
3561
|
+
events,
|
|
3562
|
+
review: await reviewIntegratedFile(reviewDeps, file, events)
|
|
3563
|
+
})
|
|
3564
|
+
);
|
|
3565
|
+
await removeWorktree(reviewWorktree).catch(() => {
|
|
3566
|
+
});
|
|
3567
|
+
for (const reviewed of reviews) {
|
|
3568
|
+
if (reviewed.review.ok) {
|
|
3569
|
+
for (const event of reviewed.events) eventStatuses.set(event.code, "reviewed");
|
|
3570
|
+
await saveProgress();
|
|
3571
|
+
continue;
|
|
3572
|
+
}
|
|
3573
|
+
const repairWorktree = await createWorktree(
|
|
3574
|
+
input.repoPath,
|
|
3575
|
+
input.force ?? false,
|
|
3576
|
+
[...changedFiles2]
|
|
3577
|
+
);
|
|
3578
|
+
const repairDeps = integrationDeps(
|
|
3579
|
+
input,
|
|
3580
|
+
bootstrap,
|
|
3581
|
+
registered,
|
|
3582
|
+
survey,
|
|
3583
|
+
repairWorktree,
|
|
3584
|
+
[...changedFiles2]
|
|
3585
|
+
);
|
|
3586
|
+
const repaired = await repairIntegratedFile(
|
|
3587
|
+
repairDeps,
|
|
3588
|
+
reviewed.file,
|
|
3589
|
+
reviewed.events,
|
|
3590
|
+
reviewed.review
|
|
3591
|
+
);
|
|
3592
|
+
const reviewCluster = {
|
|
3593
|
+
id: clusterIdFor([reviewed.file], reviewed.events.map((event) => event.eventId)),
|
|
3594
|
+
files: [reviewed.file],
|
|
3595
|
+
events: placementsFor(reviewed.events),
|
|
3596
|
+
status: "verified",
|
|
3597
|
+
attempts: 1
|
|
3598
|
+
};
|
|
3599
|
+
const structural = repaired ? await structuralClusterCheck(repairWorktree.path, reviewCluster) : { ok: false, notes: "Sol repair did not complete" };
|
|
3600
|
+
const recheck = structural.ok ? await reviewIntegratedFile(repairDeps, reviewed.file, reviewed.events, ":recheck") : { ok: false, issues: [], notes: structural.notes };
|
|
3601
|
+
const repairPatch = await worktreePatch(repairWorktree);
|
|
3602
|
+
const repairFiles = patchFiles(repairPatch);
|
|
3603
|
+
if (repaired && structural.ok && recheck.ok && repairFiles.every((file) => file === reviewed.file)) {
|
|
3604
|
+
for (const file of repairFiles) changedFiles2.add(file);
|
|
3605
|
+
await saveProgress();
|
|
3606
|
+
await applyPatchToRepo(repairWorktree, repairPatch);
|
|
3607
|
+
ownedPatches.push({
|
|
3608
|
+
patch: repairPatch,
|
|
3609
|
+
files: repairFiles,
|
|
3610
|
+
eventCodes: reviewed.events.map((event) => event.code),
|
|
3611
|
+
kind: "semantic"
|
|
3612
|
+
});
|
|
3613
|
+
for (const event of reviewed.events) eventStatuses.set(event.code, "reviewed");
|
|
3614
|
+
} else {
|
|
3615
|
+
const reasons = reviewed.review.issues.map((issue) => issue.reason).join("; ") || recheck.notes || structural.notes;
|
|
3616
|
+
await rollbackFileEventPatches(
|
|
3617
|
+
input.repoPath,
|
|
3618
|
+
reviewed.file,
|
|
3619
|
+
ownedPatches
|
|
3620
|
+
);
|
|
3621
|
+
for (const event of reviewed.events) {
|
|
3622
|
+
eventStatuses.set(event.code, "failed");
|
|
3623
|
+
eventFailureReasons.set(event.code, reasons.slice(0, 500));
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
await removeWorktree(repairWorktree).catch(() => {
|
|
3627
|
+
});
|
|
3628
|
+
await saveProgress();
|
|
3629
|
+
}
|
|
2828
3630
|
}
|
|
2829
3631
|
await patchPhase("verifying");
|
|
2830
|
-
|
|
3632
|
+
let verificationWorktree = await createWorktree(
|
|
3633
|
+
input.repoPath,
|
|
3634
|
+
input.force ?? false,
|
|
3635
|
+
[...changedFiles2]
|
|
3636
|
+
);
|
|
3637
|
+
let verificationDeps = integrationDeps(
|
|
3638
|
+
input,
|
|
3639
|
+
bootstrap,
|
|
3640
|
+
registered,
|
|
3641
|
+
survey,
|
|
3642
|
+
verificationWorktree,
|
|
3643
|
+
[...changedFiles2]
|
|
3644
|
+
);
|
|
3645
|
+
let finalVerify = await finalizeIntegration(verificationDeps, input.repoPath);
|
|
3646
|
+
for (let cycle = 0; finalVerify.status === "failed" && cycle < 2; cycle += 1) {
|
|
3647
|
+
const repaired = await repairVerification(verificationDeps, finalVerify.results, cycle + 1);
|
|
3648
|
+
if (!repaired) break;
|
|
3649
|
+
const repairPatch = await worktreePatch(verificationWorktree);
|
|
3650
|
+
const repairFiles = patchFiles(repairPatch);
|
|
3651
|
+
const allowedRepairFiles = new Set(verificationDeps.allowedRepairFiles ?? []);
|
|
3652
|
+
const structural = await structuralRepairCheck(
|
|
3653
|
+
verificationWorktree.path,
|
|
3654
|
+
registered,
|
|
3655
|
+
repairFiles
|
|
3656
|
+
);
|
|
3657
|
+
if (repairPatch.trim() === "" || repairFiles.some((file) => !allowedRepairFiles.has(file)) || !structural.ok) {
|
|
3658
|
+
break;
|
|
3659
|
+
}
|
|
3660
|
+
for (const file of repairFiles) changedFiles2.add(file);
|
|
3661
|
+
await saveProgress();
|
|
3662
|
+
await applyPatchToRepo(verificationWorktree, repairPatch);
|
|
3663
|
+
ownedPatches.push({
|
|
3664
|
+
patch: repairPatch,
|
|
3665
|
+
files: repairFiles,
|
|
3666
|
+
eventCodes: registered.map((event) => event.code),
|
|
3667
|
+
kind: "verification"
|
|
3668
|
+
});
|
|
3669
|
+
await removeWorktree(verificationWorktree).catch(() => {
|
|
3670
|
+
});
|
|
3671
|
+
finalVerify = await finalizeIntegration(verificationDeps, input.repoPath);
|
|
3672
|
+
if (finalVerify.status === "failed" && cycle + 1 < 2) {
|
|
3673
|
+
verificationWorktree = await createWorktree(
|
|
3674
|
+
input.repoPath,
|
|
3675
|
+
input.force ?? false,
|
|
3676
|
+
[...changedFiles2]
|
|
3677
|
+
);
|
|
3678
|
+
verificationDeps = integrationDeps(
|
|
3679
|
+
input,
|
|
3680
|
+
bootstrap,
|
|
3681
|
+
registered,
|
|
3682
|
+
survey,
|
|
3683
|
+
verificationWorktree,
|
|
3684
|
+
[...changedFiles2]
|
|
3685
|
+
);
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
verificationStatus = finalVerify.status;
|
|
3689
|
+
if (finalVerify.status !== "passed") {
|
|
3690
|
+
await removeWorktree(verificationWorktree).catch(() => {
|
|
3691
|
+
});
|
|
3692
|
+
await rollbackOwnedPatches(input.repoPath, ownedPatches);
|
|
3693
|
+
changedFiles2.clear();
|
|
3694
|
+
identifyWired = false;
|
|
3695
|
+
for (const event of registered) {
|
|
3696
|
+
eventStatuses.set(event.code, "failed");
|
|
3697
|
+
eventFailureReasons.set(
|
|
3698
|
+
event.code,
|
|
3699
|
+
`final verification ${finalVerify.status}${finalVerify.command ? `: ${finalVerify.command}` : ""}`
|
|
3700
|
+
);
|
|
3701
|
+
}
|
|
3702
|
+
await saveProgress();
|
|
3703
|
+
throw new Error(`final verification ${finalVerify.status} after two Sol repair cycles`);
|
|
3704
|
+
}
|
|
3705
|
+
await removeWorktree(verificationWorktree).catch(() => {
|
|
3706
|
+
});
|
|
3707
|
+
identifyWired = await setupLooksPresent(input.repoPath, changedFiles2, survey);
|
|
3708
|
+
if (!identifyWired) {
|
|
3709
|
+
await rollbackOwnedPatches(input.repoPath, ownedPatches);
|
|
3710
|
+
changedFiles2.clear();
|
|
3711
|
+
for (const event of registered) {
|
|
3712
|
+
eventStatuses.set(event.code, "failed");
|
|
3713
|
+
eventFailureReasons.set(event.code, "SDK initialization or identify() is missing after final repair");
|
|
3714
|
+
}
|
|
3715
|
+
await saveProgress();
|
|
3716
|
+
throw new Error("SDK initialization, identify(), and logout reset() must be present before completion");
|
|
3717
|
+
}
|
|
3718
|
+
for (const event of registered) {
|
|
3719
|
+
if (eventStatuses.get(event.code) === "reviewed") {
|
|
3720
|
+
eventStatuses.set(event.code, "build_verified");
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
await saveProgress();
|
|
3724
|
+
const failures = registered.filter((event) => eventStatuses.get(event.code) !== "build_verified");
|
|
3725
|
+
if (failures.length > 0) {
|
|
3726
|
+
const reason = `${failures.length} event${failures.length === 1 ? "" : "s"} could not be verified`;
|
|
3727
|
+
await failRun(reason);
|
|
3728
|
+
finishTimings();
|
|
3729
|
+
return {
|
|
3730
|
+
kind: "partial",
|
|
3731
|
+
runId,
|
|
3732
|
+
registered,
|
|
3733
|
+
eventStatuses,
|
|
3734
|
+
reportEvents: buildReportEvents(registered, eventStatuses, eventFailureReasons),
|
|
3735
|
+
changedFiles: [...changedFiles2].sort(),
|
|
3736
|
+
identifyWired,
|
|
3737
|
+
applied: changedFiles2.size > 0,
|
|
3738
|
+
summary: summarize(registered, eventStatuses).join("\n"),
|
|
3739
|
+
verificationStatus,
|
|
3740
|
+
phaseTimings,
|
|
3741
|
+
modelRoutes: modelRoutes(input.models)
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
2831
3744
|
await patchPhase("reporting");
|
|
2832
3745
|
await runs.completeRun(runId, {
|
|
2833
3746
|
changedFiles: [...changedFiles2].sort(),
|
|
@@ -2836,38 +3749,194 @@ async function runEngine(input) {
|
|
|
2836
3749
|
...finalVerify.command ? { verificationCommand: finalVerify.command } : {},
|
|
2837
3750
|
events: buildEvidence(registered, eventStatuses)
|
|
2838
3751
|
});
|
|
2839
|
-
await removeWorktree(worktree).catch(() => {
|
|
3752
|
+
if (worktree) await removeWorktree(worktree).catch(() => {
|
|
2840
3753
|
});
|
|
2841
3754
|
const summaryLines = summarize(registered, eventStatuses);
|
|
2842
|
-
|
|
3755
|
+
finishTimings();
|
|
2843
3756
|
return {
|
|
2844
|
-
kind:
|
|
3757
|
+
kind: "completed",
|
|
2845
3758
|
runId,
|
|
2846
3759
|
registered,
|
|
2847
3760
|
eventStatuses,
|
|
2848
|
-
reportEvents: buildReportEvents(registered, eventStatuses),
|
|
3761
|
+
reportEvents: buildReportEvents(registered, eventStatuses, eventFailureReasons),
|
|
2849
3762
|
changedFiles: [...changedFiles2].sort(),
|
|
2850
3763
|
identifyWired,
|
|
2851
3764
|
applied: changedFiles2.size > 0,
|
|
2852
|
-
summary: summaryLines.join("\n")
|
|
3765
|
+
summary: summaryLines.join("\n"),
|
|
3766
|
+
verificationStatus,
|
|
3767
|
+
phaseTimings,
|
|
3768
|
+
modelRoutes: modelRoutes(input.models)
|
|
2853
3769
|
};
|
|
2854
3770
|
} catch (error) {
|
|
2855
3771
|
const reason = error instanceof Error ? error.message : String(error);
|
|
3772
|
+
for (const event of registered) {
|
|
3773
|
+
const status = eventStatuses.get(event.code) ?? "planned";
|
|
3774
|
+
if (status === "planned") {
|
|
3775
|
+
eventStatuses.set(event.code, "failed");
|
|
3776
|
+
eventFailureReasons.set(event.code, reason);
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
2856
3779
|
await failRun(reason);
|
|
2857
|
-
await removeWorktree(worktree).catch(() => {
|
|
3780
|
+
if (worktree) await removeWorktree(worktree).catch(() => {
|
|
2858
3781
|
});
|
|
3782
|
+
finishTimings();
|
|
2859
3783
|
return {
|
|
2860
3784
|
kind: "partial",
|
|
2861
3785
|
runId,
|
|
2862
3786
|
registered,
|
|
2863
3787
|
eventStatuses,
|
|
2864
|
-
reportEvents: buildReportEvents(registered, eventStatuses),
|
|
3788
|
+
reportEvents: buildReportEvents(registered, eventStatuses, eventFailureReasons),
|
|
2865
3789
|
changedFiles: [...changedFiles2].sort(),
|
|
2866
3790
|
identifyWired,
|
|
2867
3791
|
applied: changedFiles2.size > 0,
|
|
2868
|
-
summary: `Integration stopped early: ${reason}. Saved event progress will be verified on rerun
|
|
3792
|
+
summary: `Integration stopped early: ${reason}. Saved event progress will be verified on rerun.`,
|
|
3793
|
+
verificationStatus,
|
|
3794
|
+
phaseTimings,
|
|
3795
|
+
modelRoutes: modelRoutes(input.models)
|
|
3796
|
+
};
|
|
3797
|
+
}
|
|
3798
|
+
}
|
|
3799
|
+
function artifactContent(bootstrap, kind) {
|
|
3800
|
+
return bootstrap.artifacts.find((artifact) => artifact.kind === kind)?.content;
|
|
3801
|
+
}
|
|
3802
|
+
function modelConfigForTask(models, task, role) {
|
|
3803
|
+
return models[task] ?? models[role];
|
|
3804
|
+
}
|
|
3805
|
+
function modelMetadata(config) {
|
|
3806
|
+
return {
|
|
3807
|
+
model: config.model,
|
|
3808
|
+
...config.effort ? { effort: config.effort } : {},
|
|
3809
|
+
...config.serviceTier ? { serviceTier: config.serviceTier } : {},
|
|
3810
|
+
fastMode: config.fastMode === true
|
|
3811
|
+
};
|
|
3812
|
+
}
|
|
3813
|
+
function eventRequest(event) {
|
|
3814
|
+
return {
|
|
3815
|
+
code: event.code,
|
|
3816
|
+
name: event.name,
|
|
3817
|
+
reasoning: event.reasoning,
|
|
3818
|
+
anchorFile: event.anchorFile,
|
|
3819
|
+
...event.anchorSymbol ? { anchorSymbol: event.anchorSymbol } : {},
|
|
3820
|
+
...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
async function runClusterWorker(input, bootstrap, registered, survey, changedFiles2, cluster) {
|
|
3824
|
+
let handle;
|
|
3825
|
+
try {
|
|
3826
|
+
handle = await createWorktree(input.repoPath, input.force ?? false, [...changedFiles2]);
|
|
3827
|
+
const deps = integrationDeps(input, bootstrap, registered, survey, handle, [...changedFiles2]);
|
|
3828
|
+
const integration = await runClusterIntegration(
|
|
3829
|
+
deps,
|
|
3830
|
+
[cluster],
|
|
3831
|
+
async () => worktreePatch(handle)
|
|
3832
|
+
);
|
|
3833
|
+
return {
|
|
3834
|
+
cluster,
|
|
3835
|
+
handle,
|
|
3836
|
+
integration,
|
|
3837
|
+
patch: await worktreePatch(handle)
|
|
3838
|
+
};
|
|
3839
|
+
} catch (error) {
|
|
3840
|
+
return {
|
|
3841
|
+
cluster,
|
|
3842
|
+
handle,
|
|
3843
|
+
patch: "",
|
|
3844
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3845
|
+
};
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
function integrationDeps(input, bootstrap, registered, survey, worktree, allowedRepairFiles) {
|
|
3849
|
+
return {
|
|
3850
|
+
provider: input.provider,
|
|
3851
|
+
models: input.models,
|
|
3852
|
+
worktree,
|
|
3853
|
+
target: input.target,
|
|
3854
|
+
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
3855
|
+
registered,
|
|
3856
|
+
ingestion: bootstrap.ingestion,
|
|
3857
|
+
survey,
|
|
3858
|
+
allowedRepairFiles: allowedRepairFiles.filter(
|
|
3859
|
+
(file) => file !== generateBindings(input.target, registered).relativePath
|
|
3860
|
+
),
|
|
3861
|
+
sink: input.sink,
|
|
3862
|
+
saveClusters: () => {
|
|
3863
|
+
},
|
|
3864
|
+
discardChanges: async () => discardWorktreeChanges(worktree),
|
|
3865
|
+
onClusterIntegrated: async () => {
|
|
3866
|
+
}
|
|
3867
|
+
};
|
|
3868
|
+
}
|
|
3869
|
+
function groupEventsByFile(events) {
|
|
3870
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3871
|
+
for (const event of events) {
|
|
3872
|
+
const current = grouped.get(event.anchorFile) ?? [];
|
|
3873
|
+
current.push(event);
|
|
3874
|
+
grouped.set(event.anchorFile, current);
|
|
3875
|
+
}
|
|
3876
|
+
return grouped;
|
|
3877
|
+
}
|
|
3878
|
+
async function structuralRepairCheck(repoPath, events, files) {
|
|
3879
|
+
const repairedFiles = new Set(files);
|
|
3880
|
+
for (const [file, fileEvents] of groupEventsByFile(
|
|
3881
|
+
events.filter((event) => repairedFiles.has(event.anchorFile))
|
|
3882
|
+
)) {
|
|
3883
|
+
const result = await structuralClusterCheck(repoPath, {
|
|
3884
|
+
id: clusterIdFor([file], fileEvents.map((event) => event.eventId)),
|
|
3885
|
+
files: [file],
|
|
3886
|
+
events: placementsFor(fileEvents),
|
|
3887
|
+
status: "verified",
|
|
3888
|
+
attempts: 1
|
|
3889
|
+
});
|
|
3890
|
+
if (!result.ok) return result;
|
|
3891
|
+
}
|
|
3892
|
+
return { ok: true, notes: "" };
|
|
3893
|
+
}
|
|
3894
|
+
async function mapLimit(values, limit, operation) {
|
|
3895
|
+
const results = new Array(values.length);
|
|
3896
|
+
let nextIndex = 0;
|
|
3897
|
+
const workers = Array.from({ length: Math.min(limit, values.length) }, async () => {
|
|
3898
|
+
for (; ; ) {
|
|
3899
|
+
const index = nextIndex++;
|
|
3900
|
+
if (index >= values.length) return;
|
|
3901
|
+
results[index] = await operation(values[index]);
|
|
3902
|
+
}
|
|
3903
|
+
});
|
|
3904
|
+
await Promise.all(workers);
|
|
3905
|
+
return results;
|
|
3906
|
+
}
|
|
3907
|
+
async function rollbackFileEventPatches(repoPath, file, patches) {
|
|
3908
|
+
for (let index = patches.length - 1; index >= 0; index -= 1) {
|
|
3909
|
+
const patch = patches[index];
|
|
3910
|
+
if (patch.rolledBack || patch.kind !== "cluster" && patch.kind !== "semantic" || !patch.files.includes(file)) {
|
|
3911
|
+
continue;
|
|
3912
|
+
}
|
|
3913
|
+
await reversePatchInRepo(repoPath, patch.patch);
|
|
3914
|
+
patch.rolledBack = true;
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
async function rollbackOwnedPatches(repoPath, patches) {
|
|
3918
|
+
for (let index = patches.length - 1; index >= 0; index -= 1) {
|
|
3919
|
+
const patch = patches[index];
|
|
3920
|
+
if (patch.rolledBack) continue;
|
|
3921
|
+
await reversePatchInRepo(repoPath, patch.patch);
|
|
3922
|
+
patch.rolledBack = true;
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
function modelRoutes(models) {
|
|
3926
|
+
const result = {};
|
|
3927
|
+
for (const task of ["survey", "design", "sdk_setup", "cluster_edit", "review", "repair"]) {
|
|
3928
|
+
const role = task === "survey" || task === "design" ? task : task === "review" ? "review" : "edit";
|
|
3929
|
+
const config = models[task] ?? models[role];
|
|
3930
|
+
result[task] = {
|
|
3931
|
+
model: config.model,
|
|
3932
|
+
...config.effort ? { effort: config.effort } : {},
|
|
3933
|
+
serviceTier: config.fastMode ? "fast" : config.serviceTier ?? "default",
|
|
3934
|
+
maxTurns: config.maxTurns,
|
|
3935
|
+
maxMs: config.maxMs,
|
|
3936
|
+
...config.maxOutputTokens ? { maxOutputTokens: config.maxOutputTokens } : {}
|
|
2869
3937
|
};
|
|
2870
3938
|
}
|
|
3939
|
+
return result;
|
|
2871
3940
|
}
|
|
2872
3941
|
function registeredFromSnapshot(events) {
|
|
2873
3942
|
return events.filter((event) => Boolean(event.anchorFile)).map((event) => ({
|
|
@@ -2880,13 +3949,6 @@ function registeredFromSnapshot(events) {
|
|
|
2880
3949
|
payloadSchema: event.payloadSchema ?? {}
|
|
2881
3950
|
}));
|
|
2882
3951
|
}
|
|
2883
|
-
function mergeRegistered(existing, added) {
|
|
2884
|
-
const merged = new Map(existing.map((event) => [event.code, event]));
|
|
2885
|
-
for (const event of added) {
|
|
2886
|
-
merged.set(event.code, event);
|
|
2887
|
-
}
|
|
2888
|
-
return [...merged.values()].sort((a, b) => a.code.localeCompare(b.code));
|
|
2889
|
-
}
|
|
2890
3952
|
async function reconcileProgress(repoPath, registered, prior, sink) {
|
|
2891
3953
|
const priorById = new Map((prior?.events ?? []).map((event) => [event.eventId, event]));
|
|
2892
3954
|
const statuses = /* @__PURE__ */ new Map();
|
|
@@ -2913,32 +3975,46 @@ async function reconcileProgress(repoPath, registered, prior, sink) {
|
|
|
2913
3975
|
async function fileContainsWrapper(repoPath, file, eventCode) {
|
|
2914
3976
|
try {
|
|
2915
3977
|
const path = safeRepoPath(repoPath, file);
|
|
2916
|
-
const content = await
|
|
3978
|
+
const content = await readFile4(path, "utf8");
|
|
2917
3979
|
return content.includes(eventWrapperName(eventCode));
|
|
2918
3980
|
} catch {
|
|
2919
3981
|
return false;
|
|
2920
3982
|
}
|
|
2921
3983
|
}
|
|
2922
|
-
async function setupLooksPresent(repoPath, changedFiles2) {
|
|
3984
|
+
async function setupLooksPresent(repoPath, changedFiles2, survey) {
|
|
2923
3985
|
let bindingsFound = false;
|
|
2924
3986
|
let bindingCallFound = false;
|
|
3987
|
+
let identifyFound = false;
|
|
3988
|
+
let resetFound = false;
|
|
3989
|
+
let sdkInitializationFound = false;
|
|
3990
|
+
const contents = /* @__PURE__ */ new Map();
|
|
2925
3991
|
for (const file of changedFiles2) {
|
|
2926
3992
|
try {
|
|
2927
|
-
const content = await
|
|
3993
|
+
const content = await readFile4(safeRepoPath(repoPath, file), "utf8");
|
|
3994
|
+
contents.set(file, content);
|
|
2928
3995
|
const generated = content.includes("Generated by @whisperr/wizard");
|
|
2929
3996
|
bindingsFound ||= generated;
|
|
2930
3997
|
if (!generated) {
|
|
2931
3998
|
bindingCallFound ||= content.includes("bindWhisperr(") || content.includes("WhisperrEvents.bind(");
|
|
3999
|
+
identifyFound ||= /\bidentify\s*\(/.test(content);
|
|
4000
|
+
resetFound ||= /\breset\s*\(/.test(content);
|
|
4001
|
+
sdkInitializationFound ||= /whisperr/i.test(content) && /(initiali[sz]e|configure|client|setup)/i.test(content);
|
|
2932
4002
|
}
|
|
2933
4003
|
} catch {
|
|
2934
4004
|
}
|
|
2935
4005
|
}
|
|
2936
|
-
|
|
4006
|
+
const identifyAnchorsCovered = (survey?.identifyAnchors ?? []).every(
|
|
4007
|
+
(anchor) => /\bidentify\s*\(/.test(contents.get(anchor.file) ?? "")
|
|
4008
|
+
);
|
|
4009
|
+
const logoutAnchorsCovered = (survey?.logoutAnchors ?? []).every(
|
|
4010
|
+
(anchor) => /\breset\s*\(/.test(contents.get(anchor.file) ?? "")
|
|
4011
|
+
);
|
|
4012
|
+
return bindingsFound && bindingCallFound && identifyFound && resetFound && sdkInitializationFound && identifyAnchorsCovered && logoutAnchorsCovered;
|
|
2937
4013
|
}
|
|
2938
4014
|
function safeRepoPath(repoPath, file) {
|
|
2939
|
-
const root =
|
|
2940
|
-
const path =
|
|
2941
|
-
if (path === root || !path.startsWith(root +
|
|
4015
|
+
const root = resolve4(repoPath);
|
|
4016
|
+
const path = resolve4(root, file);
|
|
4017
|
+
if (path === root || !path.startsWith(root + sep4)) {
|
|
2942
4018
|
throw new Error(`unsafe progress file path: ${file}`);
|
|
2943
4019
|
}
|
|
2944
4020
|
return path;
|
|
@@ -2951,7 +4027,7 @@ function buildEvidence(registered, statuses) {
|
|
|
2951
4027
|
status: statuses.get(event.code) ?? "planned"
|
|
2952
4028
|
}));
|
|
2953
4029
|
}
|
|
2954
|
-
function buildReportEvents(registered, statuses) {
|
|
4030
|
+
function buildReportEvents(registered, statuses, failureReasons = /* @__PURE__ */ new Map()) {
|
|
2955
4031
|
return registered.map((event) => {
|
|
2956
4032
|
const status = statuses.get(event.code);
|
|
2957
4033
|
const wired = status !== void 0 && status !== "failed" && status !== "unsupported" && status !== "planned";
|
|
@@ -2959,7 +4035,7 @@ function buildReportEvents(registered, statuses) {
|
|
|
2959
4035
|
event_type: event.code,
|
|
2960
4036
|
status: wired ? "wired" : "skipped",
|
|
2961
4037
|
...wired ? { file: event.anchorFile } : {},
|
|
2962
|
-
...wired ? {} : { reason: status ?? "not integrated" }
|
|
4038
|
+
...wired ? {} : { reason: failureReasons.get(event.code) ?? status ?? "not integrated" }
|
|
2963
4039
|
};
|
|
2964
4040
|
});
|
|
2965
4041
|
}
|
|
@@ -2979,6 +4055,21 @@ function patchFiles(patch) {
|
|
|
2979
4055
|
}
|
|
2980
4056
|
return [...files];
|
|
2981
4057
|
}
|
|
4058
|
+
function patchAddsProductEvents(patch, bindingsFile, events) {
|
|
4059
|
+
const wrappers = events.map((event) => eventWrapperName(event.code));
|
|
4060
|
+
let file = "";
|
|
4061
|
+
for (const line of patch.split("\n")) {
|
|
4062
|
+
const header = /^\+\+\+ b\/(.+)$/.exec(line);
|
|
4063
|
+
if (header?.[1]) {
|
|
4064
|
+
file = header[1];
|
|
4065
|
+
continue;
|
|
4066
|
+
}
|
|
4067
|
+
if (file !== bindingsFile && line.startsWith("+") && !line.startsWith("+++") && wrappers.some((wrapper) => line.includes(wrapper))) {
|
|
4068
|
+
return true;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
return false;
|
|
4072
|
+
}
|
|
2982
4073
|
|
|
2983
4074
|
// src/engine/providers/claude.ts
|
|
2984
4075
|
import { createSdkMcpServer, query as query2, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -2990,9 +4081,9 @@ import {
|
|
|
2990
4081
|
|
|
2991
4082
|
// src/core/git.ts
|
|
2992
4083
|
import { spawn as spawn3 } from "child_process";
|
|
2993
|
-
import { createHash as
|
|
2994
|
-
import { readFile as
|
|
2995
|
-
import { basename as basename2, join as
|
|
4084
|
+
import { createHash as createHash4 } from "crypto";
|
|
4085
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
4086
|
+
import { basename as basename2, join as join6 } from "path";
|
|
2996
4087
|
async function takeCheckpoint(repoPath) {
|
|
2997
4088
|
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
2998
4089
|
if (!isRepo) return { isRepo: false };
|
|
@@ -3041,7 +4132,7 @@ async function revertToCheckpoint(repoPath, checkpoint) {
|
|
|
3041
4132
|
async function repoFingerprint(repoPath) {
|
|
3042
4133
|
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
3043
4134
|
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename2(repoPath)}`;
|
|
3044
|
-
return
|
|
4135
|
+
return createHash4("sha256").update(seed).digest("hex").slice(0, 16);
|
|
3045
4136
|
}
|
|
3046
4137
|
function normalizeRemote(url) {
|
|
3047
4138
|
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
@@ -3058,7 +4149,7 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
|
3058
4149
|
const contents = [];
|
|
3059
4150
|
for (const file of files) {
|
|
3060
4151
|
try {
|
|
3061
|
-
contents.push({ file, content: await
|
|
4152
|
+
contents.push({ file, content: await readFile5(join6(repoPath, file), "utf8") });
|
|
3062
4153
|
} catch {
|
|
3063
4154
|
continue;
|
|
3064
4155
|
}
|
|
@@ -3088,14 +4179,14 @@ function escapeRegExp(s) {
|
|
|
3088
4179
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3089
4180
|
}
|
|
3090
4181
|
function run(cwd, args) {
|
|
3091
|
-
return new Promise((
|
|
4182
|
+
return new Promise((resolve8) => {
|
|
3092
4183
|
const child = spawn3("git", args, { cwd });
|
|
3093
4184
|
let stdout = "";
|
|
3094
4185
|
let stderr = "";
|
|
3095
4186
|
child.stdout.on("data", (d) => stdout += d.toString());
|
|
3096
4187
|
child.stderr.on("data", (d) => stderr += d.toString());
|
|
3097
|
-
child.on("close", (code) =>
|
|
3098
|
-
child.on("error", () =>
|
|
4188
|
+
child.on("close", (code) => resolve8({ ok: code === 0, stdout, stderr }));
|
|
4189
|
+
child.on("error", () => resolve8({ ok: false, stdout, stderr }));
|
|
3099
4190
|
});
|
|
3100
4191
|
}
|
|
3101
4192
|
|
|
@@ -3334,7 +4425,7 @@ function coverageNote(coverage) {
|
|
|
3334
4425
|
}
|
|
3335
4426
|
|
|
3336
4427
|
// src/core/toolPolicy.ts
|
|
3337
|
-
import { isAbsolute, relative as relative2, resolve as
|
|
4428
|
+
import { isAbsolute, relative as relative2, resolve as resolve5 } from "path";
|
|
3338
4429
|
var SECRET_MATERIAL_DENIAL = "blocked: secret material must not be accessed";
|
|
3339
4430
|
var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
|
|
3340
4431
|
var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
|
|
@@ -3430,8 +4521,8 @@ function isSecretPathLike(value) {
|
|
|
3430
4521
|
}
|
|
3431
4522
|
function isPathInsideRepo(pathValue, repoPath) {
|
|
3432
4523
|
if (pathValue.includes("..")) return false;
|
|
3433
|
-
const repoRoot =
|
|
3434
|
-
const candidate = isAbsolute(pathValue) ?
|
|
4524
|
+
const repoRoot = resolve5(repoPath);
|
|
4525
|
+
const candidate = isAbsolute(pathValue) ? resolve5(pathValue) : resolve5(repoRoot, pathValue);
|
|
3435
4526
|
const rel = relative2(repoRoot, candidate);
|
|
3436
4527
|
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
3437
4528
|
}
|
|
@@ -3448,6 +4539,12 @@ function evaluateReadLike(secretValues, repoPathValues, context) {
|
|
|
3448
4539
|
message: "blocked: reads must stay inside the target repository"
|
|
3449
4540
|
};
|
|
3450
4541
|
}
|
|
4542
|
+
if (context.allowedFiles && !allowedPath(value, context)) {
|
|
4543
|
+
return {
|
|
4544
|
+
behavior: "deny",
|
|
4545
|
+
message: "blocked: this task may only access its explicitly supplied repository files"
|
|
4546
|
+
};
|
|
4547
|
+
}
|
|
3451
4548
|
}
|
|
3452
4549
|
return { behavior: "allow" };
|
|
3453
4550
|
}
|
|
@@ -3465,8 +4562,19 @@ function evaluateWrite(input, context) {
|
|
|
3465
4562
|
if (isSensitiveWritePath(pathValue, context.repoPath)) {
|
|
3466
4563
|
return { behavior: "deny", message: SENSITIVE_WRITE_DENIAL };
|
|
3467
4564
|
}
|
|
4565
|
+
if (context.allowedFiles && !allowedPath(pathValue, context)) {
|
|
4566
|
+
return {
|
|
4567
|
+
behavior: "deny",
|
|
4568
|
+
message: "blocked: this task may only edit its explicitly supplied repository files"
|
|
4569
|
+
};
|
|
4570
|
+
}
|
|
3468
4571
|
return { behavior: "allow" };
|
|
3469
4572
|
}
|
|
4573
|
+
function allowedPath(pathValue, context) {
|
|
4574
|
+
if (!context.allowedFiles) return true;
|
|
4575
|
+
const relativePath = repoRelativePath(pathValue, context.repoPath);
|
|
4576
|
+
return context.allowedFiles.has(relativePath);
|
|
4577
|
+
}
|
|
3470
4578
|
function evaluateBash(input) {
|
|
3471
4579
|
const command = firstString(input, ["command"]);
|
|
3472
4580
|
if (!command) {
|
|
@@ -3555,8 +4663,8 @@ function normalizePathLike(value) {
|
|
|
3555
4663
|
return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
|
|
3556
4664
|
}
|
|
3557
4665
|
function repoRelativePath(pathValue, repoPath) {
|
|
3558
|
-
const repoRoot =
|
|
3559
|
-
const candidate = isAbsolute(pathValue) ?
|
|
4666
|
+
const repoRoot = resolve5(repoPath);
|
|
4667
|
+
const candidate = isAbsolute(pathValue) ? resolve5(pathValue) : resolve5(repoRoot, pathValue);
|
|
3560
4668
|
return normalizePathLike(relative2(repoRoot, candidate));
|
|
3561
4669
|
}
|
|
3562
4670
|
function isSensitiveWritePath(pathValue, repoPath) {
|
|
@@ -4423,7 +5531,7 @@ async function runPass(opts) {
|
|
|
4423
5531
|
}
|
|
4424
5532
|
return { summary, costUsd, ok, maxedOut };
|
|
4425
5533
|
}
|
|
4426
|
-
function buildToolPolicyHooks(repoPath, trustedTools) {
|
|
5534
|
+
function buildToolPolicyHooks(repoPath, trustedTools, allowedFiles) {
|
|
4427
5535
|
return {
|
|
4428
5536
|
PreToolUse: [
|
|
4429
5537
|
{
|
|
@@ -4432,7 +5540,8 @@ function buildToolPolicyHooks(repoPath, trustedTools) {
|
|
|
4432
5540
|
if (input.hook_event_name !== "PreToolUse") return { continue: true };
|
|
4433
5541
|
const decision = evaluateToolUse(input.tool_name, toolInputRecord(input.tool_input), {
|
|
4434
5542
|
repoPath,
|
|
4435
|
-
trustedTools
|
|
5543
|
+
trustedTools,
|
|
5544
|
+
allowedFiles
|
|
4436
5545
|
});
|
|
4437
5546
|
if (decision.behavior === "allow") {
|
|
4438
5547
|
return { continue: true };
|
|
@@ -4450,9 +5559,9 @@ function buildToolPolicyHooks(repoPath, trustedTools) {
|
|
|
4450
5559
|
]
|
|
4451
5560
|
};
|
|
4452
5561
|
}
|
|
4453
|
-
function createToolPermissionCallback(repoPath, trustedTools) {
|
|
5562
|
+
function createToolPermissionCallback(repoPath, trustedTools, allowedFiles) {
|
|
4454
5563
|
return async (toolName, input, options) => {
|
|
4455
|
-
const decision = evaluateToolUse(toolName, input, { repoPath, trustedTools });
|
|
5564
|
+
const decision = evaluateToolUse(toolName, input, { repoPath, trustedTools, allowedFiles });
|
|
4456
5565
|
return decision.behavior === "allow" ? { behavior: "allow", toolUseID: options.toolUseID } : {
|
|
4457
5566
|
behavior: "deny",
|
|
4458
5567
|
message: decision.message,
|
|
@@ -4519,14 +5628,19 @@ function short(s, max = 60) {
|
|
|
4519
5628
|
|
|
4520
5629
|
// src/engine/providers/claude.ts
|
|
4521
5630
|
var READ_ONLY_TOOLS2 = ["Read", "Glob", "Grep"];
|
|
4522
|
-
var
|
|
5631
|
+
var CONSTRAINED_EDIT_TOOLS = ["Read", "Edit", "Write"];
|
|
5632
|
+
var SETUP_TOOLS = ["Read", "Edit", "Write", "Bash"];
|
|
4523
5633
|
var FAST_MODE_MODELS = /* @__PURE__ */ new Set(["claude-opus-5", "claude-opus-4-8"]);
|
|
4524
5634
|
function supportsClaudeFastMode(model) {
|
|
4525
5635
|
return FAST_MODE_MODELS.has(model);
|
|
4526
5636
|
}
|
|
4527
5637
|
function createClaudeProvider(config, session) {
|
|
5638
|
+
let runId = "";
|
|
4528
5639
|
return {
|
|
4529
5640
|
name: "claude",
|
|
5641
|
+
onRunReady(readyRunId) {
|
|
5642
|
+
runId = readyRunId;
|
|
5643
|
+
},
|
|
4530
5644
|
async invoke(invocation, roleConfig, onProgress) {
|
|
4531
5645
|
applyModelAuthEnv(config, session);
|
|
4532
5646
|
const hostTools = invocation.tools.map(
|
|
@@ -4536,16 +5650,28 @@ function createClaudeProvider(config, session) {
|
|
|
4536
5650
|
})
|
|
4537
5651
|
);
|
|
4538
5652
|
const trustedTools = trustedEngineMcpTools(invocation.tools);
|
|
4539
|
-
const
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
5653
|
+
const accessMode = invocation.repositoryAccess?.mode ?? (invocation.allowRepoWrite ? "repair" : "survey");
|
|
5654
|
+
const repositoryTools = accessMode === "none" || accessMode === "review" ? [] : accessMode === "survey" ? READ_ONLY_TOOLS2 : accessMode === "sdk_setup" ? SETUP_TOOLS : CONSTRAINED_EDIT_TOOLS;
|
|
5655
|
+
const allowedTools = [...repositoryTools, ...trustedTools];
|
|
5656
|
+
const allowedFiles = invocation.repositoryAccess?.allowedFiles ? new Set(invocation.repositoryAccess.allowedFiles.map((file) => file.replaceAll("\\", "/"))) : void 0;
|
|
4543
5657
|
let sessionId = invocation.resumeSessionId;
|
|
4544
5658
|
let costUsd = 0;
|
|
4545
5659
|
let turns = 0;
|
|
4546
5660
|
const deadline = Date.now() + roleConfig.maxMs;
|
|
4547
5661
|
const outcome = (kind, text = "", error = "") => kind === "completed" ? { kind, text, sessionId, costUsd, turns } : kind === "failed" ? { kind, error, sessionId, costUsd, turns } : { kind, sessionId, costUsd, turns };
|
|
4548
5662
|
try {
|
|
5663
|
+
const task = invocation.task ?? invocation.role;
|
|
5664
|
+
const sessionKey = invocation.sessionKey ?? task;
|
|
5665
|
+
const customHeaders = [
|
|
5666
|
+
process.env.ANTHROPIC_CUSTOM_HEADERS,
|
|
5667
|
+
runId ? `X-Whisperr-Wizard-Run-ID: ${headerValue(runId)}` : "",
|
|
5668
|
+
`X-Whisperr-Wizard-Task: ${headerValue(task)}`,
|
|
5669
|
+
`X-Whisperr-Wizard-Session-Key: ${headerValue(sessionKey)}`,
|
|
5670
|
+
`X-Whisperr-Wizard-Model: ${headerValue(roleConfig.model)}`,
|
|
5671
|
+
`X-Whisperr-Wizard-Effort: ${headerValue(roleConfig.effort ?? "")}`,
|
|
5672
|
+
`X-Whisperr-Wizard-Service-Tier: ${roleConfig.fastMode ? "fast" : "default"}`,
|
|
5673
|
+
`X-Whisperr-Wizard-Fallback: ${invocation.fallback ? "true" : "false"}`
|
|
5674
|
+
].filter(Boolean).join("\n");
|
|
4549
5675
|
const response = query2({
|
|
4550
5676
|
prompt: invocation.userPrompt,
|
|
4551
5677
|
options: {
|
|
@@ -4556,12 +5682,17 @@ function createClaudeProvider(config, session) {
|
|
|
4556
5682
|
...roleConfig.effort ? { effort: roleConfig.effort } : {},
|
|
4557
5683
|
tools: [...allowedTools],
|
|
4558
5684
|
...hostTools.length > 0 ? { mcpServers: { engine: createSdkMcpServer({ name: "engine", tools: hostTools }) } } : {},
|
|
4559
|
-
hooks: buildToolPolicyHooks(invocation.cwd, trustedTools),
|
|
4560
|
-
canUseTool: createToolPermissionCallback(invocation.cwd, trustedTools),
|
|
5685
|
+
hooks: buildToolPolicyHooks(invocation.cwd, trustedTools, allowedFiles),
|
|
5686
|
+
canUseTool: createToolPermissionCallback(invocation.cwd, trustedTools, allowedFiles),
|
|
4561
5687
|
maxTurns: roleConfig.maxTurns,
|
|
4562
|
-
...
|
|
5688
|
+
...roleConfig.maxOutputTokens ? { taskBudget: { total: roleConfig.maxOutputTokens } } : {},
|
|
5689
|
+
...roleConfig.fastMode ?? supportsClaudeFastMode(roleConfig.model) ? { settings: { fastMode: true } } : {},
|
|
4563
5690
|
...invocation.resumeSessionId ? { resume: invocation.resumeSessionId } : {},
|
|
4564
|
-
settingSources: []
|
|
5691
|
+
settingSources: [],
|
|
5692
|
+
env: {
|
|
5693
|
+
...process.env,
|
|
5694
|
+
ANTHROPIC_CUSTOM_HEADERS: customHeaders
|
|
5695
|
+
}
|
|
4565
5696
|
}
|
|
4566
5697
|
});
|
|
4567
5698
|
for await (const raw of response) {
|
|
@@ -4609,30 +5740,37 @@ function createClaudeProvider(config, session) {
|
|
|
4609
5740
|
}
|
|
4610
5741
|
};
|
|
4611
5742
|
}
|
|
5743
|
+
function headerValue(value) {
|
|
5744
|
+
return value.replace(/[\r\n]/g, "").slice(0, 200);
|
|
5745
|
+
}
|
|
4612
5746
|
|
|
4613
5747
|
// src/engine/providers/openai.ts
|
|
4614
|
-
import { readdirSync, readFileSync as
|
|
4615
|
-
import { join as
|
|
5748
|
+
import { readdirSync, readFileSync as readFileSync3, statSync, writeFileSync } from "fs";
|
|
5749
|
+
import { join as join7, resolve as resolve6, sep as sep5 } from "path";
|
|
4616
5750
|
function jailedPath(cwd, candidate) {
|
|
4617
|
-
const resolved =
|
|
4618
|
-
if (resolved !== cwd && !resolved.startsWith(cwd +
|
|
5751
|
+
const resolved = resolve6(cwd, candidate);
|
|
5752
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + sep5)) {
|
|
4619
5753
|
throw new Error(`path ${candidate} escapes the repository`);
|
|
4620
5754
|
}
|
|
4621
5755
|
return resolved;
|
|
4622
5756
|
}
|
|
4623
|
-
function guardedPath(cwd, toolName, candidate) {
|
|
5757
|
+
function guardedPath(cwd, toolName, candidate, allowedFiles) {
|
|
4624
5758
|
const path = jailedPath(cwd, candidate);
|
|
4625
5759
|
const decision = evaluateToolUse(
|
|
4626
5760
|
toolName,
|
|
4627
5761
|
toolName === "Glob" ? { path: candidate, pattern: "*" } : { file_path: candidate },
|
|
4628
|
-
{ repoPath: cwd }
|
|
5762
|
+
{ repoPath: cwd, allowedFiles }
|
|
4629
5763
|
);
|
|
4630
5764
|
if (decision.behavior === "deny") {
|
|
4631
5765
|
throw new Error(decision.message);
|
|
4632
5766
|
}
|
|
4633
5767
|
return path;
|
|
4634
5768
|
}
|
|
4635
|
-
function fileTools(cwd, allowWrite) {
|
|
5769
|
+
function fileTools(cwd, allowWrite, access = { mode: allowWrite ? "repair" : "survey" }) {
|
|
5770
|
+
if (access.mode === "none" || access.mode === "review") {
|
|
5771
|
+
return [];
|
|
5772
|
+
}
|
|
5773
|
+
const allowedFiles = access.allowedFiles ? new Set(access.allowedFiles.map((file) => file.replaceAll("\\", "/"))) : void 0;
|
|
4636
5774
|
const tools = [
|
|
4637
5775
|
{
|
|
4638
5776
|
name: "read_file",
|
|
@@ -4644,8 +5782,8 @@ function fileTools(cwd, allowWrite) {
|
|
|
4644
5782
|
required: ["path"]
|
|
4645
5783
|
},
|
|
4646
5784
|
handler: async (input) => {
|
|
4647
|
-
const path = guardedPath(cwd, "Read", String(input.path ?? ""));
|
|
4648
|
-
const content =
|
|
5785
|
+
const path = guardedPath(cwd, "Read", String(input.path ?? ""), allowedFiles);
|
|
5786
|
+
const content = readFileSync3(path, "utf8");
|
|
4649
5787
|
return { content: content.slice(0, 4e4) };
|
|
4650
5788
|
}
|
|
4651
5789
|
},
|
|
@@ -4659,9 +5797,9 @@ function fileTools(cwd, allowWrite) {
|
|
|
4659
5797
|
required: ["path"]
|
|
4660
5798
|
},
|
|
4661
5799
|
handler: async (input) => {
|
|
4662
|
-
const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."));
|
|
5800
|
+
const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."), allowedFiles);
|
|
4663
5801
|
const entries = readdirSync(target9).filter((entry) => !isSecretPathLike(entry)).map((entry) => {
|
|
4664
|
-
const kind = statSync(
|
|
5802
|
+
const kind = statSync(join7(target9, entry)).isDirectory() ? "dir" : "file";
|
|
4665
5803
|
return `${kind}:${entry}`;
|
|
4666
5804
|
});
|
|
4667
5805
|
return { entries: entries.slice(0, 500) };
|
|
@@ -4680,7 +5818,7 @@ function fileTools(cwd, allowWrite) {
|
|
|
4680
5818
|
required: ["path", "content"]
|
|
4681
5819
|
},
|
|
4682
5820
|
handler: async (input) => {
|
|
4683
|
-
const path = guardedPath(cwd, "Write", String(input.path ?? ""));
|
|
5821
|
+
const path = guardedPath(cwd, "Write", String(input.path ?? ""), allowedFiles);
|
|
4684
5822
|
writeFileSync(path, String(input.content ?? ""));
|
|
4685
5823
|
return { ok: true };
|
|
4686
5824
|
}
|
|
@@ -4699,8 +5837,8 @@ function fileTools(cwd, allowWrite) {
|
|
|
4699
5837
|
required: ["path", "oldText", "newText"]
|
|
4700
5838
|
},
|
|
4701
5839
|
handler: async (input) => {
|
|
4702
|
-
const path = guardedPath(cwd, "Edit", String(input.path ?? ""));
|
|
4703
|
-
const content =
|
|
5840
|
+
const path = guardedPath(cwd, "Edit", String(input.path ?? ""), allowedFiles);
|
|
5841
|
+
const content = readFileSync3(path, "utf8");
|
|
4704
5842
|
const oldText = String(input.oldText ?? "");
|
|
4705
5843
|
if (!content.includes(oldText)) {
|
|
4706
5844
|
return { ok: false, reason: "oldText not found" };
|
|
@@ -4715,8 +5853,9 @@ function fileTools(cwd, allowWrite) {
|
|
|
4715
5853
|
}
|
|
4716
5854
|
function createOpenAIProvider(config, session) {
|
|
4717
5855
|
let runId;
|
|
4718
|
-
|
|
4719
|
-
|
|
5856
|
+
const conversationIds = /* @__PURE__ */ new Map();
|
|
5857
|
+
let legacyConversationId;
|
|
5858
|
+
const gatewayFetch = async (path, body, sessionKey, task) => {
|
|
4720
5859
|
if (!runId) {
|
|
4721
5860
|
throw new Error("OpenAI provider used before the run was created");
|
|
4722
5861
|
}
|
|
@@ -4726,7 +5865,12 @@ function createOpenAIProvider(config, session) {
|
|
|
4726
5865
|
method: "POST",
|
|
4727
5866
|
headers: {
|
|
4728
5867
|
"Content-Type": "application/json",
|
|
4729
|
-
Authorization: `Bearer ${session.token}
|
|
5868
|
+
Authorization: `Bearer ${session.token}`,
|
|
5869
|
+
...sessionKey === "legacy" ? {} : {
|
|
5870
|
+
"X-Whisperr-Wizard-Session-Key": sessionKey,
|
|
5871
|
+
"X-Whisperr-Wizard-Task": task,
|
|
5872
|
+
"X-Whisperr-Wizard-Fallback": sessionKey.startsWith("fallback:") ? "true" : "false"
|
|
5873
|
+
}
|
|
4730
5874
|
},
|
|
4731
5875
|
body: JSON.stringify(body)
|
|
4732
5876
|
}
|
|
@@ -4739,45 +5883,65 @@ function createOpenAIProvider(config, session) {
|
|
|
4739
5883
|
payload = {};
|
|
4740
5884
|
}
|
|
4741
5885
|
if (!response.ok) {
|
|
4742
|
-
const
|
|
5886
|
+
const top = payload;
|
|
5887
|
+
const nested = top.error?.error ?? top.error;
|
|
5888
|
+
const detail = top.message ?? nested?.message ?? text;
|
|
4743
5889
|
const error = new Error(detail || `gateway request failed (${response.status})`);
|
|
4744
|
-
error.code =
|
|
5890
|
+
error.code = top.code ?? nested?.code;
|
|
4745
5891
|
throw error;
|
|
4746
5892
|
}
|
|
4747
5893
|
return payload;
|
|
4748
5894
|
};
|
|
4749
|
-
const ensureConversation = async (resumeId) => {
|
|
4750
|
-
|
|
4751
|
-
|
|
5895
|
+
const ensureConversation = async (sessionKey, task, resumeId) => {
|
|
5896
|
+
const existing = sessionKey === "legacy" ? legacyConversationId : conversationIds.get(sessionKey);
|
|
5897
|
+
if (existing) {
|
|
5898
|
+
return existing;
|
|
4752
5899
|
}
|
|
4753
5900
|
if (resumeId) {
|
|
4754
|
-
|
|
5901
|
+
if (sessionKey === "legacy") {
|
|
5902
|
+
legacyConversationId = resumeId;
|
|
5903
|
+
} else {
|
|
5904
|
+
conversationIds.set(sessionKey, resumeId);
|
|
5905
|
+
}
|
|
4755
5906
|
return resumeId;
|
|
4756
5907
|
}
|
|
4757
5908
|
try {
|
|
4758
|
-
const created = await gatewayFetch("/v1/conversations", {});
|
|
4759
|
-
|
|
5909
|
+
const created = await gatewayFetch("/v1/conversations", {}, sessionKey, task);
|
|
5910
|
+
if (created.id) {
|
|
5911
|
+
if (sessionKey === "legacy") {
|
|
5912
|
+
legacyConversationId = created.id;
|
|
5913
|
+
} else {
|
|
5914
|
+
conversationIds.set(sessionKey, created.id);
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
4760
5917
|
} catch (error) {
|
|
4761
5918
|
if (error.code !== "conversation_already_bound") {
|
|
4762
5919
|
throw error;
|
|
4763
5920
|
}
|
|
4764
5921
|
}
|
|
4765
|
-
|
|
5922
|
+
const createdId = sessionKey === "legacy" ? legacyConversationId : conversationIds.get(sessionKey);
|
|
5923
|
+
if (!createdId) {
|
|
4766
5924
|
throw new Error("wizard run already has a bound conversation; rerun with its resume state");
|
|
4767
5925
|
}
|
|
4768
|
-
return
|
|
5926
|
+
return createdId;
|
|
4769
5927
|
};
|
|
4770
5928
|
return {
|
|
4771
5929
|
name: "openai-gateway",
|
|
4772
5930
|
onRunReady(readyRunId, resumeSessionId) {
|
|
4773
5931
|
runId = readyRunId;
|
|
4774
|
-
|
|
5932
|
+
legacyConversationId = resumeSessionId;
|
|
4775
5933
|
},
|
|
4776
5934
|
async invoke(invocation, roleConfig, onProgress) {
|
|
4777
5935
|
let costUsd = 0;
|
|
4778
5936
|
let turns = 0;
|
|
4779
|
-
const
|
|
4780
|
-
const
|
|
5937
|
+
const task = invocation.task ?? invocation.role;
|
|
5938
|
+
const sessionKey = invocation.sessionKey?.trim() || "legacy";
|
|
5939
|
+
let activeConversationId = sessionKey === "legacy" ? legacyConversationId : conversationIds.get(sessionKey);
|
|
5940
|
+
const outcome = (kind, text = "", error = "") => kind === "completed" ? { kind, text, sessionId: activeConversationId, costUsd, turns } : kind === "failed" ? { kind, error, sessionId: activeConversationId, costUsd, turns } : { kind, sessionId: activeConversationId, costUsd, turns };
|
|
5941
|
+
const allTools = [
|
|
5942
|
+
...invocation.tools,
|
|
5943
|
+
...fileTools(invocation.cwd, invocation.allowRepoWrite, invocation.repositoryAccess)
|
|
5944
|
+
];
|
|
4781
5945
|
const toolsByName = new Map(allTools.map((tool2) => [tool2.name, tool2]));
|
|
4782
5946
|
const toolDefs = allTools.map((tool2) => ({
|
|
4783
5947
|
type: "function",
|
|
@@ -4786,7 +5950,12 @@ function createOpenAIProvider(config, session) {
|
|
|
4786
5950
|
parameters: tool2.jsonSchema
|
|
4787
5951
|
}));
|
|
4788
5952
|
try {
|
|
4789
|
-
const conversation = await ensureConversation(
|
|
5953
|
+
const conversation = await ensureConversation(
|
|
5954
|
+
sessionKey,
|
|
5955
|
+
task,
|
|
5956
|
+
invocation.resumeSessionId
|
|
5957
|
+
);
|
|
5958
|
+
activeConversationId = conversation;
|
|
4790
5959
|
const deadline = Date.now() + roleConfig.maxMs;
|
|
4791
5960
|
let input = [
|
|
4792
5961
|
{ role: "user", content: invocation.userPrompt }
|
|
@@ -4802,8 +5971,10 @@ function createOpenAIProvider(config, session) {
|
|
|
4802
5971
|
conversation,
|
|
4803
5972
|
input,
|
|
4804
5973
|
tools: toolDefs,
|
|
4805
|
-
...roleConfig.effort ? { reasoning: { effort: roleConfig.effort } } : {}
|
|
4806
|
-
|
|
5974
|
+
...roleConfig.effort ? { reasoning: { effort: roleConfig.effort } } : {},
|
|
5975
|
+
...roleConfig.serviceTier ? { service_tier: roleConfig.serviceTier } : {},
|
|
5976
|
+
...roleConfig.maxOutputTokens ? { max_output_tokens: roleConfig.maxOutputTokens } : {}
|
|
5977
|
+
}, sessionKey, task);
|
|
4807
5978
|
const items = response.output ?? [];
|
|
4808
5979
|
const calls = items.filter(
|
|
4809
5980
|
(item) => item.type === "function_call"
|
|
@@ -4849,18 +6020,115 @@ function createOpenAIProvider(config, session) {
|
|
|
4849
6020
|
}
|
|
4850
6021
|
|
|
4851
6022
|
// src/engine/providers/router.ts
|
|
4852
|
-
|
|
4853
|
-
|
|
6023
|
+
var Semaphore = class {
|
|
6024
|
+
constructor(limit) {
|
|
6025
|
+
this.limit = limit;
|
|
6026
|
+
}
|
|
6027
|
+
limit;
|
|
6028
|
+
active = 0;
|
|
6029
|
+
waiting = [];
|
|
6030
|
+
async run(operation) {
|
|
6031
|
+
if (this.active >= this.limit) {
|
|
6032
|
+
await new Promise((resolve8) => this.waiting.push(resolve8));
|
|
6033
|
+
}
|
|
6034
|
+
this.active += 1;
|
|
6035
|
+
try {
|
|
6036
|
+
return await operation();
|
|
6037
|
+
} finally {
|
|
6038
|
+
this.active -= 1;
|
|
6039
|
+
this.waiting.shift()?.();
|
|
6040
|
+
}
|
|
6041
|
+
}
|
|
6042
|
+
};
|
|
6043
|
+
function shouldFallback(outcome) {
|
|
6044
|
+
return outcome.kind !== "completed";
|
|
6045
|
+
}
|
|
6046
|
+
function shouldDisableProvider(outcome) {
|
|
6047
|
+
return outcome.kind === "failed" && classifyFailure(outcome.error) === "transient";
|
|
6048
|
+
}
|
|
6049
|
+
function createRoutedProvider(routes, fallback, fallbackConcurrency = 2) {
|
|
6050
|
+
const normalized = /* @__PURE__ */ new Map();
|
|
6051
|
+
for (const [key, value] of Object.entries(routes)) {
|
|
6052
|
+
normalized.set(
|
|
6053
|
+
key,
|
|
6054
|
+
"provider" in value ? value : { provider: value }
|
|
6055
|
+
);
|
|
6056
|
+
}
|
|
6057
|
+
const providers = /* @__PURE__ */ new Set([
|
|
6058
|
+
fallback,
|
|
6059
|
+
...[...normalized.values()].map((route) => route.provider)
|
|
6060
|
+
]);
|
|
6061
|
+
const semaphores = /* @__PURE__ */ new Map();
|
|
6062
|
+
const disabledRoutes = /* @__PURE__ */ new Set();
|
|
6063
|
+
const semaphoreFor = (key, concurrency) => {
|
|
6064
|
+
let semaphore = semaphores.get(key);
|
|
6065
|
+
if (!semaphore) {
|
|
6066
|
+
semaphore = new Semaphore(concurrency ?? fallbackConcurrency);
|
|
6067
|
+
semaphores.set(key, semaphore);
|
|
6068
|
+
}
|
|
6069
|
+
return semaphore;
|
|
6070
|
+
};
|
|
6071
|
+
const invocationRouteKey = (invocation, config) => `${invocation.task ?? invocation.role}:${config.model}`;
|
|
6072
|
+
const routeFor = (invocation) => normalized.get(invocation.task ?? invocation.role) ?? normalized.get(invocation.role);
|
|
6073
|
+
const call = (provider, concurrency, invocation, config, onProgress, key = invocationRouteKey(invocation, config)) => semaphoreFor(key, concurrency).run(() => provider.invoke(invocation, config, onProgress));
|
|
6074
|
+
const fallbackInvocation = (invocation) => ({
|
|
6075
|
+
...invocation,
|
|
6076
|
+
fallback: true,
|
|
6077
|
+
sessionKey: `fallback:${invocation.sessionKey ?? invocation.task ?? invocation.role}`
|
|
6078
|
+
});
|
|
4854
6079
|
return {
|
|
4855
6080
|
name: "routed",
|
|
4856
6081
|
onRunReady(runId, resumeSessionId) {
|
|
6082
|
+
disabledRoutes.clear();
|
|
4857
6083
|
for (const provider of providers) {
|
|
4858
6084
|
provider.onRunReady?.(runId, resumeSessionId);
|
|
4859
6085
|
}
|
|
4860
6086
|
},
|
|
4861
6087
|
invoke(invocation, config, onProgress) {
|
|
4862
|
-
const
|
|
4863
|
-
|
|
6088
|
+
const route = routeFor(invocation);
|
|
6089
|
+
if (!route) {
|
|
6090
|
+
return call(fallback, fallbackConcurrency, invocation, config, onProgress);
|
|
6091
|
+
}
|
|
6092
|
+
const routeKey = invocationRouteKey(invocation, config);
|
|
6093
|
+
if (disabledRoutes.has(routeKey)) {
|
|
6094
|
+
return call(
|
|
6095
|
+
fallback,
|
|
6096
|
+
fallbackConcurrency,
|
|
6097
|
+
fallbackInvocation(invocation),
|
|
6098
|
+
route.fallbackConfig ?? config,
|
|
6099
|
+
onProgress,
|
|
6100
|
+
`fallback:${routeKey}`
|
|
6101
|
+
);
|
|
6102
|
+
}
|
|
6103
|
+
return call(route.provider, route.concurrency, invocation, config, onProgress).then(
|
|
6104
|
+
async (outcome) => {
|
|
6105
|
+
if (shouldDisableProvider(outcome)) {
|
|
6106
|
+
disabledRoutes.add(routeKey);
|
|
6107
|
+
}
|
|
6108
|
+
if (!invocation.allowRepoWrite && shouldFallback(outcome)) {
|
|
6109
|
+
onProgress(`${route.provider.name} unavailable; continuing with ${fallback.name}`);
|
|
6110
|
+
return call(
|
|
6111
|
+
fallback,
|
|
6112
|
+
fallbackConcurrency,
|
|
6113
|
+
fallbackInvocation(invocation),
|
|
6114
|
+
route.fallbackConfig ?? config,
|
|
6115
|
+
onProgress,
|
|
6116
|
+
`fallback:${routeKey}`
|
|
6117
|
+
);
|
|
6118
|
+
}
|
|
6119
|
+
return outcome;
|
|
6120
|
+
}
|
|
6121
|
+
);
|
|
6122
|
+
},
|
|
6123
|
+
invokeFallback(invocation, config, onProgress) {
|
|
6124
|
+
return call(
|
|
6125
|
+
fallback,
|
|
6126
|
+
fallbackConcurrency,
|
|
6127
|
+
fallbackInvocation(invocation),
|
|
6128
|
+
config,
|
|
6129
|
+
onProgress,
|
|
6130
|
+
`fallback:${invocationRouteKey(invocation, config)}`
|
|
6131
|
+
);
|
|
4864
6132
|
}
|
|
4865
6133
|
};
|
|
4866
6134
|
}
|
|
@@ -4868,26 +6136,86 @@ function createRoutedProvider(routes, fallback) {
|
|
|
4868
6136
|
// src/engine/cli.ts
|
|
4869
6137
|
var MINUTE = 6e4;
|
|
4870
6138
|
function engineModelConfig(config) {
|
|
4871
|
-
const model = (
|
|
6139
|
+
const model = (task, fallback) => process.env[`WHISPERR_WIZARD_ENGINE_${task.toUpperCase()}_MODEL`]?.trim() || fallback;
|
|
6140
|
+
const terra = "gpt-5.6-terra";
|
|
6141
|
+
const sol = "gpt-5.6-sol";
|
|
6142
|
+
const opus = "claude-opus-5";
|
|
6143
|
+
const opusOnly = process.env.WHISPERR_WIZARD_PRESET?.trim() === "claude-default" || config.offline === true;
|
|
6144
|
+
if (opusOnly) {
|
|
6145
|
+
const survey2 = { model: model("survey", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 5 * MINUTE };
|
|
6146
|
+
const design2 = { model: model("design", opus), effort: "medium", fastMode: true, maxTurns: 80, maxMs: 20 * MINUTE };
|
|
6147
|
+
const sdkSetup2 = { model: model("sdk_setup", opus), effort: "medium", fastMode: true, maxTurns: 60, maxMs: 12 * MINUTE };
|
|
6148
|
+
const clusterEdit2 = { model: model("cluster_edit", opus), effort: "medium", fastMode: true, maxTurns: 60, maxMs: 12 * MINUTE };
|
|
6149
|
+
const repair2 = { model: model("repair", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 8 * MINUTE };
|
|
6150
|
+
const review2 = { model: model("review", opus), effort: "medium", fastMode: true, maxTurns: 30, maxMs: 8 * MINUTE };
|
|
6151
|
+
return {
|
|
6152
|
+
survey: survey2,
|
|
6153
|
+
design: design2,
|
|
6154
|
+
edit: sdkSetup2,
|
|
6155
|
+
review: review2,
|
|
6156
|
+
sdk_setup: sdkSetup2,
|
|
6157
|
+
cluster_edit: clusterEdit2,
|
|
6158
|
+
repair: repair2
|
|
6159
|
+
};
|
|
6160
|
+
}
|
|
6161
|
+
const survey = { model: model("survey", opus), effort: "low", fastMode: true, maxTurns: 30, maxMs: 5 * MINUTE };
|
|
6162
|
+
const design = { model: model("design", opus), effort: "low", fastMode: true, maxTurns: 20, maxMs: 5 * MINUTE };
|
|
6163
|
+
const sdkSetup = { model: model("sdk_setup", opus), effort: "low", fastMode: true, maxTurns: 30, maxMs: 6 * MINUTE };
|
|
6164
|
+
const clusterEdit = opusOnly ? { model: model("cluster_edit", opus), effort: "medium", fastMode: true, maxTurns: 60, maxMs: 12 * MINUTE } : {
|
|
6165
|
+
model: model("cluster_edit", terra),
|
|
6166
|
+
effort: "medium",
|
|
6167
|
+
serviceTier: "priority",
|
|
6168
|
+
maxTurns: 20,
|
|
6169
|
+
maxMs: 4 * MINUTE,
|
|
6170
|
+
maxOutputTokens: 8e3
|
|
6171
|
+
};
|
|
6172
|
+
const repair = {
|
|
6173
|
+
model: model("repair", sol),
|
|
6174
|
+
effort: "medium",
|
|
6175
|
+
serviceTier: "priority",
|
|
6176
|
+
maxTurns: 30,
|
|
6177
|
+
maxMs: 8 * MINUTE,
|
|
6178
|
+
maxOutputTokens: 12e3
|
|
6179
|
+
};
|
|
6180
|
+
const review = {
|
|
6181
|
+
model: model("review", sol),
|
|
6182
|
+
effort: "medium",
|
|
6183
|
+
serviceTier: "priority",
|
|
6184
|
+
maxTurns: 8,
|
|
6185
|
+
maxMs: 3 * MINUTE,
|
|
6186
|
+
maxOutputTokens: 4e3
|
|
6187
|
+
};
|
|
4872
6188
|
return {
|
|
4873
|
-
survey
|
|
4874
|
-
design
|
|
4875
|
-
edit:
|
|
4876
|
-
review
|
|
6189
|
+
survey,
|
|
6190
|
+
design,
|
|
6191
|
+
edit: sdkSetup,
|
|
6192
|
+
review,
|
|
6193
|
+
sdk_setup: sdkSetup,
|
|
6194
|
+
cluster_edit: clusterEdit,
|
|
6195
|
+
repair
|
|
4877
6196
|
};
|
|
4878
6197
|
}
|
|
4879
6198
|
function engineProvider(config, session) {
|
|
4880
6199
|
const claude = createClaudeProvider(config, session);
|
|
4881
|
-
const
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
return createRoutedProvider({
|
|
4885
|
-
}
|
|
4886
|
-
|
|
6200
|
+
const models = engineModelConfig(config);
|
|
6201
|
+
const preset = process.env.WHISPERR_WIZARD_PRESET?.trim() ?? "hybrid";
|
|
6202
|
+
if (preset === "claude-default" || config.offline) {
|
|
6203
|
+
return createRoutedProvider({}, claude, 2);
|
|
6204
|
+
}
|
|
6205
|
+
const openai = createOpenAIProvider(config, session);
|
|
6206
|
+
return createRoutedProvider({
|
|
6207
|
+
survey: { provider: claude, concurrency: 3, fallbackConfig: models.repair },
|
|
6208
|
+
design: { provider: claude, concurrency: 3, fallbackConfig: models.repair },
|
|
6209
|
+
sdk_setup: { provider: claude, concurrency: 2, fallbackConfig: models.repair },
|
|
6210
|
+
cluster_edit: { provider: openai, concurrency: 4, fallbackConfig: models.repair },
|
|
6211
|
+
review: { provider: openai, concurrency: 2, fallbackConfig: models.review },
|
|
6212
|
+
repair: { provider: openai, concurrency: 2, fallbackConfig: models.repair }
|
|
6213
|
+
}, openai, 2);
|
|
4887
6214
|
}
|
|
4888
6215
|
async function tryEngineFlow(options) {
|
|
4889
6216
|
const { repoPath, fingerprint, config, session, playbook, theme: theme2 } = options;
|
|
4890
6217
|
const startedAt = Date.now();
|
|
6218
|
+
const models = engineModelConfig(config);
|
|
4891
6219
|
const sink = createProgressSink({
|
|
4892
6220
|
json: false,
|
|
4893
6221
|
isTTY: Boolean(process.stdout.isTTY),
|
|
@@ -4899,16 +6227,13 @@ async function tryEngineFlow(options) {
|
|
|
4899
6227
|
config,
|
|
4900
6228
|
session,
|
|
4901
6229
|
provider: engineProvider(config, session),
|
|
4902
|
-
models
|
|
6230
|
+
models,
|
|
4903
6231
|
target: playbook.target.id,
|
|
4904
6232
|
projectKind: ["node", "python", "php", "go", "express", "django", "fastapi", "laravel", "spring-boot"].includes(
|
|
4905
6233
|
playbook.target.id
|
|
4906
6234
|
) ? "backend" : "frontend",
|
|
4907
6235
|
playbookSystemPrompt: playbook.systemPrompt,
|
|
4908
|
-
sink
|
|
4909
|
-
callbacks: {
|
|
4910
|
-
reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2)
|
|
4911
|
-
}
|
|
6236
|
+
sink
|
|
4912
6237
|
});
|
|
4913
6238
|
if (result.kind === "wrong_mode") {
|
|
4914
6239
|
return { handled: false, exitCode: 0 };
|
|
@@ -4925,11 +6250,18 @@ async function tryEngineFlow(options) {
|
|
|
4925
6250
|
target: playbook.target.id,
|
|
4926
6251
|
repo_fingerprint: fingerprint,
|
|
4927
6252
|
identify_wired: result.identifyWired,
|
|
4928
|
-
verified: null,
|
|
6253
|
+
verified: result.verificationStatus === "passed" ? true : result.verificationStatus === "failed" ? false : null,
|
|
6254
|
+
verification_status: result.verificationStatus,
|
|
4929
6255
|
cost_usd: 0,
|
|
4930
6256
|
duration_ms: Date.now() - startedAt,
|
|
4931
6257
|
summary: result.summary.slice(0, 2e3),
|
|
4932
6258
|
events: result.reportEvents,
|
|
6259
|
+
wizard_version: packageVersion(),
|
|
6260
|
+
planner_model: models.survey.model,
|
|
6261
|
+
editor_model: models.cluster_edit?.model ?? models.edit.model,
|
|
6262
|
+
effort: models.cluster_edit?.effort ?? models.edit.effort,
|
|
6263
|
+
phase_timings: result.phaseTimings,
|
|
6264
|
+
model_routes: result.modelRoutes,
|
|
4933
6265
|
exit_class: result.kind === "completed" ? "engine_completed" : "engine_partial"
|
|
4934
6266
|
});
|
|
4935
6267
|
if (result.applied && result.registered.length > 0) {
|
|
@@ -4942,32 +6274,6 @@ async function tryEngineFlow(options) {
|
|
|
4942
6274
|
}
|
|
4943
6275
|
return { handled: true, exitCode: result.kind === "completed" ? 0 : 1 };
|
|
4944
6276
|
}
|
|
4945
|
-
async function reviewEventsInTerminal(proposed, theme2) {
|
|
4946
|
-
if (proposed.length === 0) {
|
|
4947
|
-
return [];
|
|
4948
|
-
}
|
|
4949
|
-
const lines = proposed.map((event) => {
|
|
4950
|
-
const fields = Object.keys(event.payloadSchema);
|
|
4951
|
-
return [
|
|
4952
|
-
`${theme2.bright(event.code)} \u2014 ${event.name}`,
|
|
4953
|
-
` ${event.reasoning}`,
|
|
4954
|
-
` anchor: ${event.anchorFile}${event.anchorSymbol ? ` (${event.anchorSymbol})` : ""}`,
|
|
4955
|
-
fields.length > 0 ? ` payload: ${fields.join(", ")}` : " payload: none"
|
|
4956
|
-
].join("\n");
|
|
4957
|
-
});
|
|
4958
|
-
p.note(lines.join("\n\n"), `Proposed events (${proposed.length})`);
|
|
4959
|
-
const selection = await p.multiselect({
|
|
4960
|
-
message: "Register and integrate these events? Deselect any you don't want.",
|
|
4961
|
-
options: proposed.map((event) => ({ value: event.code, label: event.code, hint: event.name })),
|
|
4962
|
-
initialValues: proposed.map((event) => event.code),
|
|
4963
|
-
required: false
|
|
4964
|
-
});
|
|
4965
|
-
if (p.isCancel(selection)) {
|
|
4966
|
-
return null;
|
|
4967
|
-
}
|
|
4968
|
-
const chosen = new Set(selection);
|
|
4969
|
-
return proposed.filter((event) => chosen.has(event.code));
|
|
4970
|
-
}
|
|
4971
6277
|
|
|
4972
6278
|
// src/core/opportunities.ts
|
|
4973
6279
|
function normalizeCode(value) {
|
|
@@ -5200,7 +6506,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5200
6506
|
return Promise.resolve({ ran: false, ok: true, output: "" });
|
|
5201
6507
|
}
|
|
5202
6508
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
5203
|
-
return new Promise((
|
|
6509
|
+
return new Promise((resolve8) => {
|
|
5204
6510
|
const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
|
|
5205
6511
|
let out = "";
|
|
5206
6512
|
const append = (d) => {
|
|
@@ -5211,11 +6517,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5211
6517
|
child.stderr?.on("data", append);
|
|
5212
6518
|
const timer = setTimeout(() => {
|
|
5213
6519
|
child.kill("SIGKILL");
|
|
5214
|
-
|
|
6520
|
+
resolve8({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
|
|
5215
6521
|
}, timeoutMs);
|
|
5216
6522
|
child.on("close", (code) => {
|
|
5217
6523
|
clearTimeout(timer);
|
|
5218
|
-
|
|
6524
|
+
resolve8({
|
|
5219
6525
|
ran: true,
|
|
5220
6526
|
ok: code === 0,
|
|
5221
6527
|
command,
|
|
@@ -5227,7 +6533,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5227
6533
|
});
|
|
5228
6534
|
child.on("error", () => {
|
|
5229
6535
|
clearTimeout(timer);
|
|
5230
|
-
|
|
6536
|
+
resolve8({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
|
|
5231
6537
|
});
|
|
5232
6538
|
});
|
|
5233
6539
|
}
|
|
@@ -5236,25 +6542,6 @@ function tail2(s) {
|
|
|
5236
6542
|
return t.length > MAX_OUTPUT ? `\u2026${t.slice(-MAX_OUTPUT)}` : t;
|
|
5237
6543
|
}
|
|
5238
6544
|
|
|
5239
|
-
// src/core/version.ts
|
|
5240
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
5241
|
-
import { dirname as dirname4, join as join7, parse } from "path";
|
|
5242
|
-
import { fileURLToPath } from "url";
|
|
5243
|
-
var PACKAGE_NAME = "@whisperr/wizard";
|
|
5244
|
-
function packageVersion() {
|
|
5245
|
-
let dir = dirname4(fileURLToPath(import.meta.url));
|
|
5246
|
-
const { root } = parse(dir);
|
|
5247
|
-
while (true) {
|
|
5248
|
-
try {
|
|
5249
|
-
const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf8"));
|
|
5250
|
-
if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
|
|
5251
|
-
} catch {
|
|
5252
|
-
}
|
|
5253
|
-
if (dir === root) return "0.0.0";
|
|
5254
|
-
dir = dirname4(dir);
|
|
5255
|
-
}
|
|
5256
|
-
}
|
|
5257
|
-
|
|
5258
6545
|
// src/core/gapreport.ts
|
|
5259
6546
|
function buildGapReport(input) {
|
|
5260
6547
|
const outcomes = new Map(
|
|
@@ -5384,7 +6671,7 @@ function banner() {
|
|
|
5384
6671
|
|
|
5385
6672
|
// src/cli.ts
|
|
5386
6673
|
async function run2(options) {
|
|
5387
|
-
const repoPath =
|
|
6674
|
+
const repoPath = resolve7(options.path ?? process.cwd());
|
|
5388
6675
|
const config = resolveConfig(options);
|
|
5389
6676
|
console.log(banner());
|
|
5390
6677
|
p2.intro(theme.signal("Let's wire Whisperr into your app."));
|
|
@@ -5496,40 +6783,23 @@ Flutter is live today; ${theme.bright(
|
|
|
5496
6783
|
}
|
|
5497
6784
|
const tick = useIntegrationSpinner ? setInterval(updateIntegrationMessage, 1e3) : void 0;
|
|
5498
6785
|
const additions = { proposed: 0, applied: [] };
|
|
5499
|
-
const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
5500
6786
|
const outcome = await runIntegrationAgent({
|
|
5501
6787
|
repoPath,
|
|
5502
6788
|
config,
|
|
5503
6789
|
session,
|
|
5504
6790
|
playbook: chosen.playbook,
|
|
5505
6791
|
manifest,
|
|
5506
|
-
// The plan is
|
|
5507
|
-
//
|
|
5508
|
-
// default run reads it out and keeps going.
|
|
6792
|
+
// The plan is informational. Event selection is frozen automatically, so
|
|
6793
|
+
// integration never pauses at a terminal approval prompt.
|
|
5509
6794
|
async onPlanReady(plan) {
|
|
5510
6795
|
if (useIntegrationSpinner) {
|
|
5511
6796
|
spin.stop(theme.success("Placement plan ready"));
|
|
5512
6797
|
}
|
|
5513
6798
|
p2.note(renderPlacementPlan(plan), "Placement plan");
|
|
5514
|
-
const placeEntries = plan.filter((entry) => entry.decision === "place");
|
|
5515
|
-
let selectedEvents = placeEntries.map((entry) => entry.event);
|
|
5516
|
-
if (config.reviewPlan && interactive && placeEntries.length) {
|
|
5517
|
-
const selection = await p2.multiselect({
|
|
5518
|
-
message: "Wire these events?",
|
|
5519
|
-
options: placeEntries.map((entry) => ({
|
|
5520
|
-
value: entry.event,
|
|
5521
|
-
label: entry.event,
|
|
5522
|
-
hint: `${entry.file ?? "unknown file"}@${entry.anchor ?? "unknown anchor"}`
|
|
5523
|
-
})),
|
|
5524
|
-
initialValues: selectedEvents,
|
|
5525
|
-
required: false
|
|
5526
|
-
});
|
|
5527
|
-
selectedEvents = p2.isCancel(selection) ? [] : selection;
|
|
5528
|
-
}
|
|
5529
6799
|
if (useIntegrationSpinner) {
|
|
5530
6800
|
spin.start(theme.bright("Continuing integration"));
|
|
5531
6801
|
}
|
|
5532
|
-
return
|
|
6802
|
+
return plan;
|
|
5533
6803
|
},
|
|
5534
6804
|
async onOpportunitiesReady(raw) {
|
|
5535
6805
|
if (useIntegrationSpinner) spin.stop(theme.success("Planning done"));
|
|
@@ -6097,12 +7367,6 @@ function formatDuration(ms) {
|
|
|
6097
7367
|
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
6098
7368
|
return `${minutes}m${seconds}s`;
|
|
6099
7369
|
}
|
|
6100
|
-
function filterEventPlan(plan, selectedEvents) {
|
|
6101
|
-
const selected = new Set(selectedEvents);
|
|
6102
|
-
return plan.map(
|
|
6103
|
-
(entry) => entry.decision === "place" && !selected.has(entry.event) ? { ...entry, decision: "skip", reason: "Deselected in plan review." } : entry
|
|
6104
|
-
);
|
|
6105
|
-
}
|
|
6106
7370
|
function renderPlacementPlan(plan) {
|
|
6107
7371
|
return plan.map((entry) => {
|
|
6108
7372
|
if (entry.decision === "place") {
|
|
@@ -6228,7 +7492,6 @@ function printHelp() {
|
|
|
6228
7492
|
` ${theme.bright("Options")}`,
|
|
6229
7493
|
" --offline Use a demo manifest, no account/browser needed",
|
|
6230
7494
|
" --force Proceed without a clean git tree (no safe undo)",
|
|
6231
|
-
" --review-plan Confirm the placement plan before anything is wired",
|
|
6232
7495
|
" --propose-only Send new events/interventions for dashboard approval",
|
|
6233
7496
|
" instead of adding them to your universe now",
|
|
6234
7497
|
" --strict-review Review the finished diff with the deeper planner model",
|