@whisperr/wizard 0.7.3 → 0.7.4
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/dist/index.js +522 -392
- 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 resolve5 } from "path";
|
|
9
9
|
import * as p2 from "@clack/prompts";
|
|
10
10
|
import open2 from "open";
|
|
11
11
|
|
|
@@ -1308,84 +1308,8 @@ function createProgressSink(options) {
|
|
|
1308
1308
|
}
|
|
1309
1309
|
|
|
1310
1310
|
// src/engine/orchestrator.ts
|
|
1311
|
-
import {
|
|
1312
|
-
|
|
1313
|
-
// src/engine/clusters.ts
|
|
1314
|
-
import { createHash } from "crypto";
|
|
1315
|
-
import { dirname } from "path";
|
|
1316
|
-
var MIN_CLUSTER_EVENTS = 3;
|
|
1317
|
-
var MAX_CLUSTER_EVENTS = 8;
|
|
1318
|
-
function clusterIdFor(files) {
|
|
1319
|
-
const digest = createHash("sha256").update([...files].sort().join("\n")).digest("hex");
|
|
1320
|
-
return `cluster_${digest.slice(0, 12)}`;
|
|
1321
|
-
}
|
|
1322
|
-
function deriveClusters(placements) {
|
|
1323
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
1324
|
-
for (const placement of placements) {
|
|
1325
|
-
const existing = byFile.get(placement.file) ?? [];
|
|
1326
|
-
existing.push(placement);
|
|
1327
|
-
byFile.set(placement.file, existing);
|
|
1328
|
-
}
|
|
1329
|
-
const byDirectory = /* @__PURE__ */ new Map();
|
|
1330
|
-
for (const [file, events] of byFile) {
|
|
1331
|
-
const directory = dirname(file);
|
|
1332
|
-
const bucket = byDirectory.get(directory) ?? { files: /* @__PURE__ */ new Set(), events: [] };
|
|
1333
|
-
bucket.files.add(file);
|
|
1334
|
-
bucket.events.push(...events);
|
|
1335
|
-
byDirectory.set(directory, bucket);
|
|
1336
|
-
}
|
|
1337
|
-
const clusters = [];
|
|
1338
|
-
const pending = [];
|
|
1339
|
-
for (const directory of [...byDirectory.keys()].sort()) {
|
|
1340
|
-
const bucket = byDirectory.get(directory);
|
|
1341
|
-
if (bucket.events.length >= MIN_CLUSTER_EVENTS) {
|
|
1342
|
-
pending.push(bucket);
|
|
1343
|
-
continue;
|
|
1344
|
-
}
|
|
1345
|
-
const last = pending[pending.length - 1];
|
|
1346
|
-
if (last && last.events.length + bucket.events.length <= MAX_CLUSTER_EVENTS) {
|
|
1347
|
-
for (const file of bucket.files) last.files.add(file);
|
|
1348
|
-
last.events.push(...bucket.events);
|
|
1349
|
-
} else {
|
|
1350
|
-
pending.push(bucket);
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
for (const bucket of pending) {
|
|
1354
|
-
const events = [...bucket.events].sort((a, b) => a.eventCode.localeCompare(b.eventCode));
|
|
1355
|
-
for (let start = 0; start < events.length; start += MAX_CLUSTER_EVENTS) {
|
|
1356
|
-
const slice = events.slice(start, start + MAX_CLUSTER_EVENTS);
|
|
1357
|
-
const files = [...new Set(slice.map((event) => event.file))].sort();
|
|
1358
|
-
clusters.push({
|
|
1359
|
-
id: clusterIdFor(files.length > 0 ? files : [`slice_${start}`]),
|
|
1360
|
-
files,
|
|
1361
|
-
events: slice,
|
|
1362
|
-
status: "pending",
|
|
1363
|
-
attempts: 0
|
|
1364
|
-
});
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
return clusters;
|
|
1368
|
-
}
|
|
1369
|
-
var TERMINAL_STATUSES = ["reviewed", "failed_resumable", "blocked"];
|
|
1370
|
-
function nextPendingCluster(clusters) {
|
|
1371
|
-
return clusters.find((cluster) => cluster.status === "pending" || cluster.status === "editing");
|
|
1372
|
-
}
|
|
1373
|
-
function clustersComplete(clusters) {
|
|
1374
|
-
return clusters.every((cluster) => TERMINAL_STATUSES.includes(cluster.status));
|
|
1375
|
-
}
|
|
1376
|
-
function transitionCluster(clusters, clusterId, status, note3) {
|
|
1377
|
-
return clusters.map((cluster) => {
|
|
1378
|
-
if (cluster.id !== clusterId) {
|
|
1379
|
-
return cluster;
|
|
1380
|
-
}
|
|
1381
|
-
const attempts = status === "editing" ? cluster.attempts + 1 : cluster.attempts;
|
|
1382
|
-
return { ...cluster, status, attempts, note: note3 ?? cluster.note };
|
|
1383
|
-
});
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// src/engine/integrate.ts
|
|
1387
|
-
import { writeFile } from "fs/promises";
|
|
1388
|
-
import { join as join3 } from "path";
|
|
1311
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1312
|
+
import { basename, resolve as resolve2, sep as sep2 } from "path";
|
|
1389
1313
|
|
|
1390
1314
|
// src/engine/bindings.ts
|
|
1391
1315
|
function bindingsTargetFor(target9) {
|
|
@@ -1582,6 +1506,89 @@ function generateDart(events) {
|
|
|
1582
1506
|
bindInstruction: "Call WhisperrEvents.bind(track) once, immediately after the Whisperr SDK is initialized, forwarding to the SDK's track call."
|
|
1583
1507
|
};
|
|
1584
1508
|
}
|
|
1509
|
+
function bindingsClassName(target9) {
|
|
1510
|
+
return bindingsTargetFor(target9) === "typescript" ? "WhisperrEvents" : "WhisperrEvents";
|
|
1511
|
+
}
|
|
1512
|
+
function eventWrapperName(code) {
|
|
1513
|
+
return `${bindingsClassName("")}.${camelCase(code)}`;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/engine/clusters.ts
|
|
1517
|
+
import { createHash } from "crypto";
|
|
1518
|
+
import { dirname } from "path";
|
|
1519
|
+
var MIN_CLUSTER_EVENTS = 3;
|
|
1520
|
+
var MAX_CLUSTER_EVENTS = 8;
|
|
1521
|
+
function clusterIdFor(files) {
|
|
1522
|
+
const digest = createHash("sha256").update([...files].sort().join("\n")).digest("hex");
|
|
1523
|
+
return `cluster_${digest.slice(0, 12)}`;
|
|
1524
|
+
}
|
|
1525
|
+
function deriveClusters(placements) {
|
|
1526
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1527
|
+
for (const placement of placements) {
|
|
1528
|
+
const existing = byFile.get(placement.file) ?? [];
|
|
1529
|
+
existing.push(placement);
|
|
1530
|
+
byFile.set(placement.file, existing);
|
|
1531
|
+
}
|
|
1532
|
+
const byDirectory = /* @__PURE__ */ new Map();
|
|
1533
|
+
for (const [file, events] of byFile) {
|
|
1534
|
+
const directory = dirname(file);
|
|
1535
|
+
const bucket = byDirectory.get(directory) ?? { files: /* @__PURE__ */ new Set(), events: [] };
|
|
1536
|
+
bucket.files.add(file);
|
|
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));
|
|
1558
|
+
for (let start = 0; start < events.length; start += MAX_CLUSTER_EVENTS) {
|
|
1559
|
+
const slice = events.slice(start, start + MAX_CLUSTER_EVENTS);
|
|
1560
|
+
const files = [...new Set(slice.map((event) => event.file))].sort();
|
|
1561
|
+
clusters.push({
|
|
1562
|
+
id: clusterIdFor(files.length > 0 ? files : [`slice_${start}`]),
|
|
1563
|
+
files,
|
|
1564
|
+
events: slice,
|
|
1565
|
+
status: "pending",
|
|
1566
|
+
attempts: 0
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
return clusters;
|
|
1571
|
+
}
|
|
1572
|
+
var TERMINAL_STATUSES = ["reviewed", "failed_resumable", "blocked"];
|
|
1573
|
+
function nextPendingCluster(clusters) {
|
|
1574
|
+
return clusters.find((cluster) => cluster.status === "pending" || cluster.status === "editing");
|
|
1575
|
+
}
|
|
1576
|
+
function clustersComplete(clusters) {
|
|
1577
|
+
return clusters.every((cluster) => TERMINAL_STATUSES.includes(cluster.status));
|
|
1578
|
+
}
|
|
1579
|
+
function transitionCluster(clusters, clusterId, status, note3) {
|
|
1580
|
+
return clusters.map((cluster) => {
|
|
1581
|
+
if (cluster.id !== clusterId) {
|
|
1582
|
+
return cluster;
|
|
1583
|
+
}
|
|
1584
|
+
const attempts = status === "editing" ? cluster.attempts + 1 : cluster.attempts;
|
|
1585
|
+
return { ...cluster, status, attempts, note: note3 ?? cluster.note };
|
|
1586
|
+
});
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// src/engine/integrate.ts
|
|
1590
|
+
import { writeFile } from "fs/promises";
|
|
1591
|
+
import { join as join3 } from "path";
|
|
1585
1592
|
|
|
1586
1593
|
// src/engine/budgets.ts
|
|
1587
1594
|
var OVERFLOW_PATTERNS = [
|
|
@@ -1666,7 +1673,7 @@ function npmCommand() {
|
|
|
1666
1673
|
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
1667
1674
|
}
|
|
1668
1675
|
function runVerifyStep(cwd, step, timeoutMs = 3e5) {
|
|
1669
|
-
return new Promise((
|
|
1676
|
+
return new Promise((resolve6) => {
|
|
1670
1677
|
const child = spawn(step.command, step.args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
1671
1678
|
let output = "";
|
|
1672
1679
|
const capture = (chunk) => {
|
|
@@ -1681,11 +1688,11 @@ function runVerifyStep(cwd, step, timeoutMs = 3e5) {
|
|
|
1681
1688
|
}, timeoutMs);
|
|
1682
1689
|
child.on("error", (error) => {
|
|
1683
1690
|
clearTimeout(timer);
|
|
1684
|
-
|
|
1691
|
+
resolve6({ ...step, ok: false, output: String(error) });
|
|
1685
1692
|
});
|
|
1686
1693
|
child.on("close", (exitCode) => {
|
|
1687
1694
|
clearTimeout(timer);
|
|
1688
|
-
|
|
1695
|
+
resolve6({ ...step, ok: exitCode === 0, output });
|
|
1689
1696
|
});
|
|
1690
1697
|
});
|
|
1691
1698
|
}
|
|
@@ -1857,6 +1864,7 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
|
1857
1864
|
for (const placement of cluster.events) {
|
|
1858
1865
|
eventStatuses.set(placement.eventCode, "failed");
|
|
1859
1866
|
}
|
|
1867
|
+
await deps.discardChanges();
|
|
1860
1868
|
continue;
|
|
1861
1869
|
}
|
|
1862
1870
|
let statuses = "applied";
|
|
@@ -1896,6 +1904,7 @@ async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
|
1896
1904
|
for (const placement of cluster.events) {
|
|
1897
1905
|
eventStatuses.set(placement.eventCode, finalStatus);
|
|
1898
1906
|
}
|
|
1907
|
+
await deps.onClusterIntegrated(cluster, finalStatus);
|
|
1899
1908
|
}
|
|
1900
1909
|
if (!clustersComplete(clusters)) {
|
|
1901
1910
|
throw new Error("cluster queue did not reach a terminal state");
|
|
@@ -1977,6 +1986,9 @@ var RunsClient = class {
|
|
|
1977
1986
|
patchRun(runId, patch) {
|
|
1978
1987
|
return this.request("PATCH", `/wizard/runs/${runId}`, { ...patch });
|
|
1979
1988
|
}
|
|
1989
|
+
saveProgress(runId, progress) {
|
|
1990
|
+
return this.patchRun(runId, { integrationEvidence: progress });
|
|
1991
|
+
}
|
|
1980
1992
|
writeItem(runId, kind, payload, idempotencyExtra = "") {
|
|
1981
1993
|
const code = typeof payload.code === "string" ? payload.code : JSON.stringify(payload);
|
|
1982
1994
|
return this.request("POST", `/wizard/runs/${runId}/items`, {
|
|
@@ -2029,7 +2041,13 @@ function normalizeRunBootstrap(payload) {
|
|
|
2029
2041
|
target: stringValue(project.target),
|
|
2030
2042
|
kind: stringValue(project.kind)
|
|
2031
2043
|
},
|
|
2032
|
-
run: {
|
|
2044
|
+
run: {
|
|
2045
|
+
id: run3.id,
|
|
2046
|
+
status: stringValue(run3.status),
|
|
2047
|
+
phase: stringValue(run3.phase),
|
|
2048
|
+
...stringValue(run3.modelConversationId) ? { modelConversationId: stringValue(run3.modelConversationId) } : {},
|
|
2049
|
+
...normalizeIntegrationProgress(run3.integrationEvidence) ? { integrationEvidence: normalizeIntegrationProgress(run3.integrationEvidence) } : {}
|
|
2050
|
+
},
|
|
2033
2051
|
resumed: value.resumed === true,
|
|
2034
2052
|
universeMode: stringValue(value.universeMode),
|
|
2035
2053
|
...record(value.onboardingContext) ? { onboardingContext: record(value.onboardingContext) } : {},
|
|
@@ -2041,6 +2059,31 @@ function normalizeRunBootstrap(payload) {
|
|
|
2041
2059
|
ingestion: { apiKey: ingestion.apiKey, baseUrl: ingestion.baseUrl }
|
|
2042
2060
|
};
|
|
2043
2061
|
}
|
|
2062
|
+
function normalizeIntegrationProgress(payload) {
|
|
2063
|
+
const value = record(payload);
|
|
2064
|
+
if (!value || !Array.isArray(value.changedFiles) || !Array.isArray(value.events)) {
|
|
2065
|
+
return void 0;
|
|
2066
|
+
}
|
|
2067
|
+
const events = value.events.map((candidate) => {
|
|
2068
|
+
const event = record(candidate);
|
|
2069
|
+
if (!event || !stringValue(event.eventId) || !stringValue(event.eventCode)) {
|
|
2070
|
+
return void 0;
|
|
2071
|
+
}
|
|
2072
|
+
return {
|
|
2073
|
+
eventId: stringValue(event.eventId),
|
|
2074
|
+
eventCode: stringValue(event.eventCode),
|
|
2075
|
+
...stringValue(event.file) ? { file: stringValue(event.file) } : {},
|
|
2076
|
+
status: stringValue(event.status)
|
|
2077
|
+
};
|
|
2078
|
+
}).filter((event) => event !== void 0);
|
|
2079
|
+
return {
|
|
2080
|
+
changedFiles: value.changedFiles.filter((file) => typeof file === "string"),
|
|
2081
|
+
identifyWired: value.identifyWired === true,
|
|
2082
|
+
verificationStatus: stringValue(value.verificationStatus) || "pending",
|
|
2083
|
+
...stringValue(value.verificationCommand) ? { verificationCommand: stringValue(value.verificationCommand) } : {},
|
|
2084
|
+
events
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2044
2087
|
function record(value) {
|
|
2045
2088
|
return typeof value === "object" && value !== null ? value : void 0;
|
|
2046
2089
|
}
|
|
@@ -2049,7 +2092,7 @@ function stringValue(value) {
|
|
|
2049
2092
|
}
|
|
2050
2093
|
function isSnapshotEvent(value) {
|
|
2051
2094
|
const event = record(value);
|
|
2052
|
-
return !!event && typeof event.id === "string" && typeof event.code === "string" && typeof event.name === "string" && typeof event.reasoning === "string" && typeof event.projectId === "string" && !!record(event.payloadSchema);
|
|
2095
|
+
return !!event && typeof event.id === "string" && typeof event.code === "string" && typeof event.name === "string" && typeof event.reasoning === "string" && typeof event.projectId === "string" && (event.anchorFile === void 0 || typeof event.anchorFile === "string") && (event.anchorSymbol === void 0 || typeof event.anchorSymbol === "string") && !!record(event.payloadSchema);
|
|
2053
2096
|
}
|
|
2054
2097
|
|
|
2055
2098
|
// src/engine/selection.ts
|
|
@@ -2193,9 +2236,14 @@ function validateProposedEvent(input, seen, rejected) {
|
|
|
2193
2236
|
if (rejected.has(code)) {
|
|
2194
2237
|
return { ok: false, reason: `event ${code} was rejected by the user \u2014 do not re-propose it` };
|
|
2195
2238
|
}
|
|
2196
|
-
|
|
2239
|
+
const anchorFile = input.anchorFile.trim().replaceAll("\\", "/");
|
|
2240
|
+
const anchorParts = anchorFile.split("/");
|
|
2241
|
+
if (!input.name.trim() || !input.reasoning.trim() || !anchorFile) {
|
|
2197
2242
|
return { ok: false, reason: "name, reasoning, and anchorFile are required" };
|
|
2198
2243
|
}
|
|
2244
|
+
if (anchorFile.startsWith("/") || /^[a-z]:/i.test(anchorFile) || anchorParts.some((part) => part === "" || part === "." || part === "..")) {
|
|
2245
|
+
return { ok: false, reason: "anchorFile must be a safe repository-relative path" };
|
|
2246
|
+
}
|
|
2199
2247
|
const schema = {};
|
|
2200
2248
|
for (const [field, description] of Object.entries(input.payloadSchema ?? {})) {
|
|
2201
2249
|
const normalizedField = normalizeEventCode(field);
|
|
@@ -2216,7 +2264,7 @@ function validateProposedEvent(input, seen, rejected) {
|
|
|
2216
2264
|
code,
|
|
2217
2265
|
name: input.name.trim(),
|
|
2218
2266
|
reasoning: input.reasoning.trim(),
|
|
2219
|
-
anchorFile
|
|
2267
|
+
anchorFile,
|
|
2220
2268
|
anchorSymbol: input.anchorSymbol?.trim() || void 0,
|
|
2221
2269
|
payloadSchema: schema
|
|
2222
2270
|
}
|
|
@@ -2239,7 +2287,7 @@ async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSess
|
|
|
2239
2287
|
);
|
|
2240
2288
|
return { report: outcome.kind === "completed" ? outcome.text : "", outcome };
|
|
2241
2289
|
}
|
|
2242
|
-
async function runSelection(provider, models, repoPath, bootstrap, surveyReport, rejectedCodes, sink) {
|
|
2290
|
+
async function runSelection(provider, models, repoPath, bootstrap, surveyReport, rejectedCodes, sink, existingEventCodes = bootstrap.snapshot.events.map((event) => event.code)) {
|
|
2243
2291
|
const proposed = [];
|
|
2244
2292
|
const seen = /* @__PURE__ */ new Set();
|
|
2245
2293
|
const rejected = new Set(rejectedCodes);
|
|
@@ -2318,7 +2366,7 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2318
2366
|
userPrompt: selectionUserPrompt(
|
|
2319
2367
|
surveyReport,
|
|
2320
2368
|
renderOnboardingContext(bootstrap.onboardingContext),
|
|
2321
|
-
|
|
2369
|
+
existingEventCodes,
|
|
2322
2370
|
rejectedCodes
|
|
2323
2371
|
),
|
|
2324
2372
|
tools: [proposeTool, finishTool],
|
|
@@ -2340,12 +2388,27 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2340
2388
|
async function registerApprovedEvents(runs, runId, approved, sink) {
|
|
2341
2389
|
const registered = [];
|
|
2342
2390
|
for (const event of approved) {
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2391
|
+
let result;
|
|
2392
|
+
try {
|
|
2393
|
+
result = await runs.writeItem(runId, "event", {
|
|
2394
|
+
code: event.code,
|
|
2395
|
+
name: event.name,
|
|
2396
|
+
reasoning: event.reasoning,
|
|
2397
|
+
anchorFile: event.anchorFile,
|
|
2398
|
+
...event.anchorSymbol ? { anchorSymbol: event.anchorSymbol } : {},
|
|
2399
|
+
...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
|
|
2400
|
+
}, "placement-v2");
|
|
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;
|
|
2409
|
+
}
|
|
2410
|
+
throw error;
|
|
2411
|
+
}
|
|
2349
2412
|
registered.push({ ...event, eventId: result.id || result.item?.id || "" });
|
|
2350
2413
|
sink.emit({ phase: "persisting", file: event.anchorFile, action: `registered ${event.code}` });
|
|
2351
2414
|
}
|
|
@@ -2361,99 +2424,11 @@ function placementsFor(registered) {
|
|
|
2361
2424
|
}));
|
|
2362
2425
|
}
|
|
2363
2426
|
|
|
2364
|
-
// src/engine/state.ts
|
|
2365
|
-
import { createHash as createHash3 } from "crypto";
|
|
2366
|
-
import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
|
|
2367
|
-
import { homedir, tmpdir } from "os";
|
|
2368
|
-
import { join as join4 } from "path";
|
|
2369
|
-
|
|
2370
|
-
// src/engine/types.ts
|
|
2371
|
-
var ENGINE_PHASES = [
|
|
2372
|
-
"authorizing",
|
|
2373
|
-
"surveying",
|
|
2374
|
-
"designing",
|
|
2375
|
-
"persisting",
|
|
2376
|
-
"binding",
|
|
2377
|
-
"integrating",
|
|
2378
|
-
"reviewing",
|
|
2379
|
-
"verifying",
|
|
2380
|
-
"reporting",
|
|
2381
|
-
"completed"
|
|
2382
|
-
];
|
|
2383
|
-
|
|
2384
|
-
// src/engine/state.ts
|
|
2385
|
-
function stateDirectory() {
|
|
2386
|
-
const override = process.env.WHISPERR_WIZARD_STATE_DIR?.trim();
|
|
2387
|
-
const preferred = override || join4(homedir(), ".whisperr", "wizard-runs");
|
|
2388
|
-
if (canCreateStateDirectory(preferred)) {
|
|
2389
|
-
return preferred;
|
|
2390
|
-
}
|
|
2391
|
-
const user = typeof process.getuid === "function" ? process.getuid() : "user";
|
|
2392
|
-
const fallback = join4(tmpdir(), `whisperr-${user}`, "wizard-runs");
|
|
2393
|
-
if (canCreateStateDirectory(fallback)) {
|
|
2394
|
-
return fallback;
|
|
2395
|
-
}
|
|
2396
|
-
throw new Error("Whisperr Wizard could not create a directory for run state");
|
|
2397
|
-
}
|
|
2398
|
-
function stateKey(apiBaseUrl, appId, repoFingerprint2) {
|
|
2399
|
-
return createHash3("sha256").update(`${apiBaseUrl}
|
|
2400
|
-
${appId}
|
|
2401
|
-
${repoFingerprint2}`).digest("hex").slice(0, 24);
|
|
2402
|
-
}
|
|
2403
|
-
function statePath(apiBaseUrl, appId, repoFingerprint2) {
|
|
2404
|
-
return join4(stateDirectory(), `${stateKey(apiBaseUrl, appId, repoFingerprint2)}.json`);
|
|
2405
|
-
}
|
|
2406
|
-
function saveRunState(state) {
|
|
2407
|
-
const directory = stateDirectory();
|
|
2408
|
-
const path = join4(directory, `${stateKey(state.apiBaseUrl, state.appId, state.repoFingerprint)}.json`);
|
|
2409
|
-
const payload = JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2);
|
|
2410
|
-
const temporary = `${path}.tmp`;
|
|
2411
|
-
writeFileSync(temporary, payload, { mode: 384 });
|
|
2412
|
-
renameSync(temporary, path);
|
|
2413
|
-
}
|
|
2414
|
-
function canCreateStateDirectory(directory) {
|
|
2415
|
-
try {
|
|
2416
|
-
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
2417
|
-
return true;
|
|
2418
|
-
} catch {
|
|
2419
|
-
return false;
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
function loadRunState(apiBaseUrl, appId, repoFingerprint2) {
|
|
2423
|
-
const path = statePath(apiBaseUrl, appId, repoFingerprint2);
|
|
2424
|
-
let raw;
|
|
2425
|
-
try {
|
|
2426
|
-
raw = readFileSync2(path, "utf8");
|
|
2427
|
-
} catch {
|
|
2428
|
-
return void 0;
|
|
2429
|
-
}
|
|
2430
|
-
let parsed;
|
|
2431
|
-
try {
|
|
2432
|
-
parsed = JSON.parse(raw);
|
|
2433
|
-
} catch {
|
|
2434
|
-
return void 0;
|
|
2435
|
-
}
|
|
2436
|
-
if (!isEngineRunState(parsed)) {
|
|
2437
|
-
return void 0;
|
|
2438
|
-
}
|
|
2439
|
-
if (parsed.apiBaseUrl !== apiBaseUrl || parsed.appId !== appId || parsed.repoFingerprint !== repoFingerprint2) {
|
|
2440
|
-
return void 0;
|
|
2441
|
-
}
|
|
2442
|
-
return parsed;
|
|
2443
|
-
}
|
|
2444
|
-
function isEngineRunState(value) {
|
|
2445
|
-
if (typeof value !== "object" || value === null) {
|
|
2446
|
-
return false;
|
|
2447
|
-
}
|
|
2448
|
-
const candidate = value;
|
|
2449
|
-
return candidate.version === 1 && typeof candidate.apiBaseUrl === "string" && typeof candidate.appId === "string" && typeof candidate.repoFingerprint === "string" && typeof candidate.runId === "string" && typeof candidate.phase === "string" && ENGINE_PHASES.includes(candidate.phase) && typeof candidate.designComplete === "boolean" && Array.isArray(candidate.tombstonedCodes) && Array.isArray(candidate.clusters) && typeof candidate.providerSessions === "object" && candidate.providerSessions !== null;
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
2427
|
// src/engine/worktree.ts
|
|
2453
2428
|
import { spawn as spawn2 } from "child_process";
|
|
2454
|
-
import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
|
|
2455
|
-
import { tmpdir
|
|
2456
|
-
import { join as
|
|
2429
|
+
import { cp, lstat, mkdir, mkdtemp, rm, writeFile as writeFile2 } from "fs/promises";
|
|
2430
|
+
import { tmpdir } from "os";
|
|
2431
|
+
import { dirname as dirname2, join as join4, relative, resolve, sep } from "path";
|
|
2457
2432
|
var WorktreeError = class extends Error {
|
|
2458
2433
|
constructor(code, message) {
|
|
2459
2434
|
super(message);
|
|
@@ -2463,7 +2438,7 @@ var WorktreeError = class extends Error {
|
|
|
2463
2438
|
code;
|
|
2464
2439
|
};
|
|
2465
2440
|
function git(cwd, args) {
|
|
2466
|
-
return new Promise((
|
|
2441
|
+
return new Promise((resolve6) => {
|
|
2467
2442
|
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2468
2443
|
let stdout = "";
|
|
2469
2444
|
let stderr = "";
|
|
@@ -2473,8 +2448,8 @@ function git(cwd, args) {
|
|
|
2473
2448
|
child.stderr.on("data", (chunk) => {
|
|
2474
2449
|
stderr += String(chunk);
|
|
2475
2450
|
});
|
|
2476
|
-
child.on("error", () =>
|
|
2477
|
-
child.on("close", (exitCode) =>
|
|
2451
|
+
child.on("error", () => resolve6({ ok: false, stdout, stderr: "git is not available" }));
|
|
2452
|
+
child.on("close", (exitCode) => resolve6({ ok: exitCode === 0, stdout, stderr }));
|
|
2478
2453
|
});
|
|
2479
2454
|
}
|
|
2480
2455
|
async function must(cwd, args) {
|
|
@@ -2484,21 +2459,64 @@ async function must(cwd, args) {
|
|
|
2484
2459
|
}
|
|
2485
2460
|
return result.stdout;
|
|
2486
2461
|
}
|
|
2487
|
-
|
|
2462
|
+
function safeRepoFile(repoPath, file) {
|
|
2463
|
+
const path = resolve(repoPath, file);
|
|
2464
|
+
if (path === repoPath || !path.startsWith(repoPath + sep) || relative(repoPath, path).startsWith("..")) {
|
|
2465
|
+
throw new WorktreeError("git_failed", `unsafe progress file path: ${file}`);
|
|
2466
|
+
}
|
|
2467
|
+
return path;
|
|
2468
|
+
}
|
|
2469
|
+
async function dirtyFiles(repoPath) {
|
|
2470
|
+
const tracked = await must(repoPath, ["diff", "--name-only", "-z", "HEAD"]);
|
|
2471
|
+
const untracked = await must(repoPath, ["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
2472
|
+
return [...new Set((tracked + untracked).split("\0").filter(Boolean))];
|
|
2473
|
+
}
|
|
2474
|
+
async function seedWorktree(repoPath, worktreePath, files) {
|
|
2475
|
+
for (const file of files) {
|
|
2476
|
+
const source = safeRepoFile(repoPath, file);
|
|
2477
|
+
const destination = safeRepoFile(worktreePath, file);
|
|
2478
|
+
try {
|
|
2479
|
+
await lstat(source);
|
|
2480
|
+
await mkdir(dirname2(destination), { recursive: true });
|
|
2481
|
+
await cp(source, destination, { recursive: true, force: true });
|
|
2482
|
+
} catch {
|
|
2483
|
+
await rm(destination, { recursive: true, force: true });
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
async function createWorktree(repoPath, force = false, allowedDirtyFiles = []) {
|
|
2488
2488
|
const inside = await git(repoPath, ["rev-parse", "--is-inside-work-tree"]);
|
|
2489
2489
|
if (!inside.ok) {
|
|
2490
2490
|
throw new WorktreeError("not_a_repo", "the target directory is not a git repository");
|
|
2491
2491
|
}
|
|
2492
|
-
const
|
|
2493
|
-
|
|
2492
|
+
const dirty = await dirtyFiles(repoPath);
|
|
2493
|
+
const allowed = new Set(allowedDirtyFiles);
|
|
2494
|
+
const unexpected = dirty.filter((file) => !allowed.has(file));
|
|
2495
|
+
if (unexpected.length > 0 && !force) {
|
|
2494
2496
|
throw new WorktreeError(
|
|
2495
2497
|
"dirty_repo",
|
|
2496
|
-
|
|
2498
|
+
`the repository has unrelated uncommitted changes (${unexpected.slice(0, 3).join(", ")}) \u2014 commit or stash them, or rerun with --force`
|
|
2497
2499
|
);
|
|
2498
2500
|
}
|
|
2499
|
-
|
|
2500
|
-
const path = await mkdtemp(
|
|
2501
|
+
let baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
|
|
2502
|
+
const path = await mkdtemp(join4(tmpdir(), "whisperr-worktree-"));
|
|
2501
2503
|
await must(repoPath, ["worktree", "add", "--detach", "--force", path, "HEAD"]);
|
|
2504
|
+
const seedFiles = force ? dirty : dirty.filter((file) => allowed.has(file));
|
|
2505
|
+
if (seedFiles.length > 0) {
|
|
2506
|
+
await seedWorktree(repoPath, path, seedFiles);
|
|
2507
|
+
await must(path, ["add", "-A"]);
|
|
2508
|
+
await must(path, [
|
|
2509
|
+
"-c",
|
|
2510
|
+
"user.name=Whisperr Wizard",
|
|
2511
|
+
"-c",
|
|
2512
|
+
"user.email=wizard@whisperr.net",
|
|
2513
|
+
"commit",
|
|
2514
|
+
"--no-gpg-sign",
|
|
2515
|
+
"-m",
|
|
2516
|
+
"Whisperr resume checkpoint"
|
|
2517
|
+
]);
|
|
2518
|
+
baseRef = (await must(path, ["rev-parse", "HEAD"])).trim();
|
|
2519
|
+
}
|
|
2502
2520
|
return { path, baseRef, repoPath };
|
|
2503
2521
|
}
|
|
2504
2522
|
async function worktreePatch(handle) {
|
|
@@ -2509,7 +2527,7 @@ async function applyPatchToRepo(handle, patch) {
|
|
|
2509
2527
|
if (patch.trim() === "") {
|
|
2510
2528
|
return;
|
|
2511
2529
|
}
|
|
2512
|
-
await new Promise((
|
|
2530
|
+
await new Promise((resolve6, reject) => {
|
|
2513
2531
|
const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
|
|
2514
2532
|
cwd: handle.repoPath,
|
|
2515
2533
|
stdio: ["pipe", "ignore", "pipe"]
|
|
@@ -2521,7 +2539,7 @@ async function applyPatchToRepo(handle, patch) {
|
|
|
2521
2539
|
child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
|
|
2522
2540
|
child.on("close", (exitCode) => {
|
|
2523
2541
|
if (exitCode === 0) {
|
|
2524
|
-
|
|
2542
|
+
resolve6();
|
|
2525
2543
|
} else {
|
|
2526
2544
|
reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
|
|
2527
2545
|
}
|
|
@@ -2529,13 +2547,27 @@ async function applyPatchToRepo(handle, patch) {
|
|
|
2529
2547
|
child.stdin.end(patch);
|
|
2530
2548
|
});
|
|
2531
2549
|
}
|
|
2532
|
-
async function
|
|
2533
|
-
|
|
2550
|
+
async function commitWorktreeCheckpoint(handle) {
|
|
2551
|
+
await must(handle.path, ["add", "-A"]);
|
|
2552
|
+
const patch = await must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
|
|
2534
2553
|
if (patch.trim() === "") {
|
|
2535
|
-
return
|
|
2554
|
+
return;
|
|
2536
2555
|
}
|
|
2537
|
-
await
|
|
2538
|
-
|
|
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();
|
|
2567
|
+
}
|
|
2568
|
+
async function discardWorktreeChanges(handle) {
|
|
2569
|
+
await must(handle.path, ["reset", "--hard", handle.baseRef]);
|
|
2570
|
+
await must(handle.path, ["clean", "-fd"]);
|
|
2539
2571
|
}
|
|
2540
2572
|
async function removeWorktree(handle) {
|
|
2541
2573
|
await git(handle.repoPath, ["worktree", "remove", "--force", handle.path]);
|
|
@@ -2560,7 +2592,7 @@ async function runEngine(input) {
|
|
|
2560
2592
|
throw error;
|
|
2561
2593
|
}
|
|
2562
2594
|
const runId = bootstrap.run.id;
|
|
2563
|
-
input.provider.onRunReady?.(runId);
|
|
2595
|
+
input.provider.onRunReady?.(runId, bootstrap.run.modelConversationId);
|
|
2564
2596
|
const patchPhase = async (phase, message) => {
|
|
2565
2597
|
try {
|
|
2566
2598
|
await runs.patchRun(runId, { phase, ...message ? { message } : {} });
|
|
@@ -2573,45 +2605,42 @@ async function runEngine(input) {
|
|
|
2573
2605
|
} catch {
|
|
2574
2606
|
}
|
|
2575
2607
|
};
|
|
2576
|
-
let state;
|
|
2577
|
-
try {
|
|
2578
|
-
state = loadRunState(input.config.apiBaseUrl, input.session.appId, input.repoFingerprint) ?? freshState(input, runId);
|
|
2579
|
-
if (state.runId !== runId) {
|
|
2580
|
-
state = freshState(input, runId);
|
|
2581
|
-
}
|
|
2582
|
-
} catch (error) {
|
|
2583
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
2584
|
-
await failRun(`state initialization failed: ${reason}`);
|
|
2585
|
-
return { kind: "aborted", reason: `state initialization failed: ${reason}` };
|
|
2586
|
-
}
|
|
2587
|
-
const persist = () => saveRunState(state);
|
|
2588
2608
|
let registered;
|
|
2589
|
-
|
|
2590
|
-
|
|
2609
|
+
try {
|
|
2610
|
+
const projectEvents = bootstrap.snapshot.events.filter(
|
|
2611
|
+
(event) => event.projectId === bootstrap.project.id
|
|
2612
|
+
);
|
|
2613
|
+
const placed = registeredFromSnapshot(projectEvents);
|
|
2614
|
+
const missingPlacement = projectEvents.filter((event) => !event.anchorFile);
|
|
2615
|
+
if (placed.length === 0 || missingPlacement.length > 0) {
|
|
2616
|
+
input.sink.emit({
|
|
2617
|
+
phase: "surveying",
|
|
2618
|
+
action: projectEvents.length === 0 ? "no saved progress found; starting a fresh survey" : "saved events need placement recovery; verifying the repository"
|
|
2619
|
+
});
|
|
2591
2620
|
await patchPhase("surveying");
|
|
2592
2621
|
const survey = await runSurvey(
|
|
2593
2622
|
input.provider,
|
|
2594
2623
|
input.models,
|
|
2595
2624
|
input.repoPath,
|
|
2596
2625
|
bootstrap,
|
|
2597
|
-
input.sink
|
|
2598
|
-
state.providerSessions.survey
|
|
2626
|
+
input.sink
|
|
2599
2627
|
);
|
|
2600
2628
|
if (survey.outcome.kind !== "completed") {
|
|
2601
2629
|
await failRun(`survey ${survey.outcome.kind}`);
|
|
2602
2630
|
return { kind: "aborted", reason: `survey ${survey.outcome.kind}` };
|
|
2603
2631
|
}
|
|
2604
|
-
state.providerSessions.survey = survey.outcome.sessionId;
|
|
2605
|
-
persist();
|
|
2606
2632
|
await patchPhase("designing");
|
|
2633
|
+
const recoverableCodes = new Set(missingPlacement.map((event) => event.code));
|
|
2634
|
+
const existingCodes = bootstrap.snapshot.events.filter((event) => !recoverableCodes.has(event.code)).map((event) => event.code);
|
|
2607
2635
|
const selection = await runSelection(
|
|
2608
2636
|
input.provider,
|
|
2609
2637
|
input.models,
|
|
2610
2638
|
input.repoPath,
|
|
2611
2639
|
bootstrap,
|
|
2612
2640
|
survey.report,
|
|
2613
|
-
|
|
2614
|
-
input.sink
|
|
2641
|
+
[],
|
|
2642
|
+
input.sink,
|
|
2643
|
+
existingCodes
|
|
2615
2644
|
);
|
|
2616
2645
|
if (!selection.finished) {
|
|
2617
2646
|
const reason = selection.outcome === "completed" ? "selection did not call finish_selection" : `selection ${selection.outcome}`;
|
|
@@ -2631,44 +2660,114 @@ async function runEngine(input) {
|
|
|
2631
2660
|
await failRun("no events accepted for instrumentation");
|
|
2632
2661
|
return { kind: "aborted", reason: "no events accepted for instrumentation" };
|
|
2633
2662
|
}
|
|
2634
|
-
const approvedCodes = new Set(approved.map((event) => event.code));
|
|
2635
|
-
for (const event of selection.proposed) {
|
|
2636
|
-
if (!approvedCodes.has(event.code) && !state.tombstonedCodes.includes(event.code)) {
|
|
2637
|
-
state.tombstonedCodes.push(event.code);
|
|
2638
|
-
}
|
|
2639
|
-
}
|
|
2640
|
-
persist();
|
|
2641
2663
|
await patchPhase("persisting", `registering ${approved.length} events`);
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2664
|
+
const newlyRegistered = await registerApprovedEvents(runs, runId, approved, input.sink);
|
|
2665
|
+
registered = mergeRegistered(placed, newlyRegistered);
|
|
2666
|
+
const unresolved = projectEvents.filter((event) => !registered.some((candidate) => candidate.code === event.code)).map((event) => event.code);
|
|
2667
|
+
if (unresolved.length > 0) {
|
|
2668
|
+
throw new Error(`could not recover placements for: ${unresolved.join(", ")}`);
|
|
2669
|
+
}
|
|
2670
|
+
} else {
|
|
2671
|
+
registered = placed;
|
|
2672
|
+
input.sink.emit({
|
|
2673
|
+
phase: "integrating",
|
|
2674
|
+
action: `found ${registered.length} saved events; verifying current files`
|
|
2675
|
+
});
|
|
2650
2676
|
}
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
|
|
2677
|
+
} catch (error) {
|
|
2678
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2679
|
+
await failRun(`design failed: ${reason}`);
|
|
2680
|
+
return { kind: "aborted", reason: `design failed: ${reason}` };
|
|
2654
2681
|
}
|
|
2655
2682
|
if (registered.length === 0) {
|
|
2656
2683
|
await failRun("no events registered for this project");
|
|
2657
2684
|
return { kind: "aborted", reason: "no events registered for this project" };
|
|
2658
2685
|
}
|
|
2686
|
+
const prior = bootstrap.run.integrationEvidence;
|
|
2687
|
+
const changedFiles2 = new Set(prior?.changedFiles ?? []);
|
|
2688
|
+
const eventStatuses = await reconcileProgress(input.repoPath, registered, prior, input.sink);
|
|
2689
|
+
for (const event of registered) {
|
|
2690
|
+
const status = eventStatuses.get(event.code) ?? "planned";
|
|
2691
|
+
if (status !== "planned" && status !== "failed" && status !== "unsupported") {
|
|
2692
|
+
changedFiles2.add(event.anchorFile);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
let identifyWired = prior?.identifyWired === true && await setupLooksPresent(input.repoPath, changedFiles2);
|
|
2696
|
+
const progress = () => ({
|
|
2697
|
+
changedFiles: [...changedFiles2].sort(),
|
|
2698
|
+
identifyWired,
|
|
2699
|
+
verificationStatus: "pending",
|
|
2700
|
+
events: buildEvidence(registered, eventStatuses)
|
|
2701
|
+
});
|
|
2702
|
+
const saveProgress = async () => {
|
|
2703
|
+
await runs.saveProgress(runId, progress());
|
|
2704
|
+
};
|
|
2705
|
+
try {
|
|
2706
|
+
await saveProgress();
|
|
2707
|
+
} catch (error) {
|
|
2708
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2709
|
+
await failRun(`progress initialization failed: ${reason}`);
|
|
2710
|
+
return { kind: "aborted", reason: `progress initialization failed: ${reason}` };
|
|
2711
|
+
}
|
|
2659
2712
|
await patchPhase("binding");
|
|
2660
2713
|
let worktree;
|
|
2661
2714
|
try {
|
|
2662
|
-
worktree = await createWorktree(
|
|
2715
|
+
worktree = await createWorktree(
|
|
2716
|
+
input.repoPath,
|
|
2717
|
+
input.force ?? false,
|
|
2718
|
+
[...changedFiles2]
|
|
2719
|
+
);
|
|
2663
2720
|
} catch (error) {
|
|
2664
2721
|
const reason = error instanceof Error ? error.message : String(error);
|
|
2665
2722
|
await failRun(reason);
|
|
2666
2723
|
return { kind: "aborted", reason };
|
|
2667
2724
|
}
|
|
2668
|
-
|
|
2669
|
-
|
|
2725
|
+
const checkpoint = async () => {
|
|
2726
|
+
const patch = await worktreePatch(worktree);
|
|
2727
|
+
if (patch.trim() === "") {
|
|
2728
|
+
return [];
|
|
2729
|
+
}
|
|
2730
|
+
const files = patchFiles(patch);
|
|
2731
|
+
for (const file of files) {
|
|
2732
|
+
changedFiles2.add(file);
|
|
2733
|
+
}
|
|
2734
|
+
await saveProgress();
|
|
2735
|
+
await applyPatchToRepo(worktree, patch);
|
|
2736
|
+
await commitWorktreeCheckpoint(worktree);
|
|
2737
|
+
return files;
|
|
2738
|
+
};
|
|
2670
2739
|
try {
|
|
2671
2740
|
const bindings = await writeBindingsModule(worktree, input.target, registered);
|
|
2741
|
+
if (!identifyWired) {
|
|
2742
|
+
const setupCompleted = await runSdkSetupPass({
|
|
2743
|
+
provider: input.provider,
|
|
2744
|
+
models: input.models,
|
|
2745
|
+
worktree,
|
|
2746
|
+
target: input.target,
|
|
2747
|
+
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
2748
|
+
registered,
|
|
2749
|
+
ingestion: bootstrap.ingestion,
|
|
2750
|
+
sink: input.sink,
|
|
2751
|
+
saveClusters: () => {
|
|
2752
|
+
},
|
|
2753
|
+
discardChanges: async () => discardWorktreeChanges(worktree),
|
|
2754
|
+
onClusterIntegrated: async () => {
|
|
2755
|
+
}
|
|
2756
|
+
}, bindings);
|
|
2757
|
+
if (!setupCompleted) {
|
|
2758
|
+
throw new Error("SDK setup did not complete");
|
|
2759
|
+
}
|
|
2760
|
+
await checkpoint();
|
|
2761
|
+
identifyWired = true;
|
|
2762
|
+
await saveProgress();
|
|
2763
|
+
} else {
|
|
2764
|
+
await checkpoint();
|
|
2765
|
+
}
|
|
2766
|
+
const pending = registered.filter((event) => {
|
|
2767
|
+
const status = eventStatuses.get(event.code) ?? "planned";
|
|
2768
|
+
return status === "planned" || status === "failed";
|
|
2769
|
+
});
|
|
2770
|
+
const clusters = deriveClusters(placementsFor(pending));
|
|
2672
2771
|
const deps = {
|
|
2673
2772
|
provider: input.provider,
|
|
2674
2773
|
models: input.models,
|
|
@@ -2678,140 +2777,179 @@ async function runEngine(input) {
|
|
|
2678
2777
|
registered,
|
|
2679
2778
|
ingestion: bootstrap.ingestion,
|
|
2680
2779
|
sink: input.sink,
|
|
2681
|
-
saveClusters: (
|
|
2682
|
-
|
|
2683
|
-
|
|
2780
|
+
saveClusters: () => {
|
|
2781
|
+
},
|
|
2782
|
+
discardChanges: async () => discardWorktreeChanges(worktree),
|
|
2783
|
+
onClusterIntegrated: async (cluster, status) => {
|
|
2784
|
+
const verified = /* @__PURE__ */ new Map();
|
|
2785
|
+
for (const placement of cluster.events) {
|
|
2786
|
+
const present = await fileContainsWrapper(
|
|
2787
|
+
worktree.path,
|
|
2788
|
+
placement.file,
|
|
2789
|
+
placement.eventCode
|
|
2790
|
+
);
|
|
2791
|
+
verified.set(placement.eventCode, present ? status : "failed");
|
|
2792
|
+
}
|
|
2793
|
+
await checkpoint();
|
|
2794
|
+
for (const placement of cluster.events) {
|
|
2795
|
+
eventStatuses.set(
|
|
2796
|
+
placement.eventCode,
|
|
2797
|
+
verified.get(placement.eventCode) ?? "failed"
|
|
2798
|
+
);
|
|
2799
|
+
await saveProgress();
|
|
2800
|
+
input.sink.emit({
|
|
2801
|
+
phase: "integrating",
|
|
2802
|
+
file: placement.file,
|
|
2803
|
+
action: `saved ${placement.eventCode} progress`
|
|
2804
|
+
});
|
|
2805
|
+
}
|
|
2684
2806
|
}
|
|
2685
2807
|
};
|
|
2686
|
-
const identifyWired = await runSdkSetupPass(deps, bindings);
|
|
2687
2808
|
await patchPhase("integrating");
|
|
2688
2809
|
const integration = await runClusterIntegration(
|
|
2689
2810
|
deps,
|
|
2690
|
-
|
|
2811
|
+
clusters,
|
|
2691
2812
|
async () => worktreePatch(worktree)
|
|
2692
2813
|
);
|
|
2693
|
-
|
|
2694
|
-
|
|
2814
|
+
for (const [code, status] of integration.eventStatuses) {
|
|
2815
|
+
if ((eventStatuses.get(code) ?? "planned") === "planned") {
|
|
2816
|
+
eventStatuses.set(code, status);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
await saveProgress();
|
|
2820
|
+
const unfinished = registered.filter((event) => {
|
|
2821
|
+
const status = eventStatuses.get(event.code) ?? "planned";
|
|
2822
|
+
return status === "planned" || status === "failed";
|
|
2823
|
+
});
|
|
2824
|
+
if (unfinished.length > 0) {
|
|
2825
|
+
throw new Error(
|
|
2826
|
+
`${unfinished.length} event${unfinished.length === 1 ? "" : "s"} still need integration`
|
|
2827
|
+
);
|
|
2828
|
+
}
|
|
2829
|
+
await patchPhase("verifying");
|
|
2695
2830
|
const finalVerify = await finalizeIntegration(deps);
|
|
2696
|
-
const patch = await worktreePatch(worktree);
|
|
2697
|
-
const changedFiles2 = patchFiles(patch);
|
|
2698
2831
|
await patchPhase("reporting");
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
runs,
|
|
2702
|
-
runId,
|
|
2703
|
-
evidence,
|
|
2704
|
-
integration.eventStatuses,
|
|
2705
|
-
changedFiles2,
|
|
2832
|
+
await runs.completeRun(runId, {
|
|
2833
|
+
changedFiles: [...changedFiles2].sort(),
|
|
2706
2834
|
identifyWired,
|
|
2707
|
-
finalVerify.status,
|
|
2708
|
-
finalVerify.command
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
const partialPatchPath = applied ? void 0 : await savePartialPatch(worktree, join6(input.repoPath, ".whisperr-wizard.patch"));
|
|
2716
|
-
await removeWorktree(worktree);
|
|
2717
|
-
state.worktreePath = void 0;
|
|
2718
|
-
persist();
|
|
2719
|
-
const hasFailures = [...integration.eventStatuses.values()].some(
|
|
2720
|
-
(status) => status === "failed"
|
|
2721
|
-
);
|
|
2835
|
+
verificationStatus: finalVerify.status,
|
|
2836
|
+
...finalVerify.command ? { verificationCommand: finalVerify.command } : {},
|
|
2837
|
+
events: buildEvidence(registered, eventStatuses)
|
|
2838
|
+
});
|
|
2839
|
+
await removeWorktree(worktree).catch(() => {
|
|
2840
|
+
});
|
|
2841
|
+
const summaryLines = summarize(registered, eventStatuses);
|
|
2842
|
+
const hasFailures = [...eventStatuses.values()].some((status) => status === "failed");
|
|
2722
2843
|
return {
|
|
2723
2844
|
kind: hasFailures ? "partial" : "completed",
|
|
2724
2845
|
runId,
|
|
2725
2846
|
registered,
|
|
2726
|
-
eventStatuses
|
|
2727
|
-
reportEvents: buildReportEvents(registered,
|
|
2728
|
-
changedFiles: changedFiles2,
|
|
2847
|
+
eventStatuses,
|
|
2848
|
+
reportEvents: buildReportEvents(registered, eventStatuses),
|
|
2849
|
+
changedFiles: [...changedFiles2].sort(),
|
|
2729
2850
|
identifyWired,
|
|
2730
|
-
applied,
|
|
2731
|
-
partialPatchPath: partialPatchPath ?? void 0,
|
|
2851
|
+
applied: changedFiles2.size > 0,
|
|
2732
2852
|
summary: summaryLines.join("\n")
|
|
2733
2853
|
};
|
|
2734
2854
|
} catch (error) {
|
|
2735
2855
|
const reason = error instanceof Error ? error.message : String(error);
|
|
2736
2856
|
await failRun(reason);
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
join6(input.repoPath, ".whisperr-wizard.patch")
|
|
2740
|
-
).catch(() => void 0);
|
|
2741
|
-
persist();
|
|
2857
|
+
await removeWorktree(worktree).catch(() => {
|
|
2858
|
+
});
|
|
2742
2859
|
return {
|
|
2743
2860
|
kind: "partial",
|
|
2744
2861
|
runId,
|
|
2745
2862
|
registered,
|
|
2746
|
-
eventStatuses
|
|
2747
|
-
reportEvents: buildReportEvents(registered,
|
|
2748
|
-
changedFiles: [],
|
|
2749
|
-
identifyWired
|
|
2750
|
-
applied:
|
|
2751
|
-
|
|
2752
|
-
summary: `Integration stopped early: ${reason}. Registered events are saved; rerun to resume.`
|
|
2863
|
+
eventStatuses,
|
|
2864
|
+
reportEvents: buildReportEvents(registered, eventStatuses),
|
|
2865
|
+
changedFiles: [...changedFiles2].sort(),
|
|
2866
|
+
identifyWired,
|
|
2867
|
+
applied: changedFiles2.size > 0,
|
|
2868
|
+
summary: `Integration stopped early: ${reason}. Saved event progress will be verified on rerun.`
|
|
2753
2869
|
};
|
|
2754
2870
|
}
|
|
2755
2871
|
}
|
|
2756
|
-
function
|
|
2757
|
-
return {
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
tombstonedCodes: [],
|
|
2767
|
-
clusters: [],
|
|
2768
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2769
|
-
};
|
|
2872
|
+
function registeredFromSnapshot(events) {
|
|
2873
|
+
return events.filter((event) => Boolean(event.anchorFile)).map((event) => ({
|
|
2874
|
+
code: event.code,
|
|
2875
|
+
name: event.name,
|
|
2876
|
+
reasoning: event.reasoning,
|
|
2877
|
+
eventId: event.id,
|
|
2878
|
+
anchorFile: event.anchorFile.replaceAll("\\", "/"),
|
|
2879
|
+
...event.anchorSymbol ? { anchorSymbol: event.anchorSymbol } : {},
|
|
2880
|
+
payloadSchema: event.payloadSchema ?? {}
|
|
2881
|
+
}));
|
|
2770
2882
|
}
|
|
2771
|
-
function
|
|
2772
|
-
const
|
|
2773
|
-
const
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
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
|
+
async function reconcileProgress(repoPath, registered, prior, sink) {
|
|
2891
|
+
const priorById = new Map((prior?.events ?? []).map((event) => [event.eventId, event]));
|
|
2892
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
2893
|
+
for (const event of registered) {
|
|
2894
|
+
const saved = priorById.get(event.eventId);
|
|
2895
|
+
if (saved?.status === "unsupported") {
|
|
2896
|
+
statuses.set(event.code, "unsupported");
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
const present = await fileContainsWrapper(repoPath, event.anchorFile, event.code);
|
|
2900
|
+
const savedIntegrated = saved && saved.status !== "planned" && saved.status !== "failed";
|
|
2901
|
+
const status = present ? savedIntegrated ? saved.status : "applied" : "planned";
|
|
2902
|
+
statuses.set(event.code, status);
|
|
2903
|
+
if (saved && saved.status !== status) {
|
|
2904
|
+
sink.emit({
|
|
2905
|
+
phase: "verifying",
|
|
2906
|
+
file: event.anchorFile,
|
|
2907
|
+
action: present ? `recovered ${event.code} from the current code` : `${event.code} is missing and will be integrated again`
|
|
2788
2908
|
});
|
|
2789
2909
|
}
|
|
2790
2910
|
}
|
|
2791
|
-
return
|
|
2911
|
+
return statuses;
|
|
2792
2912
|
}
|
|
2793
|
-
function
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
const
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
return
|
|
2800
|
-
|
|
2801
|
-
eventCode: event.code,
|
|
2802
|
-
...needsFile && file ? { file } : {},
|
|
2803
|
-
status
|
|
2804
|
-
};
|
|
2805
|
-
});
|
|
2913
|
+
async function fileContainsWrapper(repoPath, file, eventCode) {
|
|
2914
|
+
try {
|
|
2915
|
+
const path = safeRepoPath(repoPath, file);
|
|
2916
|
+
const content = await readFile2(path, "utf8");
|
|
2917
|
+
return content.includes(eventWrapperName(eventCode));
|
|
2918
|
+
} catch {
|
|
2919
|
+
return false;
|
|
2920
|
+
}
|
|
2806
2921
|
}
|
|
2807
|
-
async function
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2922
|
+
async function setupLooksPresent(repoPath, changedFiles2) {
|
|
2923
|
+
let bindingsFound = false;
|
|
2924
|
+
let bindingCallFound = false;
|
|
2925
|
+
for (const file of changedFiles2) {
|
|
2926
|
+
try {
|
|
2927
|
+
const content = await readFile2(safeRepoPath(repoPath, file), "utf8");
|
|
2928
|
+
const generated = content.includes("Generated by @whisperr/wizard");
|
|
2929
|
+
bindingsFound ||= generated;
|
|
2930
|
+
if (!generated) {
|
|
2931
|
+
bindingCallFound ||= content.includes("bindWhisperr(") || content.includes("WhisperrEvents.bind(");
|
|
2932
|
+
}
|
|
2933
|
+
} catch {
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
return bindingsFound && bindingCallFound;
|
|
2937
|
+
}
|
|
2938
|
+
function safeRepoPath(repoPath, file) {
|
|
2939
|
+
const root = resolve2(repoPath);
|
|
2940
|
+
const path = resolve2(root, file);
|
|
2941
|
+
if (path === root || !path.startsWith(root + sep2)) {
|
|
2942
|
+
throw new Error(`unsafe progress file path: ${file}`);
|
|
2943
|
+
}
|
|
2944
|
+
return path;
|
|
2945
|
+
}
|
|
2946
|
+
function buildEvidence(registered, statuses) {
|
|
2947
|
+
return registered.map((event) => ({
|
|
2948
|
+
eventId: event.eventId,
|
|
2949
|
+
eventCode: event.code,
|
|
2950
|
+
file: event.anchorFile,
|
|
2951
|
+
status: statuses.get(event.code) ?? "planned"
|
|
2952
|
+
}));
|
|
2815
2953
|
}
|
|
2816
2954
|
function buildReportEvents(registered, statuses) {
|
|
2817
2955
|
return registered.map((event) => {
|
|
@@ -2852,9 +2990,9 @@ import {
|
|
|
2852
2990
|
|
|
2853
2991
|
// src/core/git.ts
|
|
2854
2992
|
import { spawn as spawn3 } from "child_process";
|
|
2855
|
-
import { createHash as
|
|
2856
|
-
import { readFile as
|
|
2857
|
-
import { basename as basename2, join as
|
|
2993
|
+
import { createHash as createHash3 } from "crypto";
|
|
2994
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2995
|
+
import { basename as basename2, join as join5 } from "path";
|
|
2858
2996
|
async function takeCheckpoint(repoPath) {
|
|
2859
2997
|
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
2860
2998
|
if (!isRepo) return { isRepo: false };
|
|
@@ -2903,7 +3041,7 @@ async function revertToCheckpoint(repoPath, checkpoint) {
|
|
|
2903
3041
|
async function repoFingerprint(repoPath) {
|
|
2904
3042
|
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
2905
3043
|
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename2(repoPath)}`;
|
|
2906
|
-
return
|
|
3044
|
+
return createHash3("sha256").update(seed).digest("hex").slice(0, 16);
|
|
2907
3045
|
}
|
|
2908
3046
|
function normalizeRemote(url) {
|
|
2909
3047
|
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
@@ -2920,7 +3058,7 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
|
2920
3058
|
const contents = [];
|
|
2921
3059
|
for (const file of files) {
|
|
2922
3060
|
try {
|
|
2923
|
-
contents.push({ file, content: await
|
|
3061
|
+
contents.push({ file, content: await readFile3(join5(repoPath, file), "utf8") });
|
|
2924
3062
|
} catch {
|
|
2925
3063
|
continue;
|
|
2926
3064
|
}
|
|
@@ -2950,14 +3088,14 @@ function escapeRegExp(s) {
|
|
|
2950
3088
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2951
3089
|
}
|
|
2952
3090
|
function run(cwd, args) {
|
|
2953
|
-
return new Promise((
|
|
3091
|
+
return new Promise((resolve6) => {
|
|
2954
3092
|
const child = spawn3("git", args, { cwd });
|
|
2955
3093
|
let stdout = "";
|
|
2956
3094
|
let stderr = "";
|
|
2957
3095
|
child.stdout.on("data", (d) => stdout += d.toString());
|
|
2958
3096
|
child.stderr.on("data", (d) => stderr += d.toString());
|
|
2959
|
-
child.on("close", (code) =>
|
|
2960
|
-
child.on("error", () =>
|
|
3097
|
+
child.on("close", (code) => resolve6({ ok: code === 0, stdout, stderr }));
|
|
3098
|
+
child.on("error", () => resolve6({ ok: false, stdout, stderr }));
|
|
2961
3099
|
});
|
|
2962
3100
|
}
|
|
2963
3101
|
|
|
@@ -3196,7 +3334,7 @@ function coverageNote(coverage) {
|
|
|
3196
3334
|
}
|
|
3197
3335
|
|
|
3198
3336
|
// src/core/toolPolicy.ts
|
|
3199
|
-
import { isAbsolute, relative, resolve } from "path";
|
|
3337
|
+
import { isAbsolute, relative as relative2, resolve as resolve3 } from "path";
|
|
3200
3338
|
var SECRET_MATERIAL_DENIAL = "blocked: secret material must not be accessed";
|
|
3201
3339
|
var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
|
|
3202
3340
|
var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
|
|
@@ -3292,9 +3430,9 @@ function isSecretPathLike(value) {
|
|
|
3292
3430
|
}
|
|
3293
3431
|
function isPathInsideRepo(pathValue, repoPath) {
|
|
3294
3432
|
if (pathValue.includes("..")) return false;
|
|
3295
|
-
const repoRoot =
|
|
3296
|
-
const candidate = isAbsolute(pathValue) ?
|
|
3297
|
-
const rel =
|
|
3433
|
+
const repoRoot = resolve3(repoPath);
|
|
3434
|
+
const candidate = isAbsolute(pathValue) ? resolve3(pathValue) : resolve3(repoRoot, pathValue);
|
|
3435
|
+
const rel = relative2(repoRoot, candidate);
|
|
3298
3436
|
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
3299
3437
|
}
|
|
3300
3438
|
function evaluateReadLike(secretValues, repoPathValues, context) {
|
|
@@ -3417,9 +3555,9 @@ function normalizePathLike(value) {
|
|
|
3417
3555
|
return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
|
|
3418
3556
|
}
|
|
3419
3557
|
function repoRelativePath(pathValue, repoPath) {
|
|
3420
|
-
const repoRoot =
|
|
3421
|
-
const candidate = isAbsolute(pathValue) ?
|
|
3422
|
-
return normalizePathLike(
|
|
3558
|
+
const repoRoot = resolve3(repoPath);
|
|
3559
|
+
const candidate = isAbsolute(pathValue) ? resolve3(pathValue) : resolve3(repoRoot, pathValue);
|
|
3560
|
+
return normalizePathLike(relative2(repoRoot, candidate));
|
|
3423
3561
|
}
|
|
3424
3562
|
function isSensitiveWritePath(pathValue, repoPath) {
|
|
3425
3563
|
const parts = repoRelativePath(pathValue, repoPath).split("/").map((part) => stripOuterQuotes(part.trim().toLowerCase())).filter(Boolean);
|
|
@@ -4473,11 +4611,11 @@ function createClaudeProvider(config, session) {
|
|
|
4473
4611
|
}
|
|
4474
4612
|
|
|
4475
4613
|
// src/engine/providers/openai.ts
|
|
4476
|
-
import { readdirSync, readFileSync as
|
|
4477
|
-
import { join as
|
|
4614
|
+
import { readdirSync, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
|
|
4615
|
+
import { join as join6, resolve as resolve4, sep as sep3 } from "path";
|
|
4478
4616
|
function jailedPath(cwd, candidate) {
|
|
4479
|
-
const resolved =
|
|
4480
|
-
if (resolved !== cwd && !resolved.startsWith(cwd +
|
|
4617
|
+
const resolved = resolve4(cwd, candidate);
|
|
4618
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + sep3)) {
|
|
4481
4619
|
throw new Error(`path ${candidate} escapes the repository`);
|
|
4482
4620
|
}
|
|
4483
4621
|
return resolved;
|
|
@@ -4507,7 +4645,7 @@ function fileTools(cwd, allowWrite) {
|
|
|
4507
4645
|
},
|
|
4508
4646
|
handler: async (input) => {
|
|
4509
4647
|
const path = guardedPath(cwd, "Read", String(input.path ?? ""));
|
|
4510
|
-
const content =
|
|
4648
|
+
const content = readFileSync2(path, "utf8");
|
|
4511
4649
|
return { content: content.slice(0, 4e4) };
|
|
4512
4650
|
}
|
|
4513
4651
|
},
|
|
@@ -4523,7 +4661,7 @@ function fileTools(cwd, allowWrite) {
|
|
|
4523
4661
|
handler: async (input) => {
|
|
4524
4662
|
const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."));
|
|
4525
4663
|
const entries = readdirSync(target9).filter((entry) => !isSecretPathLike(entry)).map((entry) => {
|
|
4526
|
-
const kind = statSync(
|
|
4664
|
+
const kind = statSync(join6(target9, entry)).isDirectory() ? "dir" : "file";
|
|
4527
4665
|
return `${kind}:${entry}`;
|
|
4528
4666
|
});
|
|
4529
4667
|
return { entries: entries.slice(0, 500) };
|
|
@@ -4543,7 +4681,7 @@ function fileTools(cwd, allowWrite) {
|
|
|
4543
4681
|
},
|
|
4544
4682
|
handler: async (input) => {
|
|
4545
4683
|
const path = guardedPath(cwd, "Write", String(input.path ?? ""));
|
|
4546
|
-
|
|
4684
|
+
writeFileSync(path, String(input.content ?? ""));
|
|
4547
4685
|
return { ok: true };
|
|
4548
4686
|
}
|
|
4549
4687
|
},
|
|
@@ -4562,12 +4700,12 @@ function fileTools(cwd, allowWrite) {
|
|
|
4562
4700
|
},
|
|
4563
4701
|
handler: async (input) => {
|
|
4564
4702
|
const path = guardedPath(cwd, "Edit", String(input.path ?? ""));
|
|
4565
|
-
const content =
|
|
4703
|
+
const content = readFileSync2(path, "utf8");
|
|
4566
4704
|
const oldText = String(input.oldText ?? "");
|
|
4567
4705
|
if (!content.includes(oldText)) {
|
|
4568
4706
|
return { ok: false, reason: "oldText not found" };
|
|
4569
4707
|
}
|
|
4570
|
-
|
|
4708
|
+
writeFileSync(path, content.replace(oldText, String(input.newText ?? "")));
|
|
4571
4709
|
return { ok: true };
|
|
4572
4710
|
}
|
|
4573
4711
|
}
|
|
@@ -4631,8 +4769,9 @@ function createOpenAIProvider(config, session) {
|
|
|
4631
4769
|
};
|
|
4632
4770
|
return {
|
|
4633
4771
|
name: "openai-gateway",
|
|
4634
|
-
onRunReady(readyRunId) {
|
|
4772
|
+
onRunReady(readyRunId, resumeSessionId) {
|
|
4635
4773
|
runId = readyRunId;
|
|
4774
|
+
conversationId = resumeSessionId;
|
|
4636
4775
|
},
|
|
4637
4776
|
async invoke(invocation, roleConfig, onProgress) {
|
|
4638
4777
|
let costUsd = 0;
|
|
@@ -4714,9 +4853,9 @@ function createRoutedProvider(routes, fallback) {
|
|
|
4714
4853
|
const providers = /* @__PURE__ */ new Set([fallback, ...Object.values(routes)]);
|
|
4715
4854
|
return {
|
|
4716
4855
|
name: "routed",
|
|
4717
|
-
onRunReady(runId) {
|
|
4856
|
+
onRunReady(runId, resumeSessionId) {
|
|
4718
4857
|
for (const provider of providers) {
|
|
4719
|
-
provider.onRunReady?.(runId);
|
|
4858
|
+
provider.onRunReady?.(runId, resumeSessionId);
|
|
4720
4859
|
}
|
|
4721
4860
|
},
|
|
4722
4861
|
invoke(invocation, config, onProgress) {
|
|
@@ -4768,16 +4907,7 @@ async function tryEngineFlow(options) {
|
|
|
4768
4907
|
playbookSystemPrompt: playbook.systemPrompt,
|
|
4769
4908
|
sink,
|
|
4770
4909
|
callbacks: {
|
|
4771
|
-
reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2)
|
|
4772
|
-
confirmApply: async (patch, summaryLines) => {
|
|
4773
|
-
p.note(summaryLines.join("\n"), "Integration result");
|
|
4774
|
-
const lineCount = patch.split("\n").length;
|
|
4775
|
-
const answer = await p.confirm({
|
|
4776
|
-
message: `Apply these changes to your working tree? (${lineCount} patch lines)`,
|
|
4777
|
-
initialValue: true
|
|
4778
|
-
});
|
|
4779
|
-
return answer === true;
|
|
4780
|
-
}
|
|
4910
|
+
reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2)
|
|
4781
4911
|
}
|
|
4782
4912
|
});
|
|
4783
4913
|
if (result.kind === "wrong_mode") {
|
|
@@ -4827,7 +4957,7 @@ async function reviewEventsInTerminal(proposed, theme2) {
|
|
|
4827
4957
|
});
|
|
4828
4958
|
p.note(lines.join("\n\n"), `Proposed events (${proposed.length})`);
|
|
4829
4959
|
const selection = await p.multiselect({
|
|
4830
|
-
message: "Register these events? Deselect any you don't want.",
|
|
4960
|
+
message: "Register and integrate these events? Deselect any you don't want.",
|
|
4831
4961
|
options: proposed.map((event) => ({ value: event.code, label: event.code, hint: event.name })),
|
|
4832
4962
|
initialValues: proposed.map((event) => event.code),
|
|
4833
4963
|
required: false
|
|
@@ -5070,7 +5200,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5070
5200
|
return Promise.resolve({ ran: false, ok: true, output: "" });
|
|
5071
5201
|
}
|
|
5072
5202
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
5073
|
-
return new Promise((
|
|
5203
|
+
return new Promise((resolve6) => {
|
|
5074
5204
|
const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
|
|
5075
5205
|
let out = "";
|
|
5076
5206
|
const append = (d) => {
|
|
@@ -5081,11 +5211,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5081
5211
|
child.stderr?.on("data", append);
|
|
5082
5212
|
const timer = setTimeout(() => {
|
|
5083
5213
|
child.kill("SIGKILL");
|
|
5084
|
-
|
|
5214
|
+
resolve6({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
|
|
5085
5215
|
}, timeoutMs);
|
|
5086
5216
|
child.on("close", (code) => {
|
|
5087
5217
|
clearTimeout(timer);
|
|
5088
|
-
|
|
5218
|
+
resolve6({
|
|
5089
5219
|
ran: true,
|
|
5090
5220
|
ok: code === 0,
|
|
5091
5221
|
command,
|
|
@@ -5097,7 +5227,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5097
5227
|
});
|
|
5098
5228
|
child.on("error", () => {
|
|
5099
5229
|
clearTimeout(timer);
|
|
5100
|
-
|
|
5230
|
+
resolve6({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
|
|
5101
5231
|
});
|
|
5102
5232
|
});
|
|
5103
5233
|
}
|
|
@@ -5107,21 +5237,21 @@ function tail2(s) {
|
|
|
5107
5237
|
}
|
|
5108
5238
|
|
|
5109
5239
|
// src/core/version.ts
|
|
5110
|
-
import { readFileSync as
|
|
5111
|
-
import { dirname as
|
|
5240
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
5241
|
+
import { dirname as dirname4, join as join7, parse } from "path";
|
|
5112
5242
|
import { fileURLToPath } from "url";
|
|
5113
5243
|
var PACKAGE_NAME = "@whisperr/wizard";
|
|
5114
5244
|
function packageVersion() {
|
|
5115
|
-
let dir =
|
|
5245
|
+
let dir = dirname4(fileURLToPath(import.meta.url));
|
|
5116
5246
|
const { root } = parse(dir);
|
|
5117
5247
|
while (true) {
|
|
5118
5248
|
try {
|
|
5119
|
-
const pkg = JSON.parse(
|
|
5249
|
+
const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf8"));
|
|
5120
5250
|
if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
|
|
5121
5251
|
} catch {
|
|
5122
5252
|
}
|
|
5123
5253
|
if (dir === root) return "0.0.0";
|
|
5124
|
-
dir =
|
|
5254
|
+
dir = dirname4(dir);
|
|
5125
5255
|
}
|
|
5126
5256
|
}
|
|
5127
5257
|
|
|
@@ -5254,7 +5384,7 @@ function banner() {
|
|
|
5254
5384
|
|
|
5255
5385
|
// src/cli.ts
|
|
5256
5386
|
async function run2(options) {
|
|
5257
|
-
const repoPath =
|
|
5387
|
+
const repoPath = resolve5(options.path ?? process.cwd());
|
|
5258
5388
|
const config = resolveConfig(options);
|
|
5259
5389
|
console.log(banner());
|
|
5260
5390
|
p2.intro(theme.signal("Let's wire Whisperr into your app."));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whisperr/wizard",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Whisperr Wizard \u2014 one command to integrate the Whisperr SDK into your app. Authenticates with your onboarded account and uses an AI coding agent to wire up identify() and your business events automatically.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|