@whisperr/wizard 0.7.2 → 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 +589 -407
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,13 +5,13 @@ 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
|
|
|
12
12
|
// src/core/config.ts
|
|
13
13
|
var DEFAULT_API_BASE = "https://api.whisperr.net";
|
|
14
|
-
var DEFAULT_MODEL = "claude-
|
|
14
|
+
var DEFAULT_MODEL = "claude-opus-5";
|
|
15
15
|
var DEFAULT_PLANNER_MODEL = "claude-opus-5";
|
|
16
16
|
var DEFAULT_EFFORT = "high";
|
|
17
17
|
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
@@ -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
|
|
@@ -2063,11 +2106,14 @@ var SURVEY_SYSTEM_PROMPT = [
|
|
|
2063
2106
|
"",
|
|
2064
2107
|
"Report, in plain markdown:",
|
|
2065
2108
|
"1. Stack: frameworks, package manager, app entry point file.",
|
|
2066
|
-
"2. Feature areas: name, root directory, and
|
|
2109
|
+
"2. Feature areas: name, root directory, and every relevant file containing",
|
|
2110
|
+
" committed end-user actions or meaningful lifecycle transitions.",
|
|
2067
2111
|
"3. The end-user identify() anchor: the login/signup success and session-restore call sites for the",
|
|
2068
2112
|
" PAYING end user \u2014 never admin, staff, operator, or seller paths.",
|
|
2069
2113
|
"4. Existing analytics calls, if any.",
|
|
2070
|
-
"5.
|
|
2114
|
+
"5. Exhaustive inventory of integratable call sites: file, function/symbol,",
|
|
2115
|
+
" confirmed outcome or state transition, available non-PII payload fields,",
|
|
2116
|
+
" and whether similar paths exist elsewhere.",
|
|
2071
2117
|
"Cite concrete file paths for every claim. Do not propose event names yet."
|
|
2072
2118
|
].join("\n");
|
|
2073
2119
|
function surveyUserPrompt(repoPath, onboardingContext) {
|
|
@@ -2090,9 +2136,20 @@ var SELECTION_SYSTEM_PROMPT = [
|
|
|
2090
2136
|
" streak_broken) \u2014 the server derives those later.",
|
|
2091
2137
|
"- Emit after confirmed outcomes, not intent: a purchase event fires on confirmed success, not on",
|
|
2092
2138
|
" button tap; denied permissions are not grants.",
|
|
2093
|
-
"-
|
|
2094
|
-
"
|
|
2095
|
-
"
|
|
2139
|
+
"- Aim for comprehensive coverage of the product\u2019s observable end-user lifecycle.",
|
|
2140
|
+
" Inspect every meaningful feature area and propose every distinct, concretely",
|
|
2141
|
+
" integratable event that could contribute to activation, engagement, retention,",
|
|
2142
|
+
" monetization, or churn analysis.",
|
|
2143
|
+
"- Prefer complete coverage over a short curated list. Include distinct successful",
|
|
2144
|
+
" outcomes, failures, cancellations, retries, state transitions, and meaningful",
|
|
2145
|
+
" feature usage when each has its own concrete call site and analytical value.",
|
|
2146
|
+
"- Let the product\u2019s actual complexity determine the count. A feature-rich product",
|
|
2147
|
+
" may legitimately produce roughly 100\u2013200 events, while a smaller product may",
|
|
2148
|
+
" produce fewer. Do not stop after finding only the most obvious events, and do",
|
|
2149
|
+
" not invent events merely to reach a number.",
|
|
2150
|
+
"- Before finishing, systematically revisit every surveyed feature area and confirm",
|
|
2151
|
+
" that all meaningful committed actions and lifecycle transitions have either",
|
|
2152
|
+
" been proposed or deliberately excluded with a reason.",
|
|
2096
2153
|
"- Codes are lowercase snake_case, named for what happened (checkout_completed, not do_checkout).",
|
|
2097
2154
|
"- payloadSchema lists the fields worth capturing with a short description of each \u2014 the important",
|
|
2098
2155
|
" info only. NEVER include personal identifiers: no email, phone, names, addresses, birth dates,",
|
|
@@ -2179,9 +2236,14 @@ function validateProposedEvent(input, seen, rejected) {
|
|
|
2179
2236
|
if (rejected.has(code)) {
|
|
2180
2237
|
return { ok: false, reason: `event ${code} was rejected by the user \u2014 do not re-propose it` };
|
|
2181
2238
|
}
|
|
2182
|
-
|
|
2239
|
+
const anchorFile = input.anchorFile.trim().replaceAll("\\", "/");
|
|
2240
|
+
const anchorParts = anchorFile.split("/");
|
|
2241
|
+
if (!input.name.trim() || !input.reasoning.trim() || !anchorFile) {
|
|
2183
2242
|
return { ok: false, reason: "name, reasoning, and anchorFile are required" };
|
|
2184
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
|
+
}
|
|
2185
2247
|
const schema = {};
|
|
2186
2248
|
for (const [field, description] of Object.entries(input.payloadSchema ?? {})) {
|
|
2187
2249
|
const normalizedField = normalizeEventCode(field);
|
|
@@ -2202,7 +2264,7 @@ function validateProposedEvent(input, seen, rejected) {
|
|
|
2202
2264
|
code,
|
|
2203
2265
|
name: input.name.trim(),
|
|
2204
2266
|
reasoning: input.reasoning.trim(),
|
|
2205
|
-
anchorFile
|
|
2267
|
+
anchorFile,
|
|
2206
2268
|
anchorSymbol: input.anchorSymbol?.trim() || void 0,
|
|
2207
2269
|
payloadSchema: schema
|
|
2208
2270
|
}
|
|
@@ -2225,7 +2287,7 @@ async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSess
|
|
|
2225
2287
|
);
|
|
2226
2288
|
return { report: outcome.kind === "completed" ? outcome.text : "", outcome };
|
|
2227
2289
|
}
|
|
2228
|
-
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)) {
|
|
2229
2291
|
const proposed = [];
|
|
2230
2292
|
const seen = /* @__PURE__ */ new Set();
|
|
2231
2293
|
const rejected = new Set(rejectedCodes);
|
|
@@ -2304,7 +2366,7 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2304
2366
|
userPrompt: selectionUserPrompt(
|
|
2305
2367
|
surveyReport,
|
|
2306
2368
|
renderOnboardingContext(bootstrap.onboardingContext),
|
|
2307
|
-
|
|
2369
|
+
existingEventCodes,
|
|
2308
2370
|
rejectedCodes
|
|
2309
2371
|
),
|
|
2310
2372
|
tools: [proposeTool, finishTool],
|
|
@@ -2326,12 +2388,27 @@ async function runSelection(provider, models, repoPath, bootstrap, surveyReport,
|
|
|
2326
2388
|
async function registerApprovedEvents(runs, runId, approved, sink) {
|
|
2327
2389
|
const registered = [];
|
|
2328
2390
|
for (const event of approved) {
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
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
|
+
}
|
|
2335
2412
|
registered.push({ ...event, eventId: result.id || result.item?.id || "" });
|
|
2336
2413
|
sink.emit({ phase: "persisting", file: event.anchorFile, action: `registered ${event.code}` });
|
|
2337
2414
|
}
|
|
@@ -2347,99 +2424,11 @@ function placementsFor(registered) {
|
|
|
2347
2424
|
}));
|
|
2348
2425
|
}
|
|
2349
2426
|
|
|
2350
|
-
// src/engine/state.ts
|
|
2351
|
-
import { createHash as createHash3 } from "crypto";
|
|
2352
|
-
import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
|
|
2353
|
-
import { homedir, tmpdir } from "os";
|
|
2354
|
-
import { join as join4 } from "path";
|
|
2355
|
-
|
|
2356
|
-
// src/engine/types.ts
|
|
2357
|
-
var ENGINE_PHASES = [
|
|
2358
|
-
"authorizing",
|
|
2359
|
-
"surveying",
|
|
2360
|
-
"designing",
|
|
2361
|
-
"persisting",
|
|
2362
|
-
"binding",
|
|
2363
|
-
"integrating",
|
|
2364
|
-
"reviewing",
|
|
2365
|
-
"verifying",
|
|
2366
|
-
"reporting",
|
|
2367
|
-
"completed"
|
|
2368
|
-
];
|
|
2369
|
-
|
|
2370
|
-
// src/engine/state.ts
|
|
2371
|
-
function stateDirectory() {
|
|
2372
|
-
const override = process.env.WHISPERR_WIZARD_STATE_DIR?.trim();
|
|
2373
|
-
const preferred = override || join4(homedir(), ".whisperr", "wizard-runs");
|
|
2374
|
-
if (canCreateStateDirectory(preferred)) {
|
|
2375
|
-
return preferred;
|
|
2376
|
-
}
|
|
2377
|
-
const user = typeof process.getuid === "function" ? process.getuid() : "user";
|
|
2378
|
-
const fallback = join4(tmpdir(), `whisperr-${user}`, "wizard-runs");
|
|
2379
|
-
if (canCreateStateDirectory(fallback)) {
|
|
2380
|
-
return fallback;
|
|
2381
|
-
}
|
|
2382
|
-
throw new Error("Whisperr Wizard could not create a directory for run state");
|
|
2383
|
-
}
|
|
2384
|
-
function stateKey(apiBaseUrl, appId, repoFingerprint2) {
|
|
2385
|
-
return createHash3("sha256").update(`${apiBaseUrl}
|
|
2386
|
-
${appId}
|
|
2387
|
-
${repoFingerprint2}`).digest("hex").slice(0, 24);
|
|
2388
|
-
}
|
|
2389
|
-
function statePath(apiBaseUrl, appId, repoFingerprint2) {
|
|
2390
|
-
return join4(stateDirectory(), `${stateKey(apiBaseUrl, appId, repoFingerprint2)}.json`);
|
|
2391
|
-
}
|
|
2392
|
-
function saveRunState(state) {
|
|
2393
|
-
const directory = stateDirectory();
|
|
2394
|
-
const path = join4(directory, `${stateKey(state.apiBaseUrl, state.appId, state.repoFingerprint)}.json`);
|
|
2395
|
-
const payload = JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2);
|
|
2396
|
-
const temporary = `${path}.tmp`;
|
|
2397
|
-
writeFileSync(temporary, payload, { mode: 384 });
|
|
2398
|
-
renameSync(temporary, path);
|
|
2399
|
-
}
|
|
2400
|
-
function canCreateStateDirectory(directory) {
|
|
2401
|
-
try {
|
|
2402
|
-
mkdirSync(directory, { recursive: true, mode: 448 });
|
|
2403
|
-
return true;
|
|
2404
|
-
} catch {
|
|
2405
|
-
return false;
|
|
2406
|
-
}
|
|
2407
|
-
}
|
|
2408
|
-
function loadRunState(apiBaseUrl, appId, repoFingerprint2) {
|
|
2409
|
-
const path = statePath(apiBaseUrl, appId, repoFingerprint2);
|
|
2410
|
-
let raw;
|
|
2411
|
-
try {
|
|
2412
|
-
raw = readFileSync2(path, "utf8");
|
|
2413
|
-
} catch {
|
|
2414
|
-
return void 0;
|
|
2415
|
-
}
|
|
2416
|
-
let parsed;
|
|
2417
|
-
try {
|
|
2418
|
-
parsed = JSON.parse(raw);
|
|
2419
|
-
} catch {
|
|
2420
|
-
return void 0;
|
|
2421
|
-
}
|
|
2422
|
-
if (!isEngineRunState(parsed)) {
|
|
2423
|
-
return void 0;
|
|
2424
|
-
}
|
|
2425
|
-
if (parsed.apiBaseUrl !== apiBaseUrl || parsed.appId !== appId || parsed.repoFingerprint !== repoFingerprint2) {
|
|
2426
|
-
return void 0;
|
|
2427
|
-
}
|
|
2428
|
-
return parsed;
|
|
2429
|
-
}
|
|
2430
|
-
function isEngineRunState(value) {
|
|
2431
|
-
if (typeof value !== "object" || value === null) {
|
|
2432
|
-
return false;
|
|
2433
|
-
}
|
|
2434
|
-
const candidate = value;
|
|
2435
|
-
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;
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
2427
|
// src/engine/worktree.ts
|
|
2439
2428
|
import { spawn as spawn2 } from "child_process";
|
|
2440
|
-
import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
|
|
2441
|
-
import { tmpdir
|
|
2442
|
-
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";
|
|
2443
2432
|
var WorktreeError = class extends Error {
|
|
2444
2433
|
constructor(code, message) {
|
|
2445
2434
|
super(message);
|
|
@@ -2449,7 +2438,7 @@ var WorktreeError = class extends Error {
|
|
|
2449
2438
|
code;
|
|
2450
2439
|
};
|
|
2451
2440
|
function git(cwd, args) {
|
|
2452
|
-
return new Promise((
|
|
2441
|
+
return new Promise((resolve6) => {
|
|
2453
2442
|
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2454
2443
|
let stdout = "";
|
|
2455
2444
|
let stderr = "";
|
|
@@ -2459,8 +2448,8 @@ function git(cwd, args) {
|
|
|
2459
2448
|
child.stderr.on("data", (chunk) => {
|
|
2460
2449
|
stderr += String(chunk);
|
|
2461
2450
|
});
|
|
2462
|
-
child.on("error", () =>
|
|
2463
|
-
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 }));
|
|
2464
2453
|
});
|
|
2465
2454
|
}
|
|
2466
2455
|
async function must(cwd, args) {
|
|
@@ -2470,21 +2459,64 @@ async function must(cwd, args) {
|
|
|
2470
2459
|
}
|
|
2471
2460
|
return result.stdout;
|
|
2472
2461
|
}
|
|
2473
|
-
|
|
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 = []) {
|
|
2474
2488
|
const inside = await git(repoPath, ["rev-parse", "--is-inside-work-tree"]);
|
|
2475
2489
|
if (!inside.ok) {
|
|
2476
2490
|
throw new WorktreeError("not_a_repo", "the target directory is not a git repository");
|
|
2477
2491
|
}
|
|
2478
|
-
const
|
|
2479
|
-
|
|
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) {
|
|
2480
2496
|
throw new WorktreeError(
|
|
2481
2497
|
"dirty_repo",
|
|
2482
|
-
|
|
2498
|
+
`the repository has unrelated uncommitted changes (${unexpected.slice(0, 3).join(", ")}) \u2014 commit or stash them, or rerun with --force`
|
|
2483
2499
|
);
|
|
2484
2500
|
}
|
|
2485
|
-
|
|
2486
|
-
const path = await mkdtemp(
|
|
2501
|
+
let baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
|
|
2502
|
+
const path = await mkdtemp(join4(tmpdir(), "whisperr-worktree-"));
|
|
2487
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
|
+
}
|
|
2488
2520
|
return { path, baseRef, repoPath };
|
|
2489
2521
|
}
|
|
2490
2522
|
async function worktreePatch(handle) {
|
|
@@ -2495,7 +2527,7 @@ async function applyPatchToRepo(handle, patch) {
|
|
|
2495
2527
|
if (patch.trim() === "") {
|
|
2496
2528
|
return;
|
|
2497
2529
|
}
|
|
2498
|
-
await new Promise((
|
|
2530
|
+
await new Promise((resolve6, reject) => {
|
|
2499
2531
|
const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
|
|
2500
2532
|
cwd: handle.repoPath,
|
|
2501
2533
|
stdio: ["pipe", "ignore", "pipe"]
|
|
@@ -2507,7 +2539,7 @@ async function applyPatchToRepo(handle, patch) {
|
|
|
2507
2539
|
child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
|
|
2508
2540
|
child.on("close", (exitCode) => {
|
|
2509
2541
|
if (exitCode === 0) {
|
|
2510
|
-
|
|
2542
|
+
resolve6();
|
|
2511
2543
|
} else {
|
|
2512
2544
|
reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
|
|
2513
2545
|
}
|
|
@@ -2515,13 +2547,27 @@ async function applyPatchToRepo(handle, patch) {
|
|
|
2515
2547
|
child.stdin.end(patch);
|
|
2516
2548
|
});
|
|
2517
2549
|
}
|
|
2518
|
-
async function
|
|
2519
|
-
|
|
2550
|
+
async function commitWorktreeCheckpoint(handle) {
|
|
2551
|
+
await must(handle.path, ["add", "-A"]);
|
|
2552
|
+
const patch = await must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
|
|
2520
2553
|
if (patch.trim() === "") {
|
|
2521
|
-
return
|
|
2554
|
+
return;
|
|
2522
2555
|
}
|
|
2523
|
-
await
|
|
2524
|
-
|
|
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"]);
|
|
2525
2571
|
}
|
|
2526
2572
|
async function removeWorktree(handle) {
|
|
2527
2573
|
await git(handle.repoPath, ["worktree", "remove", "--force", handle.path]);
|
|
@@ -2546,7 +2592,7 @@ async function runEngine(input) {
|
|
|
2546
2592
|
throw error;
|
|
2547
2593
|
}
|
|
2548
2594
|
const runId = bootstrap.run.id;
|
|
2549
|
-
input.provider.onRunReady?.(runId);
|
|
2595
|
+
input.provider.onRunReady?.(runId, bootstrap.run.modelConversationId);
|
|
2550
2596
|
const patchPhase = async (phase, message) => {
|
|
2551
2597
|
try {
|
|
2552
2598
|
await runs.patchRun(runId, { phase, ...message ? { message } : {} });
|
|
@@ -2559,45 +2605,42 @@ async function runEngine(input) {
|
|
|
2559
2605
|
} catch {
|
|
2560
2606
|
}
|
|
2561
2607
|
};
|
|
2562
|
-
let state;
|
|
2563
|
-
try {
|
|
2564
|
-
state = loadRunState(input.config.apiBaseUrl, input.session.appId, input.repoFingerprint) ?? freshState(input, runId);
|
|
2565
|
-
if (state.runId !== runId) {
|
|
2566
|
-
state = freshState(input, runId);
|
|
2567
|
-
}
|
|
2568
|
-
} catch (error) {
|
|
2569
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
2570
|
-
await failRun(`state initialization failed: ${reason}`);
|
|
2571
|
-
return { kind: "aborted", reason: `state initialization failed: ${reason}` };
|
|
2572
|
-
}
|
|
2573
|
-
const persist = () => saveRunState(state);
|
|
2574
2608
|
let registered;
|
|
2575
|
-
|
|
2576
|
-
|
|
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
|
+
});
|
|
2577
2620
|
await patchPhase("surveying");
|
|
2578
2621
|
const survey = await runSurvey(
|
|
2579
2622
|
input.provider,
|
|
2580
2623
|
input.models,
|
|
2581
2624
|
input.repoPath,
|
|
2582
2625
|
bootstrap,
|
|
2583
|
-
input.sink
|
|
2584
|
-
state.providerSessions.survey
|
|
2626
|
+
input.sink
|
|
2585
2627
|
);
|
|
2586
2628
|
if (survey.outcome.kind !== "completed") {
|
|
2587
2629
|
await failRun(`survey ${survey.outcome.kind}`);
|
|
2588
2630
|
return { kind: "aborted", reason: `survey ${survey.outcome.kind}` };
|
|
2589
2631
|
}
|
|
2590
|
-
state.providerSessions.survey = survey.outcome.sessionId;
|
|
2591
|
-
persist();
|
|
2592
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);
|
|
2593
2635
|
const selection = await runSelection(
|
|
2594
2636
|
input.provider,
|
|
2595
2637
|
input.models,
|
|
2596
2638
|
input.repoPath,
|
|
2597
2639
|
bootstrap,
|
|
2598
2640
|
survey.report,
|
|
2599
|
-
|
|
2600
|
-
input.sink
|
|
2641
|
+
[],
|
|
2642
|
+
input.sink,
|
|
2643
|
+
existingCodes
|
|
2601
2644
|
);
|
|
2602
2645
|
if (!selection.finished) {
|
|
2603
2646
|
const reason = selection.outcome === "completed" ? "selection did not call finish_selection" : `selection ${selection.outcome}`;
|
|
@@ -2617,44 +2660,114 @@ async function runEngine(input) {
|
|
|
2617
2660
|
await failRun("no events accepted for instrumentation");
|
|
2618
2661
|
return { kind: "aborted", reason: "no events accepted for instrumentation" };
|
|
2619
2662
|
}
|
|
2620
|
-
const approvedCodes = new Set(approved.map((event) => event.code));
|
|
2621
|
-
for (const event of selection.proposed) {
|
|
2622
|
-
if (!approvedCodes.has(event.code) && !state.tombstonedCodes.includes(event.code)) {
|
|
2623
|
-
state.tombstonedCodes.push(event.code);
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
persist();
|
|
2627
2663
|
await patchPhase("persisting", `registering ${approved.length} events`);
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
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
|
+
});
|
|
2636
2676
|
}
|
|
2637
|
-
}
|
|
2638
|
-
|
|
2639
|
-
|
|
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}` };
|
|
2640
2681
|
}
|
|
2641
2682
|
if (registered.length === 0) {
|
|
2642
2683
|
await failRun("no events registered for this project");
|
|
2643
2684
|
return { kind: "aborted", reason: "no events registered for this project" };
|
|
2644
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
|
+
}
|
|
2645
2712
|
await patchPhase("binding");
|
|
2646
2713
|
let worktree;
|
|
2647
2714
|
try {
|
|
2648
|
-
worktree = await createWorktree(
|
|
2715
|
+
worktree = await createWorktree(
|
|
2716
|
+
input.repoPath,
|
|
2717
|
+
input.force ?? false,
|
|
2718
|
+
[...changedFiles2]
|
|
2719
|
+
);
|
|
2649
2720
|
} catch (error) {
|
|
2650
2721
|
const reason = error instanceof Error ? error.message : String(error);
|
|
2651
2722
|
await failRun(reason);
|
|
2652
2723
|
return { kind: "aborted", reason };
|
|
2653
2724
|
}
|
|
2654
|
-
|
|
2655
|
-
|
|
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
|
+
};
|
|
2656
2739
|
try {
|
|
2657
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));
|
|
2658
2771
|
const deps = {
|
|
2659
2772
|
provider: input.provider,
|
|
2660
2773
|
models: input.models,
|
|
@@ -2664,140 +2777,179 @@ async function runEngine(input) {
|
|
|
2664
2777
|
registered,
|
|
2665
2778
|
ingestion: bootstrap.ingestion,
|
|
2666
2779
|
sink: input.sink,
|
|
2667
|
-
saveClusters: (
|
|
2668
|
-
|
|
2669
|
-
|
|
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
|
+
}
|
|
2670
2806
|
}
|
|
2671
2807
|
};
|
|
2672
|
-
const identifyWired = await runSdkSetupPass(deps, bindings);
|
|
2673
2808
|
await patchPhase("integrating");
|
|
2674
2809
|
const integration = await runClusterIntegration(
|
|
2675
2810
|
deps,
|
|
2676
|
-
|
|
2811
|
+
clusters,
|
|
2677
2812
|
async () => worktreePatch(worktree)
|
|
2678
2813
|
);
|
|
2679
|
-
|
|
2680
|
-
|
|
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");
|
|
2681
2830
|
const finalVerify = await finalizeIntegration(deps);
|
|
2682
|
-
const patch = await worktreePatch(worktree);
|
|
2683
|
-
const changedFiles2 = patchFiles(patch);
|
|
2684
2831
|
await patchPhase("reporting");
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
runs,
|
|
2688
|
-
runId,
|
|
2689
|
-
evidence,
|
|
2690
|
-
integration.eventStatuses,
|
|
2691
|
-
changedFiles2,
|
|
2832
|
+
await runs.completeRun(runId, {
|
|
2833
|
+
changedFiles: [...changedFiles2].sort(),
|
|
2692
2834
|
identifyWired,
|
|
2693
|
-
finalVerify.status,
|
|
2694
|
-
finalVerify.command
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
const partialPatchPath = applied ? void 0 : await savePartialPatch(worktree, join6(input.repoPath, ".whisperr-wizard.patch"));
|
|
2702
|
-
await removeWorktree(worktree);
|
|
2703
|
-
state.worktreePath = void 0;
|
|
2704
|
-
persist();
|
|
2705
|
-
const hasFailures = [...integration.eventStatuses.values()].some(
|
|
2706
|
-
(status) => status === "failed"
|
|
2707
|
-
);
|
|
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");
|
|
2708
2843
|
return {
|
|
2709
2844
|
kind: hasFailures ? "partial" : "completed",
|
|
2710
2845
|
runId,
|
|
2711
2846
|
registered,
|
|
2712
|
-
eventStatuses
|
|
2713
|
-
reportEvents: buildReportEvents(registered,
|
|
2714
|
-
changedFiles: changedFiles2,
|
|
2847
|
+
eventStatuses,
|
|
2848
|
+
reportEvents: buildReportEvents(registered, eventStatuses),
|
|
2849
|
+
changedFiles: [...changedFiles2].sort(),
|
|
2715
2850
|
identifyWired,
|
|
2716
|
-
applied,
|
|
2717
|
-
partialPatchPath: partialPatchPath ?? void 0,
|
|
2851
|
+
applied: changedFiles2.size > 0,
|
|
2718
2852
|
summary: summaryLines.join("\n")
|
|
2719
2853
|
};
|
|
2720
2854
|
} catch (error) {
|
|
2721
2855
|
const reason = error instanceof Error ? error.message : String(error);
|
|
2722
2856
|
await failRun(reason);
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
join6(input.repoPath, ".whisperr-wizard.patch")
|
|
2726
|
-
).catch(() => void 0);
|
|
2727
|
-
persist();
|
|
2857
|
+
await removeWorktree(worktree).catch(() => {
|
|
2858
|
+
});
|
|
2728
2859
|
return {
|
|
2729
2860
|
kind: "partial",
|
|
2730
2861
|
runId,
|
|
2731
2862
|
registered,
|
|
2732
|
-
eventStatuses
|
|
2733
|
-
reportEvents: buildReportEvents(registered,
|
|
2734
|
-
changedFiles: [],
|
|
2735
|
-
identifyWired
|
|
2736
|
-
applied:
|
|
2737
|
-
|
|
2738
|
-
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.`
|
|
2739
2869
|
};
|
|
2740
2870
|
}
|
|
2741
2871
|
}
|
|
2742
|
-
function
|
|
2743
|
-
return {
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
tombstonedCodes: [],
|
|
2753
|
-
clusters: [],
|
|
2754
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2755
|
-
};
|
|
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
|
+
}));
|
|
2756
2882
|
}
|
|
2757
|
-
function
|
|
2758
|
-
const
|
|
2759
|
-
const
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
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`
|
|
2774
2908
|
});
|
|
2775
2909
|
}
|
|
2776
2910
|
}
|
|
2777
|
-
return
|
|
2911
|
+
return statuses;
|
|
2778
2912
|
}
|
|
2779
|
-
function
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
const
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
return
|
|
2786
|
-
|
|
2787
|
-
eventCode: event.code,
|
|
2788
|
-
...needsFile && file ? { file } : {},
|
|
2789
|
-
status
|
|
2790
|
-
};
|
|
2791
|
-
});
|
|
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
|
+
}
|
|
2792
2921
|
}
|
|
2793
|
-
async function
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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
|
+
}));
|
|
2801
2953
|
}
|
|
2802
2954
|
function buildReportEvents(registered, statuses) {
|
|
2803
2955
|
return registered.map((event) => {
|
|
@@ -2838,9 +2990,9 @@ import {
|
|
|
2838
2990
|
|
|
2839
2991
|
// src/core/git.ts
|
|
2840
2992
|
import { spawn as spawn3 } from "child_process";
|
|
2841
|
-
import { createHash as
|
|
2842
|
-
import { readFile as
|
|
2843
|
-
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";
|
|
2844
2996
|
async function takeCheckpoint(repoPath) {
|
|
2845
2997
|
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
2846
2998
|
if (!isRepo) return { isRepo: false };
|
|
@@ -2889,7 +3041,7 @@ async function revertToCheckpoint(repoPath, checkpoint) {
|
|
|
2889
3041
|
async function repoFingerprint(repoPath) {
|
|
2890
3042
|
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
2891
3043
|
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename2(repoPath)}`;
|
|
2892
|
-
return
|
|
3044
|
+
return createHash3("sha256").update(seed).digest("hex").slice(0, 16);
|
|
2893
3045
|
}
|
|
2894
3046
|
function normalizeRemote(url) {
|
|
2895
3047
|
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
@@ -2906,7 +3058,7 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
|
2906
3058
|
const contents = [];
|
|
2907
3059
|
for (const file of files) {
|
|
2908
3060
|
try {
|
|
2909
|
-
contents.push({ file, content: await
|
|
3061
|
+
contents.push({ file, content: await readFile3(join5(repoPath, file), "utf8") });
|
|
2910
3062
|
} catch {
|
|
2911
3063
|
continue;
|
|
2912
3064
|
}
|
|
@@ -2936,14 +3088,14 @@ function escapeRegExp(s) {
|
|
|
2936
3088
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2937
3089
|
}
|
|
2938
3090
|
function run(cwd, args) {
|
|
2939
|
-
return new Promise((
|
|
3091
|
+
return new Promise((resolve6) => {
|
|
2940
3092
|
const child = spawn3("git", args, { cwd });
|
|
2941
3093
|
let stdout = "";
|
|
2942
3094
|
let stderr = "";
|
|
2943
3095
|
child.stdout.on("data", (d) => stdout += d.toString());
|
|
2944
3096
|
child.stderr.on("data", (d) => stderr += d.toString());
|
|
2945
|
-
child.on("close", (code) =>
|
|
2946
|
-
child.on("error", () =>
|
|
3097
|
+
child.on("close", (code) => resolve6({ ok: code === 0, stdout, stderr }));
|
|
3098
|
+
child.on("error", () => resolve6({ ok: false, stdout, stderr }));
|
|
2947
3099
|
});
|
|
2948
3100
|
}
|
|
2949
3101
|
|
|
@@ -3182,8 +3334,8 @@ function coverageNote(coverage) {
|
|
|
3182
3334
|
}
|
|
3183
3335
|
|
|
3184
3336
|
// src/core/toolPolicy.ts
|
|
3185
|
-
import { isAbsolute, relative, resolve } from "path";
|
|
3186
|
-
var SECRET_MATERIAL_DENIAL = "blocked: secret material
|
|
3337
|
+
import { isAbsolute, relative as relative2, resolve as resolve3 } from "path";
|
|
3338
|
+
var SECRET_MATERIAL_DENIAL = "blocked: secret material must not be accessed";
|
|
3187
3339
|
var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
|
|
3188
3340
|
var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
|
|
3189
3341
|
var BASH_ALLOWLIST_DENIAL = "blocked: command is outside the Bash allowlist - use package install/add commands, mkdir, or git status/diff/log only";
|
|
@@ -3194,9 +3346,24 @@ function engineMcpToolName(toolName) {
|
|
|
3194
3346
|
function trustedEngineMcpTools(tools) {
|
|
3195
3347
|
return new Set(tools.map((tool2) => engineMcpToolName(tool2.name)));
|
|
3196
3348
|
}
|
|
3197
|
-
var
|
|
3198
|
-
|
|
3199
|
-
|
|
3349
|
+
var SECRET_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
3350
|
+
".aws",
|
|
3351
|
+
".ssh",
|
|
3352
|
+
".gnupg",
|
|
3353
|
+
".azure",
|
|
3354
|
+
".docker",
|
|
3355
|
+
".kube",
|
|
3356
|
+
".secrets",
|
|
3357
|
+
"secrets"
|
|
3358
|
+
]);
|
|
3359
|
+
var SECRET_EXACT_FILES = /* @__PURE__ */ new Set([
|
|
3360
|
+
".netrc",
|
|
3361
|
+
".npmrc",
|
|
3362
|
+
".pypirc",
|
|
3363
|
+
"auth.json",
|
|
3364
|
+
"google-services.json",
|
|
3365
|
+
"googleservice-info.plist"
|
|
3366
|
+
]);
|
|
3200
3367
|
var SENSITIVE_WRITE_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".circleci", ".buildkite"]);
|
|
3201
3368
|
var SENSITIVE_WRITE_EXACT_FILES = /* @__PURE__ */ new Set([
|
|
3202
3369
|
".gitlab-ci.yml",
|
|
@@ -3212,6 +3379,7 @@ var SECRET_SUFFIXES = [
|
|
|
3212
3379
|
".pfx",
|
|
3213
3380
|
".jks",
|
|
3214
3381
|
".keystore",
|
|
3382
|
+
".kdbx",
|
|
3215
3383
|
".tfvars"
|
|
3216
3384
|
];
|
|
3217
3385
|
var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
@@ -3245,7 +3413,6 @@ function isSecretPathLike(value) {
|
|
|
3245
3413
|
for (const rawPart of parts) {
|
|
3246
3414
|
const part = stripOuterQuotes(rawPart.trim().toLowerCase());
|
|
3247
3415
|
if (!part || part === "." || part === "**") continue;
|
|
3248
|
-
if (ENV_EXAMPLES.has(part)) continue;
|
|
3249
3416
|
if (SECRET_DIRECTORIES.has(part)) return true;
|
|
3250
3417
|
if (SECRET_EXACT_FILES.has(part)) return true;
|
|
3251
3418
|
if (part === ".env" || part === ".envrc" || part.startsWith(".env.") || part.startsWith(".env*")) {
|
|
@@ -3256,15 +3423,16 @@ function isSecretPathLike(value) {
|
|
|
3256
3423
|
}
|
|
3257
3424
|
if (part.includes("credentials")) return true;
|
|
3258
3425
|
if (part.startsWith("secrets.")) return true;
|
|
3426
|
+
if (part.startsWith("service-account") && part.endsWith(".json")) return true;
|
|
3259
3427
|
if (SECRET_SUFFIXES.some((suffix) => part.endsWith(suffix))) return true;
|
|
3260
3428
|
}
|
|
3261
3429
|
return false;
|
|
3262
3430
|
}
|
|
3263
3431
|
function isPathInsideRepo(pathValue, repoPath) {
|
|
3264
3432
|
if (pathValue.includes("..")) return false;
|
|
3265
|
-
const repoRoot =
|
|
3266
|
-
const candidate = isAbsolute(pathValue) ?
|
|
3267
|
-
const rel =
|
|
3433
|
+
const repoRoot = resolve3(repoPath);
|
|
3434
|
+
const candidate = isAbsolute(pathValue) ? resolve3(pathValue) : resolve3(repoRoot, pathValue);
|
|
3435
|
+
const rel = relative2(repoRoot, candidate);
|
|
3268
3436
|
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
3269
3437
|
}
|
|
3270
3438
|
function evaluateReadLike(secretValues, repoPathValues, context) {
|
|
@@ -3291,6 +3459,9 @@ function evaluateWrite(input, context) {
|
|
|
3291
3459
|
if (!isPathInsideRepo(pathValue, context.repoPath)) {
|
|
3292
3460
|
return { behavior: "deny", message: WRITE_OUTSIDE_REPO_DENIAL };
|
|
3293
3461
|
}
|
|
3462
|
+
if (isSecretPathLike(pathValue)) {
|
|
3463
|
+
return { behavior: "deny", message: SECRET_MATERIAL_DENIAL };
|
|
3464
|
+
}
|
|
3294
3465
|
if (isSensitiveWritePath(pathValue, context.repoPath)) {
|
|
3295
3466
|
return { behavior: "deny", message: SENSITIVE_WRITE_DENIAL };
|
|
3296
3467
|
}
|
|
@@ -3384,9 +3555,9 @@ function normalizePathLike(value) {
|
|
|
3384
3555
|
return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
|
|
3385
3556
|
}
|
|
3386
3557
|
function repoRelativePath(pathValue, repoPath) {
|
|
3387
|
-
const repoRoot =
|
|
3388
|
-
const candidate = isAbsolute(pathValue) ?
|
|
3389
|
-
return normalizePathLike(
|
|
3558
|
+
const repoRoot = resolve3(repoPath);
|
|
3559
|
+
const candidate = isAbsolute(pathValue) ? resolve3(pathValue) : resolve3(repoRoot, pathValue);
|
|
3560
|
+
return normalizePathLike(relative2(repoRoot, candidate));
|
|
3390
3561
|
}
|
|
3391
3562
|
function isSensitiveWritePath(pathValue, repoPath) {
|
|
3392
3563
|
const parts = repoRelativePath(pathValue, repoPath).split("/").map((part) => stripOuterQuotes(part.trim().toLowerCase())).filter(Boolean);
|
|
@@ -4349,6 +4520,10 @@ function short(s, max = 60) {
|
|
|
4349
4520
|
// src/engine/providers/claude.ts
|
|
4350
4521
|
var READ_ONLY_TOOLS2 = ["Read", "Glob", "Grep"];
|
|
4351
4522
|
var EDIT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
4523
|
+
var FAST_MODE_MODELS = /* @__PURE__ */ new Set(["claude-opus-5", "claude-opus-4-8"]);
|
|
4524
|
+
function supportsClaudeFastMode(model) {
|
|
4525
|
+
return FAST_MODE_MODELS.has(model);
|
|
4526
|
+
}
|
|
4352
4527
|
function createClaudeProvider(config, session) {
|
|
4353
4528
|
return {
|
|
4354
4529
|
name: "claude",
|
|
@@ -4384,6 +4559,7 @@ function createClaudeProvider(config, session) {
|
|
|
4384
4559
|
hooks: buildToolPolicyHooks(invocation.cwd, trustedTools),
|
|
4385
4560
|
canUseTool: createToolPermissionCallback(invocation.cwd, trustedTools),
|
|
4386
4561
|
maxTurns: roleConfig.maxTurns,
|
|
4562
|
+
...supportsClaudeFastMode(roleConfig.model) ? { settings: { fastMode: true } } : {},
|
|
4387
4563
|
...invocation.resumeSessionId ? { resume: invocation.resumeSessionId } : {},
|
|
4388
4564
|
settingSources: []
|
|
4389
4565
|
}
|
|
@@ -4435,15 +4611,27 @@ function createClaudeProvider(config, session) {
|
|
|
4435
4611
|
}
|
|
4436
4612
|
|
|
4437
4613
|
// src/engine/providers/openai.ts
|
|
4438
|
-
import { readdirSync, readFileSync as
|
|
4439
|
-
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";
|
|
4440
4616
|
function jailedPath(cwd, candidate) {
|
|
4441
|
-
const resolved =
|
|
4442
|
-
if (resolved !== cwd && !resolved.startsWith(cwd +
|
|
4617
|
+
const resolved = resolve4(cwd, candidate);
|
|
4618
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + sep3)) {
|
|
4443
4619
|
throw new Error(`path ${candidate} escapes the repository`);
|
|
4444
4620
|
}
|
|
4445
4621
|
return resolved;
|
|
4446
4622
|
}
|
|
4623
|
+
function guardedPath(cwd, toolName, candidate) {
|
|
4624
|
+
const path = jailedPath(cwd, candidate);
|
|
4625
|
+
const decision = evaluateToolUse(
|
|
4626
|
+
toolName,
|
|
4627
|
+
toolName === "Glob" ? { path: candidate, pattern: "*" } : { file_path: candidate },
|
|
4628
|
+
{ repoPath: cwd }
|
|
4629
|
+
);
|
|
4630
|
+
if (decision.behavior === "deny") {
|
|
4631
|
+
throw new Error(decision.message);
|
|
4632
|
+
}
|
|
4633
|
+
return path;
|
|
4634
|
+
}
|
|
4447
4635
|
function fileTools(cwd, allowWrite) {
|
|
4448
4636
|
const tools = [
|
|
4449
4637
|
{
|
|
@@ -4456,7 +4644,8 @@ function fileTools(cwd, allowWrite) {
|
|
|
4456
4644
|
required: ["path"]
|
|
4457
4645
|
},
|
|
4458
4646
|
handler: async (input) => {
|
|
4459
|
-
const
|
|
4647
|
+
const path = guardedPath(cwd, "Read", String(input.path ?? ""));
|
|
4648
|
+
const content = readFileSync2(path, "utf8");
|
|
4460
4649
|
return { content: content.slice(0, 4e4) };
|
|
4461
4650
|
}
|
|
4462
4651
|
},
|
|
@@ -4470,9 +4659,9 @@ function fileTools(cwd, allowWrite) {
|
|
|
4470
4659
|
required: ["path"]
|
|
4471
4660
|
},
|
|
4472
4661
|
handler: async (input) => {
|
|
4473
|
-
const target9 =
|
|
4474
|
-
const entries = readdirSync(target9).map((entry) => {
|
|
4475
|
-
const kind = statSync(
|
|
4662
|
+
const target9 = guardedPath(cwd, "Glob", String(input.path ?? "."));
|
|
4663
|
+
const entries = readdirSync(target9).filter((entry) => !isSecretPathLike(entry)).map((entry) => {
|
|
4664
|
+
const kind = statSync(join6(target9, entry)).isDirectory() ? "dir" : "file";
|
|
4476
4665
|
return `${kind}:${entry}`;
|
|
4477
4666
|
});
|
|
4478
4667
|
return { entries: entries.slice(0, 500) };
|
|
@@ -4491,7 +4680,8 @@ function fileTools(cwd, allowWrite) {
|
|
|
4491
4680
|
required: ["path", "content"]
|
|
4492
4681
|
},
|
|
4493
4682
|
handler: async (input) => {
|
|
4494
|
-
|
|
4683
|
+
const path = guardedPath(cwd, "Write", String(input.path ?? ""));
|
|
4684
|
+
writeFileSync(path, String(input.content ?? ""));
|
|
4495
4685
|
return { ok: true };
|
|
4496
4686
|
}
|
|
4497
4687
|
},
|
|
@@ -4509,13 +4699,13 @@ function fileTools(cwd, allowWrite) {
|
|
|
4509
4699
|
required: ["path", "oldText", "newText"]
|
|
4510
4700
|
},
|
|
4511
4701
|
handler: async (input) => {
|
|
4512
|
-
const path =
|
|
4513
|
-
const content =
|
|
4702
|
+
const path = guardedPath(cwd, "Edit", String(input.path ?? ""));
|
|
4703
|
+
const content = readFileSync2(path, "utf8");
|
|
4514
4704
|
const oldText = String(input.oldText ?? "");
|
|
4515
4705
|
if (!content.includes(oldText)) {
|
|
4516
4706
|
return { ok: false, reason: "oldText not found" };
|
|
4517
4707
|
}
|
|
4518
|
-
|
|
4708
|
+
writeFileSync(path, content.replace(oldText, String(input.newText ?? "")));
|
|
4519
4709
|
return { ok: true };
|
|
4520
4710
|
}
|
|
4521
4711
|
}
|
|
@@ -4579,8 +4769,9 @@ function createOpenAIProvider(config, session) {
|
|
|
4579
4769
|
};
|
|
4580
4770
|
return {
|
|
4581
4771
|
name: "openai-gateway",
|
|
4582
|
-
onRunReady(readyRunId) {
|
|
4772
|
+
onRunReady(readyRunId, resumeSessionId) {
|
|
4583
4773
|
runId = readyRunId;
|
|
4774
|
+
conversationId = resumeSessionId;
|
|
4584
4775
|
},
|
|
4585
4776
|
async invoke(invocation, roleConfig, onProgress) {
|
|
4586
4777
|
let costUsd = 0;
|
|
@@ -4662,9 +4853,9 @@ function createRoutedProvider(routes, fallback) {
|
|
|
4662
4853
|
const providers = /* @__PURE__ */ new Set([fallback, ...Object.values(routes)]);
|
|
4663
4854
|
return {
|
|
4664
4855
|
name: "routed",
|
|
4665
|
-
onRunReady(runId) {
|
|
4856
|
+
onRunReady(runId, resumeSessionId) {
|
|
4666
4857
|
for (const provider of providers) {
|
|
4667
|
-
provider.onRunReady?.(runId);
|
|
4858
|
+
provider.onRunReady?.(runId, resumeSessionId);
|
|
4668
4859
|
}
|
|
4669
4860
|
},
|
|
4670
4861
|
invoke(invocation, config, onProgress) {
|
|
@@ -4679,7 +4870,7 @@ var MINUTE = 6e4;
|
|
|
4679
4870
|
function engineModelConfig(config) {
|
|
4680
4871
|
const model = (role, fallback) => process.env[`WHISPERR_WIZARD_ENGINE_${role.toUpperCase()}_MODEL`]?.trim() || fallback;
|
|
4681
4872
|
return {
|
|
4682
|
-
survey: { model: model("survey",
|
|
4873
|
+
survey: { model: model("survey", config.model), effort: "medium", maxTurns: 40, maxMs: 12 * MINUTE },
|
|
4683
4874
|
design: { model: model("design", config.plannerModel), effort: "high", maxTurns: 40, maxMs: 15 * MINUTE },
|
|
4684
4875
|
edit: { model: model("edit", config.model), effort: config.effort, maxTurns: 50, maxMs: 15 * MINUTE },
|
|
4685
4876
|
review: { model: model("review", config.model), effort: "medium", maxTurns: 15, maxMs: 8 * MINUTE }
|
|
@@ -4716,16 +4907,7 @@ async function tryEngineFlow(options) {
|
|
|
4716
4907
|
playbookSystemPrompt: playbook.systemPrompt,
|
|
4717
4908
|
sink,
|
|
4718
4909
|
callbacks: {
|
|
4719
|
-
reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2)
|
|
4720
|
-
confirmApply: async (patch, summaryLines) => {
|
|
4721
|
-
p.note(summaryLines.join("\n"), "Integration result");
|
|
4722
|
-
const lineCount = patch.split("\n").length;
|
|
4723
|
-
const answer = await p.confirm({
|
|
4724
|
-
message: `Apply these changes to your working tree? (${lineCount} patch lines)`,
|
|
4725
|
-
initialValue: true
|
|
4726
|
-
});
|
|
4727
|
-
return answer === true;
|
|
4728
|
-
}
|
|
4910
|
+
reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2)
|
|
4729
4911
|
}
|
|
4730
4912
|
});
|
|
4731
4913
|
if (result.kind === "wrong_mode") {
|
|
@@ -4775,7 +4957,7 @@ async function reviewEventsInTerminal(proposed, theme2) {
|
|
|
4775
4957
|
});
|
|
4776
4958
|
p.note(lines.join("\n\n"), `Proposed events (${proposed.length})`);
|
|
4777
4959
|
const selection = await p.multiselect({
|
|
4778
|
-
message: "Register these events? Deselect any you don't want.",
|
|
4960
|
+
message: "Register and integrate these events? Deselect any you don't want.",
|
|
4779
4961
|
options: proposed.map((event) => ({ value: event.code, label: event.code, hint: event.name })),
|
|
4780
4962
|
initialValues: proposed.map((event) => event.code),
|
|
4781
4963
|
required: false
|
|
@@ -5018,7 +5200,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5018
5200
|
return Promise.resolve({ ran: false, ok: true, output: "" });
|
|
5019
5201
|
}
|
|
5020
5202
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
5021
|
-
return new Promise((
|
|
5203
|
+
return new Promise((resolve6) => {
|
|
5022
5204
|
const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
|
|
5023
5205
|
let out = "";
|
|
5024
5206
|
const append = (d) => {
|
|
@@ -5029,11 +5211,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5029
5211
|
child.stderr?.on("data", append);
|
|
5030
5212
|
const timer = setTimeout(() => {
|
|
5031
5213
|
child.kill("SIGKILL");
|
|
5032
|
-
|
|
5214
|
+
resolve6({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
|
|
5033
5215
|
}, timeoutMs);
|
|
5034
5216
|
child.on("close", (code) => {
|
|
5035
5217
|
clearTimeout(timer);
|
|
5036
|
-
|
|
5218
|
+
resolve6({
|
|
5037
5219
|
ran: true,
|
|
5038
5220
|
ok: code === 0,
|
|
5039
5221
|
command,
|
|
@@ -5045,7 +5227,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
5045
5227
|
});
|
|
5046
5228
|
child.on("error", () => {
|
|
5047
5229
|
clearTimeout(timer);
|
|
5048
|
-
|
|
5230
|
+
resolve6({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
|
|
5049
5231
|
});
|
|
5050
5232
|
});
|
|
5051
5233
|
}
|
|
@@ -5055,21 +5237,21 @@ function tail2(s) {
|
|
|
5055
5237
|
}
|
|
5056
5238
|
|
|
5057
5239
|
// src/core/version.ts
|
|
5058
|
-
import { readFileSync as
|
|
5059
|
-
import { dirname as
|
|
5240
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
5241
|
+
import { dirname as dirname4, join as join7, parse } from "path";
|
|
5060
5242
|
import { fileURLToPath } from "url";
|
|
5061
5243
|
var PACKAGE_NAME = "@whisperr/wizard";
|
|
5062
5244
|
function packageVersion() {
|
|
5063
|
-
let dir =
|
|
5245
|
+
let dir = dirname4(fileURLToPath(import.meta.url));
|
|
5064
5246
|
const { root } = parse(dir);
|
|
5065
5247
|
while (true) {
|
|
5066
5248
|
try {
|
|
5067
|
-
const pkg = JSON.parse(
|
|
5249
|
+
const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf8"));
|
|
5068
5250
|
if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
|
|
5069
5251
|
} catch {
|
|
5070
5252
|
}
|
|
5071
5253
|
if (dir === root) return "0.0.0";
|
|
5072
|
-
dir =
|
|
5254
|
+
dir = dirname4(dir);
|
|
5073
5255
|
}
|
|
5074
5256
|
}
|
|
5075
5257
|
|
|
@@ -5202,7 +5384,7 @@ function banner() {
|
|
|
5202
5384
|
|
|
5203
5385
|
// src/cli.ts
|
|
5204
5386
|
async function run2(options) {
|
|
5205
|
-
const repoPath =
|
|
5387
|
+
const repoPath = resolve5(options.path ?? process.cwd());
|
|
5206
5388
|
const config = resolveConfig(options);
|
|
5207
5389
|
console.log(banner());
|
|
5208
5390
|
p2.intro(theme.signal("Let's wire Whisperr into your app."));
|