@whisperr/wizard 0.5.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/dist/index.js +2794 -912
- package/package.json +6 -4
package/dist/index.js
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
4
|
+
import { realpathSync } from "fs";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
6
|
|
|
8
7
|
// src/cli.ts
|
|
9
|
-
import { resolve as
|
|
10
|
-
import * as
|
|
8
|
+
import { resolve as resolve3 } from "path";
|
|
9
|
+
import * as p2 from "@clack/prompts";
|
|
11
10
|
import open2 from "open";
|
|
12
11
|
|
|
13
12
|
// src/core/config.ts
|
|
14
13
|
var DEFAULT_API_BASE = "https://api.whisperr.net";
|
|
15
14
|
var DEFAULT_MODEL = "claude-sonnet-5";
|
|
16
|
-
var DEFAULT_PLANNER_MODEL = "claude-opus-
|
|
15
|
+
var DEFAULT_PLANNER_MODEL = "claude-opus-5";
|
|
17
16
|
var DEFAULT_EFFORT = "high";
|
|
18
17
|
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
19
18
|
function resolveEffort() {
|
|
@@ -41,7 +40,10 @@ function resolveConfig(flags = {}) {
|
|
|
41
40
|
// cleanly and keeps whatever already landed. Real runs finish well under it.
|
|
42
41
|
budgetUsd: Number(process.env.WHISPERR_WIZARD_BUDGET_USD) || 25,
|
|
43
42
|
directAnthropicKey,
|
|
44
|
-
offline
|
|
43
|
+
offline,
|
|
44
|
+
reviewPlan: flags.reviewPlan ?? false,
|
|
45
|
+
proposeOnly: flags.proposeOnly ?? false,
|
|
46
|
+
strictReview: flags.strictReview ?? false
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -938,7 +940,7 @@ var ALL_PLAYBOOKS = [
|
|
|
938
940
|
swiftPlaybook
|
|
939
941
|
];
|
|
940
942
|
function playbookByTargetId(id) {
|
|
941
|
-
return ALL_PLAYBOOKS.find((
|
|
943
|
+
return ALL_PLAYBOOKS.find((p3) => p3.target.id === id);
|
|
942
944
|
}
|
|
943
945
|
|
|
944
946
|
// src/core/detect.ts
|
|
@@ -980,7 +982,7 @@ function makeDetectContext(repoPath) {
|
|
|
980
982
|
async function detectStack(repoPath) {
|
|
981
983
|
const ctx = makeDetectContext(repoPath);
|
|
982
984
|
const results = await Promise.all(
|
|
983
|
-
ALL_PLAYBOOKS.map((
|
|
985
|
+
ALL_PLAYBOOKS.map((p3) => p3.detect(ctx).catch(() => null))
|
|
984
986
|
);
|
|
985
987
|
return results.filter((d) => d !== null).sort((a, b) => b.confidence - a.confidence);
|
|
986
988
|
}
|
|
@@ -1124,7 +1126,18 @@ function mockManifest(appId, config) {
|
|
|
1124
1126
|
appName: "Acme (dev)",
|
|
1125
1127
|
ingestionApiKey: "wrk_offline_demo_key_do_not_use",
|
|
1126
1128
|
ingestionBaseUrl: config.apiBaseUrl,
|
|
1127
|
-
|
|
1129
|
+
// Same labelled shape whisperr-go renders from the app's onboarding
|
|
1130
|
+
// business context, so an offline run exercises the real prompt.
|
|
1131
|
+
businessContext: [
|
|
1132
|
+
"Product: Acme (Acme Inc)",
|
|
1133
|
+
"What it does: B2C subscription app; a free trial converts to a paid monthly plan",
|
|
1134
|
+
"Industry: Software / Productivity",
|
|
1135
|
+
"Who the paying end user is: individual consumers on the trial-to-paid path",
|
|
1136
|
+
"How it charges: monthly subscription",
|
|
1137
|
+
"Activation moment: first project created after signup",
|
|
1138
|
+
"Churn-risk signals onboarding identified: trial ending unused; first failed payment",
|
|
1139
|
+
"Healthy-usage signals: weekly active project edits"
|
|
1140
|
+
].join("\n"),
|
|
1128
1141
|
identify: {
|
|
1129
1142
|
traits: [
|
|
1130
1143
|
{ name: "plan", description: "free | trial | pro" },
|
|
@@ -1173,425 +1186,1690 @@ function mockManifest(appId, config) {
|
|
|
1173
1186
|
};
|
|
1174
1187
|
}
|
|
1175
1188
|
|
|
1176
|
-
// src/
|
|
1177
|
-
import
|
|
1178
|
-
query
|
|
1179
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
1189
|
+
// src/engine/cli.ts
|
|
1190
|
+
import * as p from "@clack/prompts";
|
|
1180
1191
|
|
|
1181
|
-
// src/core/
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
1188
|
-
if (!isRepo) return { isRepo: false };
|
|
1189
|
-
const branch = (await run(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
|
1190
|
-
const head = await run(repoPath, ["rev-parse", "HEAD"]);
|
|
1191
|
-
return {
|
|
1192
|
-
isRepo: true,
|
|
1193
|
-
baseRef: head.ok ? head.stdout.trim() : void 0,
|
|
1194
|
-
branch: branch || void 0
|
|
1195
|
-
};
|
|
1192
|
+
// src/core/report.ts
|
|
1193
|
+
function scrubSummary(text) {
|
|
1194
|
+
return scrubTerminalControls(text).replace(/sk-ant-[A-Za-z0-9_-]+/g, "[redacted]").replace(/sk-[A-Za-z0-9]{16,}/g, "[redacted]").replace(/AKIA[0-9A-Z]{16}/g, "[redacted]").replace(/Bearer\s+[A-Za-z0-9._-]+/g, "[redacted]").replace(
|
|
1195
|
+
/\b(ANTHROPIC_(?:AUTH_TOKEN|API_KEY))\s*=\s*["']?[^"'\s]+["']?/gi,
|
|
1196
|
+
"$1=[redacted]"
|
|
1197
|
+
);
|
|
1196
1198
|
}
|
|
1197
|
-
async function
|
|
1198
|
-
if (
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
if (
|
|
1199
|
+
async function postRunReport(config, session, report) {
|
|
1200
|
+
if (config.offline) return { ok: true };
|
|
1201
|
+
try {
|
|
1202
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/report`, {
|
|
1203
|
+
method: "POST",
|
|
1204
|
+
headers: {
|
|
1205
|
+
"Content-Type": "application/json",
|
|
1206
|
+
Authorization: `Bearer ${session.token}`
|
|
1207
|
+
},
|
|
1208
|
+
body: JSON.stringify(report)
|
|
1209
|
+
});
|
|
1210
|
+
if (res.ok) return { ok: true };
|
|
1211
|
+
return {
|
|
1212
|
+
ok: false,
|
|
1213
|
+
status: res.status,
|
|
1214
|
+
detail: await responseDetail(res)
|
|
1215
|
+
};
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
return { ok: false, detail: detailSlice(errorMessage(err)) };
|
|
1209
1218
|
}
|
|
1210
|
-
return [...set];
|
|
1211
1219
|
}
|
|
1212
|
-
function
|
|
1213
|
-
|
|
1214
|
-
|
|
1220
|
+
async function responseDetail(res) {
|
|
1221
|
+
try {
|
|
1222
|
+
return detailSlice(await res.text());
|
|
1223
|
+
} catch (err) {
|
|
1224
|
+
return detailSlice(errorMessage(err));
|
|
1225
|
+
}
|
|
1215
1226
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
if (checkpoint.baseRef) {
|
|
1219
|
-
const res2 = await run(repoPath, [
|
|
1220
|
-
"ls-tree",
|
|
1221
|
-
"--name-only",
|
|
1222
|
-
checkpoint.baseRef,
|
|
1223
|
-
"--",
|
|
1224
|
-
file
|
|
1225
|
-
]);
|
|
1226
|
-
return res2.ok && res2.stdout.trim() !== "";
|
|
1227
|
-
}
|
|
1228
|
-
const res = await run(repoPath, ["ls-files", "--", file]);
|
|
1229
|
-
return res.ok && res.stdout.trim() !== "";
|
|
1227
|
+
function errorMessage(err) {
|
|
1228
|
+
return err instanceof Error ? err.message : String(err);
|
|
1230
1229
|
}
|
|
1231
|
-
|
|
1232
|
-
const
|
|
1233
|
-
return
|
|
1230
|
+
function detailSlice(detail) {
|
|
1231
|
+
const sliced = detail.slice(0, 200).trim();
|
|
1232
|
+
return sliced || void 0;
|
|
1234
1233
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
if (checkpoint.baseRef) {
|
|
1238
|
-
const reset = await run(repoPath, ["reset", "--hard", checkpoint.baseRef]);
|
|
1239
|
-
const clean2 = await run(repoPath, ["clean", "-fd"]);
|
|
1240
|
-
return reset.ok && clean2.ok;
|
|
1241
|
-
}
|
|
1242
|
-
const restore = await run(repoPath, ["restore", "."]);
|
|
1243
|
-
const clean = await run(repoPath, ["clean", "-fd"]);
|
|
1244
|
-
return restore.ok && clean.ok;
|
|
1234
|
+
function scrubTerminalControls(value) {
|
|
1235
|
+
return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
1245
1236
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1237
|
+
|
|
1238
|
+
// src/core/verify.ts
|
|
1239
|
+
async function pollFirstEvent(config, session, opts = {}) {
|
|
1240
|
+
if (config.offline) return { received: false };
|
|
1241
|
+
const timeoutMs = opts.timeoutMs ?? 12e4;
|
|
1242
|
+
const intervalMs = opts.intervalMs ?? 3e3;
|
|
1243
|
+
const deadline = Date.now() + timeoutMs;
|
|
1244
|
+
while (Date.now() < deadline) {
|
|
1245
|
+
if (opts.signal?.aborted) return { received: false };
|
|
1249
1246
|
try {
|
|
1250
|
-
|
|
1247
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/first-event`, {
|
|
1248
|
+
headers: { authorization: `Bearer ${session.token}` }
|
|
1249
|
+
});
|
|
1250
|
+
if (res.ok) {
|
|
1251
|
+
const body = await res.json();
|
|
1252
|
+
if (body.received) {
|
|
1253
|
+
return { received: true, eventType: body.event_type };
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1251
1256
|
} catch {
|
|
1252
|
-
snapshot.set(file, null);
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
return snapshot;
|
|
1256
|
-
}
|
|
1257
|
-
function snapshotsEqual(a, b) {
|
|
1258
|
-
if (a.size !== b.size) return false;
|
|
1259
|
-
for (const [file, content] of a) {
|
|
1260
|
-
const other = b.get(file);
|
|
1261
|
-
if (other === void 0) return false;
|
|
1262
|
-
if (content === null || other === null) {
|
|
1263
|
-
if (content !== other) return false;
|
|
1264
|
-
} else if (!content.equals(other)) {
|
|
1265
|
-
return false;
|
|
1266
1257
|
}
|
|
1258
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1267
1259
|
}
|
|
1268
|
-
return
|
|
1260
|
+
return { received: false };
|
|
1269
1261
|
}
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1262
|
+
|
|
1263
|
+
// src/engine/output.ts
|
|
1264
|
+
function formatProgressLine(event) {
|
|
1265
|
+
const parts = [event.phase];
|
|
1266
|
+
if (event.cluster) {
|
|
1267
|
+
parts.push(`cluster ${event.cluster.index}/${event.cluster.total}`);
|
|
1268
|
+
}
|
|
1269
|
+
if (event.file) {
|
|
1270
|
+
parts.push(event.file);
|
|
1271
|
+
}
|
|
1272
|
+
parts.push(event.action);
|
|
1273
|
+
return parts.join(" \xB7 ");
|
|
1274
|
+
}
|
|
1275
|
+
function createProgressSink(options) {
|
|
1276
|
+
const interval = options.heartbeatIntervalMs ?? 15e3;
|
|
1277
|
+
const now = options.now ?? (() => Date.now());
|
|
1278
|
+
let lastLineAt = 0;
|
|
1279
|
+
let lastLine = "";
|
|
1280
|
+
const writeLine = (line) => {
|
|
1281
|
+
lastLineAt = now();
|
|
1282
|
+
lastLine = line;
|
|
1283
|
+
options.write(line);
|
|
1284
|
+
};
|
|
1285
|
+
return {
|
|
1286
|
+
emit(event) {
|
|
1287
|
+
if (options.json) {
|
|
1288
|
+
writeLine(JSON.stringify({ kind: "progress", ...event }));
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const line = formatProgressLine(event);
|
|
1292
|
+
if (!options.isTTY && line === lastLine) {
|
|
1293
|
+
return;
|
|
1287
1294
|
}
|
|
1295
|
+
writeLine(line);
|
|
1296
|
+
},
|
|
1297
|
+
heartbeat(phase, action) {
|
|
1298
|
+
if (now() - lastLineAt < interval) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
if (options.json) {
|
|
1302
|
+
writeLine(JSON.stringify({ kind: "heartbeat", phase, action }));
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
writeLine(`${phase} \xB7 still working \xB7 ${action}`);
|
|
1288
1306
|
}
|
|
1289
|
-
|
|
1290
|
-
} catch {
|
|
1291
|
-
return false;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
|
-
async function repoFingerprint(repoPath) {
|
|
1295
|
-
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
1296
|
-
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename(repoPath)}`;
|
|
1297
|
-
return createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
1298
|
-
}
|
|
1299
|
-
function normalizeRemote(url) {
|
|
1300
|
-
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
1307
|
+
};
|
|
1301
1308
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1309
|
+
|
|
1310
|
+
// src/engine/orchestrator.ts
|
|
1311
|
+
import { basename, join as join6 } from "path";
|
|
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);
|
|
1316
1343
|
continue;
|
|
1317
1344
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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);
|
|
1324
1351
|
}
|
|
1325
1352
|
}
|
|
1326
|
-
for (const
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
+
});
|
|
1331
1365
|
}
|
|
1332
1366
|
}
|
|
1333
|
-
return
|
|
1367
|
+
return clusters;
|
|
1334
1368
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
return new RegExp(`(['"\`])${leftBoundary}${escapeRegExp(eventType)}${rightBoundary}\\1`);
|
|
1369
|
+
var TERMINAL_STATUSES = ["reviewed", "failed_resumable", "blocked"];
|
|
1370
|
+
function nextPendingCluster(clusters) {
|
|
1371
|
+
return clusters.find((cluster) => cluster.status === "pending" || cluster.status === "editing");
|
|
1339
1372
|
}
|
|
1340
|
-
function
|
|
1341
|
-
return
|
|
1373
|
+
function clustersComplete(clusters) {
|
|
1374
|
+
return clusters.every((cluster) => TERMINAL_STATUSES.includes(cluster.status));
|
|
1342
1375
|
}
|
|
1343
|
-
function
|
|
1344
|
-
return
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
child.on("close", (code) => resolve3({ ok: code === 0, stdout, stderr }));
|
|
1351
|
-
child.on("error", () => resolve3({ ok: false, stdout, stderr }));
|
|
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 };
|
|
1352
1383
|
});
|
|
1353
1384
|
}
|
|
1354
1385
|
|
|
1355
|
-
// src/
|
|
1356
|
-
import {
|
|
1386
|
+
// src/engine/integrate.ts
|
|
1387
|
+
import { writeFile } from "fs/promises";
|
|
1357
1388
|
import { join as join3 } from "path";
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1389
|
+
|
|
1390
|
+
// src/engine/bindings.ts
|
|
1391
|
+
function bindingsTargetFor(target9) {
|
|
1392
|
+
switch (target9) {
|
|
1393
|
+
case "swift":
|
|
1394
|
+
return "swift";
|
|
1395
|
+
case "flutter":
|
|
1396
|
+
return "dart";
|
|
1397
|
+
default:
|
|
1398
|
+
return "typescript";
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
function camelCase(code) {
|
|
1402
|
+
return code.split("_").map((part, index) => index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1403
|
+
}
|
|
1404
|
+
function generateBindings(target9, events) {
|
|
1405
|
+
switch (bindingsTargetFor(target9)) {
|
|
1406
|
+
case "swift":
|
|
1407
|
+
return generateSwift(events);
|
|
1408
|
+
case "dart":
|
|
1409
|
+
return generateDart(events);
|
|
1410
|
+
default:
|
|
1411
|
+
return generateTypeScript(events);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function generateTypeScript(events) {
|
|
1415
|
+
const lines = [
|
|
1416
|
+
"// Generated by @whisperr/wizard \u2014 do not edit by hand.",
|
|
1417
|
+
"// Call bindWhisperr(client) once right after the Whisperr SDK is initialized;",
|
|
1418
|
+
"// events tracked before binding are queued, never dropped.",
|
|
1419
|
+
"",
|
|
1420
|
+
"type WhisperrPayloadValue = string | number | boolean;",
|
|
1421
|
+
"type WhisperrClientLike = {",
|
|
1422
|
+
" track: (event: string, payload?: Record<string, WhisperrPayloadValue>) => unknown;",
|
|
1423
|
+
"};",
|
|
1424
|
+
"",
|
|
1425
|
+
"let boundClient: WhisperrClientLike | null = null;",
|
|
1426
|
+
"const queued: Array<[string, Record<string, WhisperrPayloadValue> | undefined]> = [];",
|
|
1427
|
+
"",
|
|
1428
|
+
"export function bindWhisperr(client: WhisperrClientLike): void {",
|
|
1429
|
+
" boundClient = client;",
|
|
1430
|
+
" while (queued.length > 0) {",
|
|
1431
|
+
" const [event, payload] = queued.shift()!;",
|
|
1432
|
+
" try { client.track(event, payload); } catch { /* analytics must never break the app */ }",
|
|
1433
|
+
" }",
|
|
1434
|
+
"}",
|
|
1435
|
+
"",
|
|
1436
|
+
"function emit(event: string, payload?: Record<string, WhisperrPayloadValue>): void {",
|
|
1437
|
+
" if (boundClient) {",
|
|
1438
|
+
" try { boundClient.track(event, payload); } catch { /* analytics must never break the app */ }",
|
|
1439
|
+
" } else {",
|
|
1440
|
+
" queued.push([event, payload]);",
|
|
1441
|
+
" }",
|
|
1442
|
+
"}",
|
|
1443
|
+
"",
|
|
1444
|
+
"export const WhisperrEvents = {"
|
|
1445
|
+
];
|
|
1446
|
+
for (const event of events) {
|
|
1447
|
+
const fields = Object.keys(event.payloadSchema);
|
|
1448
|
+
for (const field of fields) {
|
|
1449
|
+
lines.push(` /** ${field}: ${event.payloadSchema[field]} */`);
|
|
1450
|
+
}
|
|
1451
|
+
if (fields.length === 0) {
|
|
1452
|
+
lines.push(` ${camelCase(event.code)}(): void {`, ` emit("${event.code}");`, " },");
|
|
1453
|
+
} else {
|
|
1454
|
+
const params = fields.map((field) => `${camelCase(field)}: WhisperrPayloadValue`).join("; ");
|
|
1455
|
+
const entries = fields.map((field) => `"${field}": ${camelCase(field)}`).join(", ");
|
|
1456
|
+
lines.push(
|
|
1457
|
+
` ${camelCase(event.code)}(payload: { ${params} }): void {`,
|
|
1458
|
+
` const { ${fields.map((field) => camelCase(field)).join(", ")} } = payload;`,
|
|
1459
|
+
` emit("${event.code}", { ${entries} });`,
|
|
1460
|
+
" },"
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
lines.push("};", "");
|
|
1465
|
+
return {
|
|
1466
|
+
relativePath: "whisperr-events.ts",
|
|
1467
|
+
content: lines.join("\n"),
|
|
1468
|
+
bindInstruction: "Import { bindWhisperr } from the generated whisperr-events module and call bindWhisperr(client) once, immediately after the Whisperr SDK client is created at app startup."
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
function generateSwift(events) {
|
|
1472
|
+
const lines = [
|
|
1473
|
+
"// Generated by @whisperr/wizard \u2014 do not edit by hand.",
|
|
1474
|
+
"// Call WhisperrEvents.bind(track:) once right after the Whisperr SDK is ready;",
|
|
1475
|
+
"// events tracked before binding are queued, never dropped.",
|
|
1476
|
+
"",
|
|
1477
|
+
"import Foundation",
|
|
1478
|
+
"",
|
|
1479
|
+
"enum WhisperrEvents {",
|
|
1480
|
+
" typealias TrackFunction = (String, [String: String]) -> Void",
|
|
1481
|
+
" private static var boundTrack: TrackFunction?",
|
|
1482
|
+
" private static var queued: [(String, [String: String])] = []",
|
|
1483
|
+
" private static let lock = NSLock()",
|
|
1484
|
+
"",
|
|
1485
|
+
" static func bind(track: @escaping TrackFunction) {",
|
|
1486
|
+
" lock.lock()",
|
|
1487
|
+
" boundTrack = track",
|
|
1488
|
+
" let pending = queued",
|
|
1489
|
+
" queued.removeAll()",
|
|
1490
|
+
" lock.unlock()",
|
|
1491
|
+
" for (event, payload) in pending { track(event, payload) }",
|
|
1492
|
+
" }",
|
|
1493
|
+
"",
|
|
1494
|
+
" private static func emit(_ event: String, _ payload: [String: String] = [:]) {",
|
|
1495
|
+
" lock.lock()",
|
|
1496
|
+
" if let track = boundTrack {",
|
|
1497
|
+
" lock.unlock()",
|
|
1498
|
+
" track(event, payload)",
|
|
1499
|
+
" } else {",
|
|
1500
|
+
" queued.append((event, payload))",
|
|
1501
|
+
" lock.unlock()",
|
|
1502
|
+
" }",
|
|
1503
|
+
" }",
|
|
1504
|
+
""
|
|
1505
|
+
];
|
|
1506
|
+
for (const event of events) {
|
|
1507
|
+
const fields = Object.keys(event.payloadSchema);
|
|
1508
|
+
for (const field of fields) {
|
|
1509
|
+
lines.push(` /// ${field}: ${event.payloadSchema[field]}`);
|
|
1510
|
+
}
|
|
1511
|
+
if (fields.length === 0) {
|
|
1512
|
+
lines.push(` static func ${camelCase(event.code)}() {`, ` emit("${event.code}")`, " }", "");
|
|
1513
|
+
} else {
|
|
1514
|
+
const params = fields.map((field) => `${camelCase(field)}: String`).join(", ");
|
|
1515
|
+
const entries = fields.map((field) => `"${field}": ${camelCase(field)}`).join(", ");
|
|
1516
|
+
lines.push(
|
|
1517
|
+
` static func ${camelCase(event.code)}(${params}) {`,
|
|
1518
|
+
` emit("${event.code}", [${entries}])`,
|
|
1519
|
+
" }",
|
|
1520
|
+
""
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
lines.push("}", "");
|
|
1525
|
+
return {
|
|
1526
|
+
relativePath: "WhisperrEvents.swift",
|
|
1527
|
+
content: lines.join("\n"),
|
|
1528
|
+
bindInstruction: "Call WhisperrEvents.bind(track:) once, immediately after Whisperr SDK initialization completes, forwarding to the SDK's track call. String payload values only."
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
function generateDart(events) {
|
|
1532
|
+
const lines = [
|
|
1533
|
+
"// Generated by @whisperr/wizard \u2014 do not edit by hand.",
|
|
1534
|
+
"// Call WhisperrEvents.bind(track) once right after the Whisperr SDK is ready;",
|
|
1535
|
+
"// events tracked before binding are queued, never dropped.",
|
|
1536
|
+
"",
|
|
1537
|
+
"typedef WhisperrTrack = void Function(String event, Map<String, Object> payload);",
|
|
1538
|
+
"",
|
|
1539
|
+
"class WhisperrEvents {",
|
|
1540
|
+
" static WhisperrTrack? _track;",
|
|
1541
|
+
" static final List<MapEntry<String, Map<String, Object>>> _queued = [];",
|
|
1542
|
+
"",
|
|
1543
|
+
" static void bind(WhisperrTrack track) {",
|
|
1544
|
+
" _track = track;",
|
|
1545
|
+
" for (final entry in _queued) {",
|
|
1546
|
+
" track(entry.key, entry.value);",
|
|
1547
|
+
" }",
|
|
1548
|
+
" _queued.clear();",
|
|
1549
|
+
" }",
|
|
1550
|
+
"",
|
|
1551
|
+
" static void _emit(String event, [Map<String, Object> payload = const {}]) {",
|
|
1552
|
+
" final track = _track;",
|
|
1553
|
+
" if (track != null) {",
|
|
1554
|
+
" track(event, payload);",
|
|
1555
|
+
" } else {",
|
|
1556
|
+
" _queued.add(MapEntry(event, payload));",
|
|
1557
|
+
" }",
|
|
1558
|
+
" }",
|
|
1559
|
+
""
|
|
1560
|
+
];
|
|
1561
|
+
for (const event of events) {
|
|
1562
|
+
const fields = Object.keys(event.payloadSchema);
|
|
1563
|
+
for (const field of fields) {
|
|
1564
|
+
lines.push(` /// ${field}: ${event.payloadSchema[field]}`);
|
|
1565
|
+
}
|
|
1566
|
+
if (fields.length === 0) {
|
|
1567
|
+
lines.push(` static void ${camelCase(event.code)}() => _emit('${event.code}');`, "");
|
|
1568
|
+
} else {
|
|
1569
|
+
const params = fields.map((field) => `required Object ${camelCase(field)}`).join(", ");
|
|
1570
|
+
const entries = fields.map((field) => `'${field}': ${camelCase(field)}`).join(", ");
|
|
1571
|
+
lines.push(
|
|
1572
|
+
` static void ${camelCase(event.code)}({${params}}) =>`,
|
|
1573
|
+
` _emit('${event.code}', {${entries}});`,
|
|
1574
|
+
""
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
lines.push("}", "");
|
|
1579
|
+
return {
|
|
1580
|
+
relativePath: "whisperr_events.dart",
|
|
1581
|
+
content: lines.join("\n"),
|
|
1582
|
+
bindInstruction: "Call WhisperrEvents.bind(track) once, immediately after the Whisperr SDK is initialized, forwarding to the SDK's track call."
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// src/engine/budgets.ts
|
|
1587
|
+
var OVERFLOW_PATTERNS = [
|
|
1588
|
+
/prompt is too long/i,
|
|
1589
|
+
/context.{0,20}(length|window|limit).{0,20}exceed/i,
|
|
1590
|
+
/exceeds? .{0,20}context/i,
|
|
1591
|
+
/maximum context/i,
|
|
1592
|
+
/input is too long/i
|
|
1593
|
+
];
|
|
1594
|
+
var TRANSIENT_PATTERNS = [
|
|
1595
|
+
/rate.?limit/i,
|
|
1596
|
+
/overloaded/i,
|
|
1597
|
+
/timeout/i,
|
|
1598
|
+
/timed out/i,
|
|
1599
|
+
/econnreset/i,
|
|
1600
|
+
/socket hang up/i,
|
|
1601
|
+
/5\d\d/,
|
|
1602
|
+
/temporarily unavailable/i
|
|
1603
|
+
];
|
|
1604
|
+
function classifyFailure(message) {
|
|
1605
|
+
const text = message ?? "";
|
|
1606
|
+
if (OVERFLOW_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
1607
|
+
return "context_overflow";
|
|
1608
|
+
}
|
|
1609
|
+
if (TRANSIENT_PATTERNS.some((pattern) => pattern.test(text))) {
|
|
1610
|
+
return "transient";
|
|
1611
|
+
}
|
|
1612
|
+
return "fatal";
|
|
1613
|
+
}
|
|
1614
|
+
var MAX_CLUSTER_ATTEMPTS = 2;
|
|
1615
|
+
function nextClusterStatusAfterFailure(failure, attempts) {
|
|
1616
|
+
if (failure === "transient" && attempts < MAX_CLUSTER_ATTEMPTS) {
|
|
1617
|
+
return "pending";
|
|
1618
|
+
}
|
|
1619
|
+
if (failure === "fatal") {
|
|
1620
|
+
return "blocked";
|
|
1621
|
+
}
|
|
1622
|
+
return "failed_resumable";
|
|
1361
1623
|
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1624
|
+
|
|
1625
|
+
// src/engine/verify.ts
|
|
1626
|
+
import { spawn } from "child_process";
|
|
1627
|
+
import { readFileSync } from "fs";
|
|
1628
|
+
import { join as join2 } from "path";
|
|
1629
|
+
var JS_TARGETS = /* @__PURE__ */ new Set(["web-js", "nextjs", "node", "react-native"]);
|
|
1630
|
+
function resolveVerifySteps(repoPath, target9) {
|
|
1631
|
+
if (target9 === "flutter") {
|
|
1632
|
+
return [{ kind: "analyze", command: "flutter", args: ["analyze", "--no-pub"] }];
|
|
1633
|
+
}
|
|
1634
|
+
if (JS_TARGETS.has(target9)) {
|
|
1635
|
+
const scripts = packageScripts(repoPath);
|
|
1636
|
+
const steps = [];
|
|
1637
|
+
if (scripts.typecheck) {
|
|
1638
|
+
steps.push({ kind: "typecheck", command: npmCommand(), args: ["run", "typecheck"] });
|
|
1639
|
+
} else if (hasTsconfig(repoPath)) {
|
|
1640
|
+
steps.push({ kind: "typecheck", command: "npx", args: ["--no-install", "tsc", "--noEmit"] });
|
|
1641
|
+
}
|
|
1642
|
+
if (steps.length === 0 && scripts.build) {
|
|
1643
|
+
steps.push({ kind: "build", command: npmCommand(), args: ["run", "build"] });
|
|
1644
|
+
}
|
|
1645
|
+
return steps;
|
|
1366
1646
|
}
|
|
1367
|
-
|
|
1647
|
+
return [];
|
|
1648
|
+
}
|
|
1649
|
+
function packageScripts(repoPath) {
|
|
1368
1650
|
try {
|
|
1369
|
-
|
|
1651
|
+
const parsed = JSON.parse(readFileSync(join2(repoPath, "package.json"), "utf8"));
|
|
1652
|
+
return parsed.scripts ?? {};
|
|
1370
1653
|
} catch {
|
|
1371
|
-
return {
|
|
1654
|
+
return {};
|
|
1372
1655
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
let raw;
|
|
1656
|
+
}
|
|
1657
|
+
function hasTsconfig(repoPath) {
|
|
1376
1658
|
try {
|
|
1377
|
-
|
|
1659
|
+
readFileSync(join2(repoPath, "tsconfig.json"), "utf8");
|
|
1660
|
+
return true;
|
|
1378
1661
|
} catch {
|
|
1379
|
-
return
|
|
1662
|
+
return false;
|
|
1380
1663
|
}
|
|
1381
|
-
return sanitizeOpportunities(raw, manifest);
|
|
1382
1664
|
}
|
|
1383
|
-
function
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1665
|
+
function npmCommand() {
|
|
1666
|
+
return process.platform === "win32" ? "npm.cmd" : "npm";
|
|
1667
|
+
}
|
|
1668
|
+
function runVerifyStep(cwd, step, timeoutMs = 3e5) {
|
|
1669
|
+
return new Promise((resolve4) => {
|
|
1670
|
+
const child = spawn(step.command, step.args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
1671
|
+
let output = "";
|
|
1672
|
+
const capture = (chunk) => {
|
|
1673
|
+
if (output.length < 2e4) {
|
|
1674
|
+
output += String(chunk);
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
child.stdout.on("data", capture);
|
|
1678
|
+
child.stderr.on("data", capture);
|
|
1679
|
+
const timer = setTimeout(() => {
|
|
1680
|
+
child.kill("SIGKILL");
|
|
1681
|
+
}, timeoutMs);
|
|
1682
|
+
child.on("error", (error) => {
|
|
1683
|
+
clearTimeout(timer);
|
|
1684
|
+
resolve4({ ...step, ok: false, output: String(error) });
|
|
1685
|
+
});
|
|
1686
|
+
child.on("close", (exitCode) => {
|
|
1687
|
+
clearTimeout(timer);
|
|
1688
|
+
resolve4({ ...step, ok: exitCode === 0, output });
|
|
1689
|
+
});
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
async function runVerifySteps(cwd, steps) {
|
|
1693
|
+
const results = [];
|
|
1694
|
+
for (const step of steps) {
|
|
1695
|
+
results.push(await runVerifyStep(cwd, step));
|
|
1399
1696
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
seenInterventions.add(iv.code);
|
|
1406
|
-
out.interventions.push(iv);
|
|
1697
|
+
return results;
|
|
1698
|
+
}
|
|
1699
|
+
function verifiedStatus(results) {
|
|
1700
|
+
if (results.length === 0) {
|
|
1701
|
+
return "applied";
|
|
1407
1702
|
}
|
|
1408
|
-
|
|
1703
|
+
if (results.some((result) => !result.ok)) {
|
|
1704
|
+
return "applied";
|
|
1705
|
+
}
|
|
1706
|
+
if (results.some((result) => result.kind === "build")) {
|
|
1707
|
+
return "build_verified";
|
|
1708
|
+
}
|
|
1709
|
+
if (results.some((result) => result.kind === "typecheck" || result.kind === "analyze")) {
|
|
1710
|
+
return "type_verified";
|
|
1711
|
+
}
|
|
1712
|
+
return "syntax_verified";
|
|
1409
1713
|
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
method: "POST",
|
|
1414
|
-
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
1415
|
-
headers: {
|
|
1416
|
-
"Content-Type": "application/json",
|
|
1417
|
-
Authorization: `Bearer ${session.token}`
|
|
1418
|
-
},
|
|
1419
|
-
body: JSON.stringify({
|
|
1420
|
-
target: target9,
|
|
1421
|
-
repoFingerprint: repoFingerprint2,
|
|
1422
|
-
events: opportunities.events,
|
|
1423
|
-
interventions: opportunities.interventions
|
|
1424
|
-
})
|
|
1425
|
-
});
|
|
1426
|
-
if (!res.ok) {
|
|
1427
|
-
const body = await res.text().catch(() => "");
|
|
1428
|
-
throw new Error(
|
|
1429
|
-
`Submitting universe additions failed (${res.status})${body ? `: ${body.slice(0, 300)}` : ""}`
|
|
1430
|
-
);
|
|
1714
|
+
function runVerificationStatus(results) {
|
|
1715
|
+
if (results.length === 0) {
|
|
1716
|
+
return "unavailable";
|
|
1431
1717
|
}
|
|
1432
|
-
return
|
|
1718
|
+
return results.every((result) => result.ok) ? "passed" : "failed";
|
|
1433
1719
|
}
|
|
1434
|
-
|
|
1435
|
-
const
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1720
|
+
function verificationCommand(results) {
|
|
1721
|
+
const last = results[results.length - 1];
|
|
1722
|
+
return last ? [last.command, ...last.args].join(" ") : void 0;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/engine/integrate.ts
|
|
1726
|
+
var REVIEW_CHECKLIST = [
|
|
1727
|
+
"- Events must fire after the confirmed outcome (post-await/success), never on intent or button tap.",
|
|
1728
|
+
"- View-appearance events must be deduplicated against re-renders and navigation returns.",
|
|
1729
|
+
"- Debounced flows must emit after the debounce commits, not per keystroke/tap.",
|
|
1730
|
+
"- Denied/failed permission or authorization results must not be recorded as granted.",
|
|
1731
|
+
"- Never persist a sent/once marker before the track call actually executed.",
|
|
1732
|
+
"- Payloads must use only the generated wrapper fields \u2014 no extra fields, no personal identifiers."
|
|
1733
|
+
].join("\n");
|
|
1734
|
+
async function writeBindingsModule(worktree, target9, registered) {
|
|
1735
|
+
const bindings = generateBindings(target9, registered);
|
|
1736
|
+
await writeFile(join3(worktree.path, bindings.relativePath), bindings.content);
|
|
1737
|
+
return bindings;
|
|
1738
|
+
}
|
|
1739
|
+
async function runSdkSetupPass(deps, bindings) {
|
|
1740
|
+
deps.sink.emit({ phase: "binding", action: "installing SDK and wiring identify()" });
|
|
1741
|
+
const outcome = await deps.provider.invoke(
|
|
1742
|
+
{
|
|
1743
|
+
role: "edit",
|
|
1744
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
1745
|
+
userPrompt: [
|
|
1746
|
+
"CORE SETUP for the Whisperr SDK in this repository. Do exactly this, then stop:",
|
|
1747
|
+
"1. Add and install the Whisperr SDK dependency for this stack.",
|
|
1748
|
+
`2. Initialize it once at app startup with API key ${deps.ingestion.apiKey} and base URL ${deps.ingestion.baseUrl}.`,
|
|
1749
|
+
`3. The generated bindings module at ${bindings.relativePath} queues events until bound.`,
|
|
1750
|
+
` ${bindings.bindInstruction}`,
|
|
1751
|
+
" You may move the generated file to the idiomatic source directory (fix imports), but never edit its generated content.",
|
|
1752
|
+
"4. Wire identify() for the END USER (paying customer) at login success and session restore; reset() on logout.",
|
|
1753
|
+
" Never instrument admin/staff/operator paths.",
|
|
1754
|
+
"Do not instrument product events yet. Do not run builds."
|
|
1755
|
+
].join("\n"),
|
|
1756
|
+
tools: [],
|
|
1757
|
+
allowRepoWrite: true,
|
|
1758
|
+
cwd: deps.worktree.path
|
|
1441
1759
|
},
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1760
|
+
deps.models.edit,
|
|
1761
|
+
(action) => deps.sink.emit({ phase: "binding", action })
|
|
1762
|
+
);
|
|
1763
|
+
return outcome.kind === "completed";
|
|
1764
|
+
}
|
|
1765
|
+
function clusterPrompt(cluster, registered, bindings) {
|
|
1766
|
+
const byCode = new Map(registered.map((event) => [event.code, event]));
|
|
1767
|
+
const briefs = cluster.events.map((placement) => {
|
|
1768
|
+
const event = byCode.get(placement.eventCode);
|
|
1769
|
+
const fields = Object.keys(event?.payloadSchema ?? {});
|
|
1770
|
+
return [
|
|
1771
|
+
`- ${placement.eventCode}: call the generated wrapper from ${bindings.relativePath}`,
|
|
1772
|
+
` anchor: ${placement.file}${placement.symbol ? ` (${placement.symbol})` : ""}`,
|
|
1773
|
+
` why: ${event?.reasoning ?? ""}`,
|
|
1774
|
+
fields.length > 0 ? ` payload fields: ${fields.join(", ")}` : " payload: none"
|
|
1775
|
+
].join("\n");
|
|
1448
1776
|
});
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1777
|
+
return [
|
|
1778
|
+
"Instrument EXACTLY these events by calling their generated wrapper functions at the anchors.",
|
|
1779
|
+
"Use only the generated wrappers \u2014 never call the SDK's raw track() and never invent event names.",
|
|
1780
|
+
"",
|
|
1781
|
+
"Placement rules:",
|
|
1782
|
+
REVIEW_CHECKLIST,
|
|
1783
|
+
"",
|
|
1784
|
+
"Events for this cluster:",
|
|
1785
|
+
...briefs,
|
|
1786
|
+
"",
|
|
1787
|
+
"Only edit the listed files (plus imports of the bindings module). Then stop."
|
|
1788
|
+
].join("\n");
|
|
1457
1789
|
}
|
|
1458
|
-
async function
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1790
|
+
async function reviewCluster(deps, cluster, diff) {
|
|
1791
|
+
const outcome = await deps.provider.invoke(
|
|
1792
|
+
{
|
|
1793
|
+
role: "review",
|
|
1794
|
+
systemPrompt: [
|
|
1795
|
+
"You review an instrumentation diff against a strict checklist. Answer with a verdict line",
|
|
1796
|
+
"`VERDICT: pass` or `VERDICT: fail`, then bullet notes for anything wrong."
|
|
1797
|
+
].join("\n"),
|
|
1798
|
+
userPrompt: [
|
|
1799
|
+
"Checklist:",
|
|
1800
|
+
REVIEW_CHECKLIST,
|
|
1801
|
+
"",
|
|
1802
|
+
`Events expected: ${cluster.events.map((event) => event.eventCode).join(", ")}`,
|
|
1803
|
+
"",
|
|
1804
|
+
"Diff:",
|
|
1805
|
+
diff.length > 6e4 ? diff.slice(0, 6e4) + "\n\u2026(truncated)" : diff
|
|
1806
|
+
].join("\n"),
|
|
1807
|
+
tools: [],
|
|
1808
|
+
allowRepoWrite: false,
|
|
1809
|
+
cwd: deps.worktree.path
|
|
1810
|
+
},
|
|
1811
|
+
deps.models.review,
|
|
1812
|
+
(action) => deps.sink.emit({ phase: "reviewing", action })
|
|
1813
|
+
);
|
|
1814
|
+
if (outcome.kind !== "completed") {
|
|
1815
|
+
return { ok: false, notes: `review pass did not complete (${outcome.kind})` };
|
|
1816
|
+
}
|
|
1817
|
+
const ok = /verdict:\s*pass/i.test(outcome.text);
|
|
1818
|
+
return { ok, notes: outcome.text.trim() };
|
|
1819
|
+
}
|
|
1820
|
+
async function runClusterIntegration(deps, initialClusters, clusterDiff) {
|
|
1821
|
+
let clusters = initialClusters;
|
|
1822
|
+
const eventStatuses = /* @__PURE__ */ new Map();
|
|
1823
|
+
const bindings = generateBindings(deps.target, deps.registered);
|
|
1824
|
+
const steps = resolveVerifySteps(deps.worktree.path, deps.target);
|
|
1825
|
+
const total = clusters.length;
|
|
1826
|
+
for (; ; ) {
|
|
1827
|
+
const cluster = nextPendingCluster(clusters);
|
|
1828
|
+
if (!cluster) {
|
|
1829
|
+
break;
|
|
1830
|
+
}
|
|
1831
|
+
const index = clusters.findIndex((candidate) => candidate.id === cluster.id) + 1;
|
|
1832
|
+
clusters = transitionCluster(clusters, cluster.id, "editing");
|
|
1833
|
+
deps.saveClusters(clusters);
|
|
1834
|
+
deps.sink.emit({
|
|
1835
|
+
phase: "integrating",
|
|
1836
|
+
cluster: { index, total },
|
|
1837
|
+
file: cluster.files[0],
|
|
1838
|
+
action: "placing events"
|
|
1466
1839
|
});
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1840
|
+
const edit = await deps.provider.invoke(
|
|
1841
|
+
{
|
|
1842
|
+
role: "edit",
|
|
1843
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
1844
|
+
userPrompt: clusterPrompt(cluster, deps.registered, bindings),
|
|
1845
|
+
tools: [],
|
|
1846
|
+
allowRepoWrite: true,
|
|
1847
|
+
cwd: deps.worktree.path
|
|
1848
|
+
},
|
|
1849
|
+
deps.models.edit,
|
|
1850
|
+
(action) => deps.sink.emit({ phase: "integrating", cluster: { index, total }, action })
|
|
1851
|
+
);
|
|
1852
|
+
if (edit.kind !== "completed") {
|
|
1853
|
+
const failure = edit.kind === "context_overflow" ? "context_overflow" : edit.kind === "budget_exhausted" ? "context_overflow" : classifyFailure(edit.kind === "failed" ? edit.error : "");
|
|
1854
|
+
const nextStatus = nextClusterStatusAfterFailure(failure, cluster.attempts);
|
|
1855
|
+
clusters = transitionCluster(clusters, cluster.id, nextStatus, `edit ${edit.kind}`);
|
|
1856
|
+
deps.saveClusters(clusters);
|
|
1857
|
+
for (const placement of cluster.events) {
|
|
1858
|
+
eventStatuses.set(placement.eventCode, "failed");
|
|
1859
|
+
}
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
let statuses = "applied";
|
|
1863
|
+
if (steps.length > 0) {
|
|
1864
|
+
deps.sink.emit({ phase: "verifying", cluster: { index, total }, action: "running verifier" });
|
|
1865
|
+
const results = await runVerifySteps(deps.worktree.path, steps);
|
|
1866
|
+
statuses = verifiedStatus(results);
|
|
1867
|
+
if (results.some((result) => !result.ok) && cluster.attempts < MAX_CLUSTER_ATTEMPTS) {
|
|
1868
|
+
deps.sink.emit({ phase: "verifying", cluster: { index, total }, action: "repairing verification failure" });
|
|
1869
|
+
await deps.provider.invoke(
|
|
1870
|
+
{
|
|
1871
|
+
role: "edit",
|
|
1872
|
+
systemPrompt: deps.playbookSystemPrompt,
|
|
1873
|
+
userPrompt: [
|
|
1874
|
+
"The verification command failed after your edits. Fix ONLY the failure below, then stop.",
|
|
1875
|
+
"",
|
|
1876
|
+
results.map((result) => result.output).join("\n").slice(0, 2e4)
|
|
1877
|
+
].join("\n"),
|
|
1878
|
+
tools: [],
|
|
1879
|
+
allowRepoWrite: true,
|
|
1880
|
+
cwd: deps.worktree.path
|
|
1881
|
+
},
|
|
1882
|
+
deps.models.edit,
|
|
1883
|
+
(action) => deps.sink.emit({ phase: "verifying", cluster: { index, total }, action })
|
|
1884
|
+
);
|
|
1885
|
+
const retried = await runVerifySteps(deps.worktree.path, steps);
|
|
1886
|
+
statuses = verifiedStatus(retried);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
clusters = transitionCluster(clusters, cluster.id, "verified");
|
|
1890
|
+
deps.saveClusters(clusters);
|
|
1891
|
+
const diff = await clusterDiff(cluster.files);
|
|
1892
|
+
const review = await reviewCluster(deps, cluster, diff);
|
|
1893
|
+
const finalStatus = review.ok ? statuses === "applied" ? "applied" : "reviewed" : statuses;
|
|
1894
|
+
clusters = transitionCluster(clusters, cluster.id, "reviewed", review.ok ? void 0 : review.notes.slice(0, 500));
|
|
1895
|
+
deps.saveClusters(clusters);
|
|
1896
|
+
for (const placement of cluster.events) {
|
|
1897
|
+
eventStatuses.set(placement.eventCode, finalStatus);
|
|
1898
|
+
}
|
|
1472
1899
|
}
|
|
1900
|
+
if (!clustersComplete(clusters)) {
|
|
1901
|
+
throw new Error("cluster queue did not reach a terminal state");
|
|
1902
|
+
}
|
|
1903
|
+
return { clusters, eventStatuses };
|
|
1473
1904
|
}
|
|
1474
|
-
function
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1905
|
+
async function finalizeIntegration(deps) {
|
|
1906
|
+
const steps = resolveVerifySteps(deps.worktree.path, deps.target);
|
|
1907
|
+
deps.sink.emit({ phase: "verifying", action: "final verification pass" });
|
|
1908
|
+
const results = await runVerifySteps(deps.worktree.path, steps);
|
|
1909
|
+
return {
|
|
1910
|
+
results,
|
|
1911
|
+
status: runVerificationStatus(results),
|
|
1912
|
+
command: verificationCommand(results)
|
|
1913
|
+
};
|
|
1478
1914
|
}
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1915
|
+
|
|
1916
|
+
// src/engine/runs.ts
|
|
1917
|
+
import { createHash as createHash2 } from "crypto";
|
|
1918
|
+
var RunsApiError = class extends Error {
|
|
1919
|
+
constructor(message, status, code) {
|
|
1920
|
+
super(message);
|
|
1921
|
+
this.status = status;
|
|
1922
|
+
this.code = code;
|
|
1923
|
+
this.name = "RunsApiError";
|
|
1924
|
+
}
|
|
1925
|
+
status;
|
|
1926
|
+
code;
|
|
1927
|
+
};
|
|
1928
|
+
function itemIdempotencyKey(kind, code, extra = "") {
|
|
1929
|
+
return createHash2("sha256").update(`${kind}
|
|
1930
|
+
${code}
|
|
1931
|
+
${extra}`).digest("hex").slice(0, 32);
|
|
1932
|
+
}
|
|
1933
|
+
var RunsClient = class {
|
|
1934
|
+
constructor(config, session) {
|
|
1935
|
+
this.config = config;
|
|
1936
|
+
this.session = session;
|
|
1937
|
+
}
|
|
1938
|
+
config;
|
|
1939
|
+
session;
|
|
1940
|
+
async request(method, path, body) {
|
|
1941
|
+
let response;
|
|
1942
|
+
try {
|
|
1943
|
+
response = await fetch(`${this.config.apiBaseUrl}${path}`, {
|
|
1944
|
+
method,
|
|
1487
1945
|
headers: {
|
|
1488
1946
|
"Content-Type": "application/json",
|
|
1489
|
-
Authorization: `Bearer ${session.token}`
|
|
1947
|
+
Authorization: `Bearer ${this.session.token}`
|
|
1490
1948
|
},
|
|
1491
|
-
body:
|
|
1492
|
-
}
|
|
1949
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
1950
|
+
});
|
|
1951
|
+
} catch (error) {
|
|
1952
|
+
throw new RunsApiError(error instanceof Error ? error.message : "request failed");
|
|
1953
|
+
}
|
|
1954
|
+
let payload;
|
|
1955
|
+
const text = await response.text();
|
|
1956
|
+
try {
|
|
1957
|
+
payload = text.length > 0 ? JSON.parse(text) : {};
|
|
1958
|
+
} catch {
|
|
1959
|
+
payload = {};
|
|
1960
|
+
}
|
|
1961
|
+
if (!response.ok) {
|
|
1962
|
+
const detail = payload;
|
|
1963
|
+
throw new RunsApiError(
|
|
1964
|
+
detail.message ?? `runs API ${method} ${path} failed`,
|
|
1965
|
+
response.status,
|
|
1966
|
+
detail.code
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
return payload;
|
|
1970
|
+
}
|
|
1971
|
+
createOrResumeRun(input) {
|
|
1972
|
+
return this.request("POST", "/wizard/runs", { ...input });
|
|
1973
|
+
}
|
|
1974
|
+
getRun(runId) {
|
|
1975
|
+
return this.request("GET", `/wizard/runs/${runId}`);
|
|
1976
|
+
}
|
|
1977
|
+
patchRun(runId, patch) {
|
|
1978
|
+
return this.request("PATCH", `/wizard/runs/${runId}`, { ...patch });
|
|
1979
|
+
}
|
|
1980
|
+
writeItem(runId, kind, payload, idempotencyExtra = "") {
|
|
1981
|
+
const code = typeof payload.code === "string" ? payload.code : JSON.stringify(payload);
|
|
1982
|
+
return this.request("POST", `/wizard/runs/${runId}/items`, {
|
|
1983
|
+
idempotencyKey: itemIdempotencyKey(kind, code, idempotencyExtra),
|
|
1984
|
+
kind,
|
|
1985
|
+
payload
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
completeRun(runId, completion) {
|
|
1989
|
+
return this.request(
|
|
1990
|
+
"POST",
|
|
1991
|
+
`/wizard/runs/${runId}/complete`,
|
|
1992
|
+
{ ...completion }
|
|
1493
1993
|
);
|
|
1494
|
-
return res.status === 200;
|
|
1495
|
-
} catch {
|
|
1496
|
-
return false;
|
|
1497
1994
|
}
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
// src/engine/selection.ts
|
|
1998
|
+
import { z } from "zod";
|
|
1999
|
+
|
|
2000
|
+
// src/engine/prompts.ts
|
|
2001
|
+
var SURVEY_SYSTEM_PROMPT = [
|
|
2002
|
+
"You are the read-only survey pass of the Whisperr integration wizard.",
|
|
2003
|
+
"Map the repository so a later pass can pick the product's important lifecycle events.",
|
|
2004
|
+
"You cannot edit files. Be fast and decisive; read entry points and feature roots, not every file.",
|
|
2005
|
+
"",
|
|
2006
|
+
"Report, in plain markdown:",
|
|
2007
|
+
"1. Stack: frameworks, package manager, app entry point file.",
|
|
2008
|
+
"2. Feature areas: name, root directory, and the 1-3 files where committed user actions happen.",
|
|
2009
|
+
"3. The end-user identify() anchor: the login/signup success and session-restore call sites for the",
|
|
2010
|
+
" PAYING end user \u2014 never admin, staff, operator, or seller paths.",
|
|
2011
|
+
"4. Existing analytics calls, if any.",
|
|
2012
|
+
"5. Committed-action call sites worth instrumenting: file, function/symbol, and what the user just did.",
|
|
2013
|
+
"Cite concrete file paths for every claim. Do not propose event names yet."
|
|
2014
|
+
].join("\n");
|
|
2015
|
+
function surveyUserPrompt(repoPath, onboardingContext) {
|
|
2016
|
+
return [
|
|
2017
|
+
`Survey the repository at ${repoPath}.`,
|
|
2018
|
+
"",
|
|
2019
|
+
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
2020
|
+
"",
|
|
2021
|
+
"Produce the survey report now."
|
|
2022
|
+
].join("\n");
|
|
1498
2023
|
}
|
|
1499
|
-
|
|
1500
|
-
|
|
2024
|
+
var SELECTION_SYSTEM_PROMPT = [
|
|
2025
|
+
"You are the event-selection pass of the Whisperr integration wizard.",
|
|
2026
|
+
"From the survey report and business context, choose the IMPORTANT product events this codebase",
|
|
2027
|
+
"can actually emit, and propose each one with the propose_event tool.",
|
|
2028
|
+
"",
|
|
2029
|
+
"Rules \u2014 these are hard constraints:",
|
|
2030
|
+
"- Only propose an event with a concrete call site: a real file and function where the committed",
|
|
2031
|
+
" user action happens. Never propose absence/timeout/derived concepts (daily_log_missed,",
|
|
2032
|
+
" streak_broken) \u2014 the server derives those later.",
|
|
2033
|
+
"- Emit after confirmed outcomes, not intent: a purchase event fires on confirmed success, not on",
|
|
2034
|
+
" button tap; denied permissions are not grants.",
|
|
2035
|
+
"- Importance is anchored in the business context: the activation moment, churn-risk and healthy",
|
|
2036
|
+
" signals, and how the product charges. A handful of load-bearing events beats wide coverage.",
|
|
2037
|
+
"- An empty or small set is a valid outcome. There is no quota. Never invent events to look thorough.",
|
|
2038
|
+
"- Codes are lowercase snake_case, named for what happened (checkout_completed, not do_checkout).",
|
|
2039
|
+
"- payloadSchema lists the fields worth capturing with a short description of each \u2014 the important",
|
|
2040
|
+
" info only. NEVER include personal identifiers: no email, phone, names, addresses, birth dates,",
|
|
2041
|
+
" government ids, usernames, passwords, device ids, or IP addresses. Domain metrics are welcome.",
|
|
2042
|
+
"- The end user is the paying customer/consumer. Never instrument admin, staff, or operator actions.",
|
|
2043
|
+
"",
|
|
2044
|
+
"Call propose_event once per event. When done, call finish_selection with a one-line summary."
|
|
2045
|
+
].join("\n");
|
|
2046
|
+
function selectionUserPrompt(surveyReport, onboardingContext, existingEventCodes, rejectedCodes) {
|
|
2047
|
+
return [
|
|
2048
|
+
"Survey report:",
|
|
2049
|
+
surveyReport,
|
|
2050
|
+
"",
|
|
2051
|
+
onboardingContext ? "Business context from onboarding:\n" + onboardingContext : "",
|
|
2052
|
+
existingEventCodes.length > 0 ? "Already registered for this app (do not re-propose; other projects may own them):\n- " + existingEventCodes.join("\n- ") : "",
|
|
2053
|
+
rejectedCodes.length > 0 ? "Previously rejected by the user (do not re-propose):\n- " + rejectedCodes.join("\n- ") : "",
|
|
2054
|
+
"",
|
|
2055
|
+
"Propose the important events now."
|
|
2056
|
+
].filter((section) => section !== "").join("\n");
|
|
1501
2057
|
}
|
|
1502
|
-
function
|
|
1503
|
-
if (
|
|
1504
|
-
|
|
1505
|
-
|
|
2058
|
+
function renderOnboardingContext(context) {
|
|
2059
|
+
if (!context) {
|
|
2060
|
+
return "";
|
|
2061
|
+
}
|
|
2062
|
+
const rendered = JSON.stringify(context, null, 1);
|
|
2063
|
+
return rendered.length > 6e3 ? rendered.slice(0, 6e3) + "\n\u2026(truncated)" : rendered;
|
|
1506
2064
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
2065
|
+
|
|
2066
|
+
// src/engine/selection.ts
|
|
2067
|
+
var PII_SEGMENTS = /* @__PURE__ */ new Set([
|
|
2068
|
+
"email",
|
|
2069
|
+
"phone",
|
|
2070
|
+
"address",
|
|
2071
|
+
"surname",
|
|
2072
|
+
"dob",
|
|
2073
|
+
"birthday",
|
|
2074
|
+
"birthdate",
|
|
2075
|
+
"ssn",
|
|
2076
|
+
"passport",
|
|
2077
|
+
"password",
|
|
2078
|
+
"username"
|
|
2079
|
+
]);
|
|
2080
|
+
var PII_PAIRS = [
|
|
2081
|
+
["first", "name"],
|
|
2082
|
+
["last", "name"],
|
|
2083
|
+
["full", "name"],
|
|
2084
|
+
["user", "name"],
|
|
2085
|
+
["middle", "name"],
|
|
2086
|
+
["maiden", "name"],
|
|
2087
|
+
["birth", "date"],
|
|
2088
|
+
["date", "birth"],
|
|
2089
|
+
["national", "id"],
|
|
2090
|
+
["government", "id"],
|
|
2091
|
+
["device", "id"],
|
|
2092
|
+
["personal", "number"],
|
|
2093
|
+
["social", "security"],
|
|
2094
|
+
["tax", "id"],
|
|
2095
|
+
["drivers", "license"]
|
|
2096
|
+
];
|
|
2097
|
+
function isPIIField(name) {
|
|
2098
|
+
const segments = name.split("_");
|
|
2099
|
+
if (segments.some((segment) => PII_SEGMENTS.has(segment))) {
|
|
2100
|
+
return true;
|
|
2101
|
+
}
|
|
2102
|
+
for (let index = 0; index + 1 < segments.length; index += 1) {
|
|
2103
|
+
if (PII_PAIRS.some(([a, b]) => segments[index] === a && segments[index + 1] === b)) {
|
|
2104
|
+
return true;
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
return false;
|
|
1510
2108
|
}
|
|
1511
|
-
function
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
return value;
|
|
2109
|
+
function normalizeEventCode(value) {
|
|
2110
|
+
const collapsed = value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/_{2,}/g, "_");
|
|
2111
|
+
return /^[a-z]/.test(collapsed) ? collapsed : "";
|
|
1515
2112
|
}
|
|
1516
|
-
function
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
2113
|
+
function validateProposedEvent(input, seen, rejected) {
|
|
2114
|
+
const code = normalizeEventCode(input.code);
|
|
2115
|
+
if (code === "") {
|
|
2116
|
+
return { ok: false, reason: `event code ${JSON.stringify(input.code)} is not snake_case` };
|
|
2117
|
+
}
|
|
2118
|
+
if (seen.has(code)) {
|
|
2119
|
+
return { ok: false, reason: `event ${code} was already proposed` };
|
|
2120
|
+
}
|
|
2121
|
+
if (rejected.has(code)) {
|
|
2122
|
+
return { ok: false, reason: `event ${code} was rejected by the user \u2014 do not re-propose it` };
|
|
2123
|
+
}
|
|
2124
|
+
if (!input.name.trim() || !input.reasoning.trim() || !input.anchorFile.trim()) {
|
|
2125
|
+
return { ok: false, reason: "name, reasoning, and anchorFile are required" };
|
|
2126
|
+
}
|
|
2127
|
+
const schema = {};
|
|
2128
|
+
for (const [field, description] of Object.entries(input.payloadSchema ?? {})) {
|
|
2129
|
+
const normalizedField = normalizeEventCode(field);
|
|
2130
|
+
if (normalizedField === "" || !description.trim()) {
|
|
2131
|
+
return { ok: false, reason: `payload field ${JSON.stringify(field)} needs a snake_case name and a description` };
|
|
1526
2132
|
}
|
|
1527
|
-
if (
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const desc = asString(pr.description);
|
|
1532
|
-
properties.push({
|
|
1533
|
-
name,
|
|
1534
|
-
...desc ? { description: desc } : {},
|
|
1535
|
-
...pr.required === true ? { required: true } : {}
|
|
1536
|
-
});
|
|
2133
|
+
if (isPIIField(normalizedField)) {
|
|
2134
|
+
return { ok: false, reason: `payload field ${normalizedField} is a personal identifier and is not allowed` };
|
|
2135
|
+
}
|
|
2136
|
+
schema[normalizedField] = description.trim();
|
|
1537
2137
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
if (typeof link !== "object" || link === null) continue;
|
|
1541
|
-
const l = link;
|
|
1542
|
-
const interventionCode = normalizeCode(asString(l.interventionCode) ?? "");
|
|
1543
|
-
if (!interventionCode) continue;
|
|
1544
|
-
const weight = asWeight(l.weight);
|
|
1545
|
-
links.push({ interventionCode, ...weight !== void 0 ? { weight } : {} });
|
|
2138
|
+
if (Object.keys(schema).length > 20) {
|
|
2139
|
+
return { ok: false, reason: "payload schema exceeds 20 fields" };
|
|
1546
2140
|
}
|
|
1547
|
-
const side = normalizeSide(asString(e.side));
|
|
1548
2141
|
return {
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
2142
|
+
ok: true,
|
|
2143
|
+
event: {
|
|
2144
|
+
code,
|
|
2145
|
+
name: input.name.trim(),
|
|
2146
|
+
reasoning: input.reasoning.trim(),
|
|
2147
|
+
anchorFile: input.anchorFile.trim(),
|
|
2148
|
+
anchorSymbol: input.anchorSymbol?.trim() || void 0,
|
|
2149
|
+
payloadSchema: schema
|
|
2150
|
+
}
|
|
1558
2151
|
};
|
|
1559
2152
|
}
|
|
1560
|
-
function
|
|
1561
|
-
|
|
1562
|
-
const
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
label: asString(i.label),
|
|
1577
|
-
description: asString(i.description),
|
|
1578
|
-
rationale: asString(i.rationale),
|
|
1579
|
-
expectedEffect: asString(i.expectedEffect),
|
|
1580
|
-
confidence: asConfidence(i.confidence),
|
|
1581
|
-
...links.length ? { links } : {}
|
|
1582
|
-
};
|
|
2153
|
+
async function runSurvey(provider, models, repoPath, bootstrap, sink, resumeSessionId) {
|
|
2154
|
+
sink.emit({ phase: "surveying", action: "mapping the repository" });
|
|
2155
|
+
const outcome = await provider.invoke(
|
|
2156
|
+
{
|
|
2157
|
+
role: "survey",
|
|
2158
|
+
systemPrompt: SURVEY_SYSTEM_PROMPT,
|
|
2159
|
+
userPrompt: surveyUserPrompt(repoPath, renderOnboardingContext(bootstrap.onboardingContext)),
|
|
2160
|
+
tools: [],
|
|
2161
|
+
allowRepoWrite: false,
|
|
2162
|
+
cwd: repoPath,
|
|
2163
|
+
resumeSessionId
|
|
2164
|
+
},
|
|
2165
|
+
models.survey,
|
|
2166
|
+
(action) => sink.emit({ phase: "surveying", action })
|
|
2167
|
+
);
|
|
2168
|
+
return { report: outcome.kind === "completed" ? outcome.text : "", outcome };
|
|
1583
2169
|
}
|
|
1584
|
-
function
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
2170
|
+
async function runSelection(provider, models, repoPath, bootstrap, surveyReport, rejectedCodes, sink) {
|
|
2171
|
+
const proposed = [];
|
|
2172
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2173
|
+
const rejected = new Set(rejectedCodes);
|
|
2174
|
+
let summary = "";
|
|
2175
|
+
const proposeTool = {
|
|
2176
|
+
name: "propose_event",
|
|
2177
|
+
description: "Propose one important, concretely-anchored product event. Returns ok:true when accepted.",
|
|
2178
|
+
inputSchema: {
|
|
2179
|
+
code: z.string().describe("lowercase snake_case event code"),
|
|
2180
|
+
name: z.string().describe("short human-readable name"),
|
|
2181
|
+
reasoning: z.string().describe("why this event matters for this business"),
|
|
2182
|
+
anchorFile: z.string().describe("repository file where the committed action happens"),
|
|
2183
|
+
anchorSymbol: z.string().optional().describe("function/symbol at the anchor"),
|
|
2184
|
+
payloadSchema: z.record(z.string(), z.string()).optional().describe("important payload fields: name -> short description; no personal identifiers")
|
|
2185
|
+
},
|
|
2186
|
+
jsonSchema: {
|
|
2187
|
+
type: "object",
|
|
2188
|
+
properties: {
|
|
2189
|
+
code: { type: "string", description: "lowercase snake_case event code" },
|
|
2190
|
+
name: { type: "string", description: "short human-readable name" },
|
|
2191
|
+
reasoning: { type: "string", description: "why this event matters for this business" },
|
|
2192
|
+
anchorFile: { type: "string", description: "repository file where the committed action happens" },
|
|
2193
|
+
anchorSymbol: { type: "string", description: "function/symbol at the anchor" },
|
|
2194
|
+
payloadSchema: {
|
|
2195
|
+
type: "object",
|
|
2196
|
+
additionalProperties: { type: "string" },
|
|
2197
|
+
description: "important payload fields: name -> short description; no personal identifiers"
|
|
2198
|
+
}
|
|
2199
|
+
},
|
|
2200
|
+
required: ["code", "name", "reasoning", "anchorFile"]
|
|
2201
|
+
},
|
|
2202
|
+
handler: async (input) => {
|
|
2203
|
+
const candidate = {
|
|
2204
|
+
code: String(input.code ?? ""),
|
|
2205
|
+
name: String(input.name ?? ""),
|
|
2206
|
+
reasoning: String(input.reasoning ?? ""),
|
|
2207
|
+
anchorFile: String(input.anchorFile ?? ""),
|
|
2208
|
+
anchorSymbol: input.anchorSymbol === void 0 ? void 0 : String(input.anchorSymbol),
|
|
2209
|
+
payloadSchema: input.payloadSchema ?? {}
|
|
2210
|
+
};
|
|
2211
|
+
const validated = validateProposedEvent(candidate, seen, rejected);
|
|
2212
|
+
if (!validated.ok) {
|
|
2213
|
+
return { ok: false, reason: validated.reason };
|
|
2214
|
+
}
|
|
2215
|
+
seen.add(validated.event.code);
|
|
2216
|
+
proposed.push(validated.event);
|
|
2217
|
+
sink.emit({
|
|
2218
|
+
phase: "designing",
|
|
2219
|
+
file: validated.event.anchorFile,
|
|
2220
|
+
action: `proposed ${validated.event.code}`
|
|
2221
|
+
});
|
|
2222
|
+
return { ok: true, code: validated.event.code };
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
const finishTool = {
|
|
2226
|
+
name: "finish_selection",
|
|
2227
|
+
description: "Signal that every important event has been proposed.",
|
|
2228
|
+
inputSchema: { summary: z.string().describe("one line on what was selected and why") },
|
|
2229
|
+
jsonSchema: {
|
|
2230
|
+
type: "object",
|
|
2231
|
+
properties: { summary: { type: "string", description: "one line on what was selected and why" } },
|
|
2232
|
+
required: ["summary"]
|
|
2233
|
+
},
|
|
2234
|
+
handler: async (input) => {
|
|
2235
|
+
summary = String(input.summary ?? "");
|
|
2236
|
+
return { ok: true };
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
sink.emit({ phase: "designing", action: "selecting important events" });
|
|
2240
|
+
const outcome = await provider.invoke(
|
|
2241
|
+
{
|
|
2242
|
+
role: "design",
|
|
2243
|
+
systemPrompt: SELECTION_SYSTEM_PROMPT,
|
|
2244
|
+
userPrompt: selectionUserPrompt(
|
|
2245
|
+
surveyReport,
|
|
2246
|
+
renderOnboardingContext(bootstrap.onboardingContext),
|
|
2247
|
+
bootstrap.snapshot.events.map((event) => event.code),
|
|
2248
|
+
rejectedCodes
|
|
2249
|
+
),
|
|
2250
|
+
tools: [proposeTool, finishTool],
|
|
2251
|
+
allowRepoWrite: false,
|
|
2252
|
+
cwd: repoPath
|
|
2253
|
+
},
|
|
2254
|
+
models.design,
|
|
2255
|
+
(action) => sink.emit({ phase: "designing", action })
|
|
2256
|
+
);
|
|
2257
|
+
return {
|
|
2258
|
+
outcome: outcome.kind,
|
|
2259
|
+
proposed,
|
|
2260
|
+
summary,
|
|
2261
|
+
surveyReport,
|
|
2262
|
+
sessionId: outcome.sessionId
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
async function registerApprovedEvents(runs, runId, approved, sink) {
|
|
2266
|
+
const registered = [];
|
|
2267
|
+
for (const event of approved) {
|
|
2268
|
+
const result = await runs.writeItem(runId, "event", {
|
|
2269
|
+
code: event.code,
|
|
2270
|
+
name: event.name,
|
|
2271
|
+
reasoning: event.reasoning,
|
|
2272
|
+
...Object.keys(event.payloadSchema).length > 0 ? { payloadSchema: event.payloadSchema } : {}
|
|
2273
|
+
});
|
|
2274
|
+
const item = result.item;
|
|
2275
|
+
registered.push({ ...event, eventId: item?.id ?? result.id ?? "" });
|
|
2276
|
+
sink.emit({ phase: "persisting", file: event.anchorFile, action: `registered ${event.code}` });
|
|
2277
|
+
}
|
|
2278
|
+
return registered;
|
|
2279
|
+
}
|
|
2280
|
+
function placementsFor(registered) {
|
|
2281
|
+
return registered.map((event) => ({
|
|
2282
|
+
eventId: event.eventId,
|
|
2283
|
+
eventCode: event.code,
|
|
2284
|
+
file: event.anchorFile,
|
|
2285
|
+
symbol: event.anchorSymbol,
|
|
2286
|
+
status: "planned"
|
|
2287
|
+
}));
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
// src/engine/state.ts
|
|
2291
|
+
import { createHash as createHash3 } from "crypto";
|
|
2292
|
+
import { mkdirSync, readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
|
|
2293
|
+
import { homedir } from "os";
|
|
2294
|
+
import { join as join4 } from "path";
|
|
2295
|
+
|
|
2296
|
+
// src/engine/types.ts
|
|
2297
|
+
var ENGINE_PHASES = [
|
|
2298
|
+
"authorizing",
|
|
2299
|
+
"surveying",
|
|
2300
|
+
"designing",
|
|
2301
|
+
"persisting",
|
|
2302
|
+
"binding",
|
|
2303
|
+
"integrating",
|
|
2304
|
+
"reviewing",
|
|
2305
|
+
"verifying",
|
|
2306
|
+
"reporting",
|
|
2307
|
+
"completed"
|
|
2308
|
+
];
|
|
2309
|
+
|
|
2310
|
+
// src/engine/state.ts
|
|
2311
|
+
function stateDirectory() {
|
|
2312
|
+
const override = process.env.WHISPERR_WIZARD_STATE_DIR?.trim();
|
|
2313
|
+
if (override) {
|
|
2314
|
+
return override;
|
|
2315
|
+
}
|
|
2316
|
+
return join4(homedir(), ".whisperr", "wizard-runs");
|
|
2317
|
+
}
|
|
2318
|
+
function stateKey(apiBaseUrl, appId, repoFingerprint2) {
|
|
2319
|
+
return createHash3("sha256").update(`${apiBaseUrl}
|
|
2320
|
+
${appId}
|
|
2321
|
+
${repoFingerprint2}`).digest("hex").slice(0, 24);
|
|
2322
|
+
}
|
|
2323
|
+
function statePath(apiBaseUrl, appId, repoFingerprint2) {
|
|
2324
|
+
return join4(stateDirectory(), `${stateKey(apiBaseUrl, appId, repoFingerprint2)}.json`);
|
|
2325
|
+
}
|
|
2326
|
+
function saveRunState(state) {
|
|
2327
|
+
const path = statePath(state.apiBaseUrl, state.appId, state.repoFingerprint);
|
|
2328
|
+
mkdirSync(stateDirectory(), { recursive: true, mode: 448 });
|
|
2329
|
+
const payload = JSON.stringify({ ...state, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2);
|
|
2330
|
+
const temporary = `${path}.tmp`;
|
|
2331
|
+
writeFileSync(temporary, payload, { mode: 384 });
|
|
2332
|
+
renameSync(temporary, path);
|
|
2333
|
+
}
|
|
2334
|
+
function loadRunState(apiBaseUrl, appId, repoFingerprint2) {
|
|
2335
|
+
const path = statePath(apiBaseUrl, appId, repoFingerprint2);
|
|
2336
|
+
let raw;
|
|
2337
|
+
try {
|
|
2338
|
+
raw = readFileSync2(path, "utf8");
|
|
2339
|
+
} catch {
|
|
2340
|
+
return void 0;
|
|
2341
|
+
}
|
|
2342
|
+
let parsed;
|
|
2343
|
+
try {
|
|
2344
|
+
parsed = JSON.parse(raw);
|
|
2345
|
+
} catch {
|
|
2346
|
+
return void 0;
|
|
2347
|
+
}
|
|
2348
|
+
if (!isEngineRunState(parsed)) {
|
|
2349
|
+
return void 0;
|
|
2350
|
+
}
|
|
2351
|
+
if (parsed.apiBaseUrl !== apiBaseUrl || parsed.appId !== appId || parsed.repoFingerprint !== repoFingerprint2) {
|
|
2352
|
+
return void 0;
|
|
2353
|
+
}
|
|
2354
|
+
return parsed;
|
|
2355
|
+
}
|
|
2356
|
+
function isEngineRunState(value) {
|
|
2357
|
+
if (typeof value !== "object" || value === null) {
|
|
2358
|
+
return false;
|
|
2359
|
+
}
|
|
2360
|
+
const candidate = value;
|
|
2361
|
+
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;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
// src/engine/worktree.ts
|
|
2365
|
+
import { spawn as spawn2 } from "child_process";
|
|
2366
|
+
import { mkdtemp, writeFile as writeFile2 } from "fs/promises";
|
|
2367
|
+
import { tmpdir } from "os";
|
|
2368
|
+
import { join as join5 } from "path";
|
|
2369
|
+
var WorktreeError = class extends Error {
|
|
2370
|
+
constructor(code, message) {
|
|
2371
|
+
super(message);
|
|
2372
|
+
this.code = code;
|
|
2373
|
+
this.name = "WorktreeError";
|
|
2374
|
+
}
|
|
2375
|
+
code;
|
|
2376
|
+
};
|
|
2377
|
+
function git(cwd, args) {
|
|
2378
|
+
return new Promise((resolve4) => {
|
|
2379
|
+
const child = spawn2("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
2380
|
+
let stdout = "";
|
|
2381
|
+
let stderr = "";
|
|
2382
|
+
child.stdout.on("data", (chunk) => {
|
|
2383
|
+
stdout += String(chunk);
|
|
2384
|
+
});
|
|
2385
|
+
child.stderr.on("data", (chunk) => {
|
|
2386
|
+
stderr += String(chunk);
|
|
2387
|
+
});
|
|
2388
|
+
child.on("error", () => resolve4({ ok: false, stdout, stderr: "git is not available" }));
|
|
2389
|
+
child.on("close", (exitCode) => resolve4({ ok: exitCode === 0, stdout, stderr }));
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
async function must(cwd, args) {
|
|
2393
|
+
const result = await git(cwd, args);
|
|
2394
|
+
if (!result.ok) {
|
|
2395
|
+
throw new WorktreeError("git_failed", `git ${args.join(" ")}: ${result.stderr.trim()}`);
|
|
2396
|
+
}
|
|
2397
|
+
return result.stdout;
|
|
2398
|
+
}
|
|
2399
|
+
async function createWorktree(repoPath, force = false) {
|
|
2400
|
+
const inside = await git(repoPath, ["rev-parse", "--is-inside-work-tree"]);
|
|
2401
|
+
if (!inside.ok) {
|
|
2402
|
+
throw new WorktreeError("not_a_repo", "the target directory is not a git repository");
|
|
2403
|
+
}
|
|
2404
|
+
const status = await must(repoPath, ["status", "--porcelain"]);
|
|
2405
|
+
if (status.trim() !== "" && !force) {
|
|
2406
|
+
throw new WorktreeError(
|
|
2407
|
+
"dirty_repo",
|
|
2408
|
+
"the repository has uncommitted changes \u2014 commit or stash them, or rerun with --force"
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
const baseRef = (await must(repoPath, ["rev-parse", "HEAD"])).trim();
|
|
2412
|
+
const path = await mkdtemp(join5(tmpdir(), "whisperr-worktree-"));
|
|
2413
|
+
await must(repoPath, ["worktree", "add", "--detach", "--force", path, "HEAD"]);
|
|
2414
|
+
return { path, baseRef, repoPath };
|
|
2415
|
+
}
|
|
2416
|
+
async function worktreePatch(handle) {
|
|
2417
|
+
await must(handle.path, ["add", "-A"]);
|
|
2418
|
+
return must(handle.path, ["diff", "--cached", "--binary", handle.baseRef]);
|
|
2419
|
+
}
|
|
2420
|
+
async function applyPatchToRepo(handle, patch) {
|
|
2421
|
+
if (patch.trim() === "") {
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
await new Promise((resolve4, reject) => {
|
|
2425
|
+
const child = spawn2("git", ["apply", "--whitespace=nowarn"], {
|
|
2426
|
+
cwd: handle.repoPath,
|
|
2427
|
+
stdio: ["pipe", "ignore", "pipe"]
|
|
2428
|
+
});
|
|
2429
|
+
let stderr = "";
|
|
2430
|
+
child.stderr.on("data", (chunk) => {
|
|
2431
|
+
stderr += String(chunk);
|
|
2432
|
+
});
|
|
2433
|
+
child.on("error", (error) => reject(new WorktreeError("git_failed", String(error))));
|
|
2434
|
+
child.on("close", (exitCode) => {
|
|
2435
|
+
if (exitCode === 0) {
|
|
2436
|
+
resolve4();
|
|
2437
|
+
} else {
|
|
2438
|
+
reject(new WorktreeError("git_failed", `git apply failed: ${stderr.trim()}`));
|
|
2439
|
+
}
|
|
2440
|
+
});
|
|
2441
|
+
child.stdin.end(patch);
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
async function savePartialPatch(handle, destination) {
|
|
2445
|
+
const patch = await worktreePatch(handle);
|
|
2446
|
+
if (patch.trim() === "") {
|
|
2447
|
+
return void 0;
|
|
2448
|
+
}
|
|
2449
|
+
await writeFile2(destination, patch, { mode: 384 });
|
|
2450
|
+
return destination;
|
|
2451
|
+
}
|
|
2452
|
+
async function removeWorktree(handle) {
|
|
2453
|
+
await git(handle.repoPath, ["worktree", "remove", "--force", handle.path]);
|
|
2454
|
+
await git(handle.repoPath, ["worktree", "prune"]);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/engine/orchestrator.ts
|
|
2458
|
+
async function runEngine(input) {
|
|
2459
|
+
const runs = new RunsClient(input.config, input.session);
|
|
2460
|
+
let bootstrap;
|
|
2461
|
+
try {
|
|
2462
|
+
bootstrap = await runs.createOrResumeRun({
|
|
2463
|
+
repoFingerprint: input.repoFingerprint,
|
|
2464
|
+
displayName: basename(input.repoPath),
|
|
2465
|
+
target: input.target,
|
|
2466
|
+
kind: input.projectKind
|
|
2467
|
+
});
|
|
2468
|
+
} catch (error) {
|
|
2469
|
+
if (error instanceof RunsApiError && error.code === "wrong_universe_mode") {
|
|
2470
|
+
return { kind: "wrong_mode" };
|
|
2471
|
+
}
|
|
2472
|
+
throw error;
|
|
2473
|
+
}
|
|
2474
|
+
const runId = bootstrap.run.id;
|
|
2475
|
+
input.provider.onRunReady?.(runId);
|
|
2476
|
+
const patchPhase = async (phase, message) => {
|
|
2477
|
+
try {
|
|
2478
|
+
await runs.patchRun(runId, { phase, ...message ? { message } : {} });
|
|
2479
|
+
} catch {
|
|
2480
|
+
}
|
|
2481
|
+
};
|
|
2482
|
+
const failRun = async (error) => {
|
|
2483
|
+
try {
|
|
2484
|
+
await runs.patchRun(runId, { status: "failed", error: error.slice(0, 500) });
|
|
2485
|
+
} catch {
|
|
2486
|
+
}
|
|
2487
|
+
};
|
|
2488
|
+
let state = loadRunState(input.config.apiBaseUrl, input.session.appId, input.repoFingerprint) ?? freshState(input, runId);
|
|
2489
|
+
if (state.runId !== runId) {
|
|
2490
|
+
state = freshState(input, runId);
|
|
2491
|
+
}
|
|
2492
|
+
const persist = () => saveRunState(state);
|
|
2493
|
+
let registered;
|
|
2494
|
+
if (!state.designComplete) {
|
|
2495
|
+
await patchPhase("surveying");
|
|
2496
|
+
const survey = await runSurvey(
|
|
2497
|
+
input.provider,
|
|
2498
|
+
input.models,
|
|
2499
|
+
input.repoPath,
|
|
2500
|
+
bootstrap,
|
|
2501
|
+
input.sink,
|
|
2502
|
+
state.providerSessions.survey
|
|
2503
|
+
);
|
|
2504
|
+
if (survey.outcome.kind !== "completed") {
|
|
2505
|
+
await failRun(`survey ${survey.outcome.kind}`);
|
|
2506
|
+
return { kind: "aborted", reason: `survey ${survey.outcome.kind}` };
|
|
2507
|
+
}
|
|
2508
|
+
state.providerSessions.survey = survey.outcome.sessionId;
|
|
2509
|
+
persist();
|
|
2510
|
+
await patchPhase("designing");
|
|
2511
|
+
const selection = await runSelection(
|
|
2512
|
+
input.provider,
|
|
2513
|
+
input.models,
|
|
2514
|
+
input.repoPath,
|
|
2515
|
+
bootstrap,
|
|
2516
|
+
survey.report,
|
|
2517
|
+
state.tombstonedCodes,
|
|
2518
|
+
input.sink
|
|
2519
|
+
);
|
|
2520
|
+
if (selection.outcome !== "completed" && selection.proposed.length === 0) {
|
|
2521
|
+
await failRun(`selection ${selection.outcome}`);
|
|
2522
|
+
return { kind: "aborted", reason: `selection ${selection.outcome}` };
|
|
2523
|
+
}
|
|
2524
|
+
const approved = await input.callbacks.reviewEvents(selection.proposed);
|
|
2525
|
+
if (approved === null) {
|
|
2526
|
+
await failRun("selection rejected by user");
|
|
2527
|
+
return { kind: "aborted", reason: "selection rejected by user" };
|
|
2528
|
+
}
|
|
2529
|
+
const approvedCodes = new Set(approved.map((event) => event.code));
|
|
2530
|
+
for (const event of selection.proposed) {
|
|
2531
|
+
if (!approvedCodes.has(event.code) && !state.tombstonedCodes.includes(event.code)) {
|
|
2532
|
+
state.tombstonedCodes.push(event.code);
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
persist();
|
|
2536
|
+
await patchPhase("persisting", `registering ${approved.length} events`);
|
|
2537
|
+
try {
|
|
2538
|
+
registered = await registerApprovedEvents(runs, runId, approved, input.sink);
|
|
2539
|
+
} catch (error) {
|
|
2540
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2541
|
+
await failRun(`event registration failed: ${reason}`);
|
|
2542
|
+
persist();
|
|
2543
|
+
return { kind: "aborted", reason: `event registration failed: ${reason}` };
|
|
2544
|
+
}
|
|
2545
|
+
state.designComplete = true;
|
|
2546
|
+
state.clusters = deriveClusters(placementsFor(registered));
|
|
2547
|
+
persist();
|
|
2548
|
+
} else {
|
|
2549
|
+
registered = rebuildRegistered(state, bootstrap);
|
|
2550
|
+
input.sink.emit({ phase: "designing", action: `resumed with ${registered.length} registered events` });
|
|
2551
|
+
}
|
|
2552
|
+
if (registered.length === 0) {
|
|
2553
|
+
await patchPhase("reporting", "no events registered for this project");
|
|
2554
|
+
await completeRun(runs, runId, [], /* @__PURE__ */ new Map(), [], true, "unavailable", void 0);
|
|
2555
|
+
return {
|
|
2556
|
+
kind: "completed",
|
|
2557
|
+
runId,
|
|
2558
|
+
registered,
|
|
2559
|
+
eventStatuses: /* @__PURE__ */ new Map(),
|
|
2560
|
+
reportEvents: [],
|
|
2561
|
+
changedFiles: [],
|
|
2562
|
+
identifyWired: false,
|
|
2563
|
+
applied: false,
|
|
2564
|
+
summary: "No important events found for this project; nothing was instrumented."
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
await patchPhase("binding");
|
|
2568
|
+
let worktree;
|
|
2569
|
+
try {
|
|
2570
|
+
worktree = await createWorktree(input.repoPath, input.force ?? false);
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2573
|
+
await failRun(reason);
|
|
2574
|
+
return { kind: "aborted", reason };
|
|
2575
|
+
}
|
|
2576
|
+
state.worktreePath = worktree.path;
|
|
2577
|
+
persist();
|
|
2578
|
+
try {
|
|
2579
|
+
const bindings = await writeBindingsModule(worktree, input.target, registered);
|
|
2580
|
+
const deps = {
|
|
2581
|
+
provider: input.provider,
|
|
2582
|
+
models: input.models,
|
|
2583
|
+
worktree,
|
|
2584
|
+
target: input.target,
|
|
2585
|
+
playbookSystemPrompt: input.playbookSystemPrompt,
|
|
2586
|
+
registered,
|
|
2587
|
+
ingestion: bootstrap.ingestion,
|
|
2588
|
+
sink: input.sink,
|
|
2589
|
+
saveClusters: (clusters) => {
|
|
2590
|
+
state.clusters = clusters;
|
|
2591
|
+
persist();
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
const identifyWired = await runSdkSetupPass(deps, bindings);
|
|
2595
|
+
await patchPhase("integrating");
|
|
2596
|
+
const integration = await runClusterIntegration(
|
|
2597
|
+
deps,
|
|
2598
|
+
state.clusters,
|
|
2599
|
+
async () => worktreePatch(worktree)
|
|
2600
|
+
);
|
|
2601
|
+
state.clusters = integration.clusters;
|
|
2602
|
+
persist();
|
|
2603
|
+
const finalVerify = await finalizeIntegration(deps);
|
|
2604
|
+
const patch = await worktreePatch(worktree);
|
|
2605
|
+
const changedFiles2 = patchFiles(patch);
|
|
2606
|
+
await patchPhase("reporting");
|
|
2607
|
+
const evidence = buildEvidence(registered, integration.eventStatuses, changedFiles2);
|
|
2608
|
+
await completeRun(
|
|
2609
|
+
runs,
|
|
2610
|
+
runId,
|
|
2611
|
+
evidence,
|
|
2612
|
+
integration.eventStatuses,
|
|
2613
|
+
changedFiles2,
|
|
2614
|
+
identifyWired,
|
|
2615
|
+
finalVerify.status,
|
|
2616
|
+
finalVerify.command
|
|
2617
|
+
);
|
|
2618
|
+
const summaryLines = summarize(registered, integration.eventStatuses);
|
|
2619
|
+
const applied = patch.trim() !== "" && await input.callbacks.confirmApply(patch, summaryLines);
|
|
2620
|
+
if (applied) {
|
|
2621
|
+
await applyPatchToRepo(worktree, patch);
|
|
2622
|
+
}
|
|
2623
|
+
const partialPatchPath = applied ? void 0 : await savePartialPatch(worktree, join6(input.repoPath, ".whisperr-wizard.patch"));
|
|
2624
|
+
await removeWorktree(worktree);
|
|
2625
|
+
state.worktreePath = void 0;
|
|
2626
|
+
persist();
|
|
2627
|
+
const hasFailures = [...integration.eventStatuses.values()].some(
|
|
2628
|
+
(status) => status === "failed"
|
|
2629
|
+
);
|
|
2630
|
+
return {
|
|
2631
|
+
kind: hasFailures ? "partial" : "completed",
|
|
2632
|
+
runId,
|
|
2633
|
+
registered,
|
|
2634
|
+
eventStatuses: integration.eventStatuses,
|
|
2635
|
+
reportEvents: buildReportEvents(registered, integration.eventStatuses),
|
|
2636
|
+
changedFiles: changedFiles2,
|
|
2637
|
+
identifyWired,
|
|
2638
|
+
applied,
|
|
2639
|
+
partialPatchPath: partialPatchPath ?? void 0,
|
|
2640
|
+
summary: summaryLines.join("\n")
|
|
2641
|
+
};
|
|
2642
|
+
} catch (error) {
|
|
2643
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2644
|
+
await failRun(reason);
|
|
2645
|
+
const partialPatchPath = await savePartialPatch(
|
|
2646
|
+
worktree,
|
|
2647
|
+
join6(input.repoPath, ".whisperr-wizard.patch")
|
|
2648
|
+
).catch(() => void 0);
|
|
2649
|
+
persist();
|
|
2650
|
+
return {
|
|
2651
|
+
kind: "partial",
|
|
2652
|
+
runId,
|
|
2653
|
+
registered,
|
|
2654
|
+
eventStatuses: /* @__PURE__ */ new Map(),
|
|
2655
|
+
reportEvents: buildReportEvents(registered, /* @__PURE__ */ new Map()),
|
|
2656
|
+
changedFiles: [],
|
|
2657
|
+
identifyWired: false,
|
|
2658
|
+
applied: false,
|
|
2659
|
+
partialPatchPath: partialPatchPath ?? void 0,
|
|
2660
|
+
summary: `Integration stopped early: ${reason}. Registered events are saved; rerun to resume.`
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
function freshState(input, runId) {
|
|
2665
|
+
return {
|
|
2666
|
+
version: 1,
|
|
2667
|
+
apiBaseUrl: input.config.apiBaseUrl,
|
|
2668
|
+
appId: input.session.appId,
|
|
2669
|
+
repoFingerprint: input.repoFingerprint,
|
|
2670
|
+
runId,
|
|
2671
|
+
phase: "authorizing",
|
|
2672
|
+
providerSessions: {},
|
|
2673
|
+
designComplete: false,
|
|
2674
|
+
tombstonedCodes: [],
|
|
2675
|
+
clusters: [],
|
|
2676
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
function rebuildRegistered(state, bootstrap) {
|
|
2680
|
+
const byCode = new Map(bootstrap.snapshot.events.map((event) => [event.code, event]));
|
|
2681
|
+
const registered = [];
|
|
2682
|
+
for (const cluster of state.clusters) {
|
|
2683
|
+
for (const placement of cluster.events) {
|
|
2684
|
+
const event = byCode.get(placement.eventCode);
|
|
2685
|
+
if (!event) {
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
registered.push({
|
|
2689
|
+
code: event.code,
|
|
2690
|
+
name: event.name,
|
|
2691
|
+
reasoning: event.reasoning,
|
|
2692
|
+
eventId: event.id,
|
|
2693
|
+
anchorFile: placement.file,
|
|
2694
|
+
anchorSymbol: placement.symbol,
|
|
2695
|
+
payloadSchema: event.payloadSchema ?? {}
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return registered;
|
|
2700
|
+
}
|
|
2701
|
+
function buildEvidence(registered, statuses, changedFiles2) {
|
|
2702
|
+
const changed = new Set(changedFiles2);
|
|
2703
|
+
return registered.map((event) => {
|
|
2704
|
+
const status = statuses.get(event.code) ?? "planned";
|
|
2705
|
+
const needsFile = status !== "planned" && status !== "unsupported" && status !== "failed";
|
|
2706
|
+
const file = changed.has(event.anchorFile) ? event.anchorFile : changedFiles2[0];
|
|
2707
|
+
return {
|
|
2708
|
+
eventId: event.eventId,
|
|
2709
|
+
eventCode: event.code,
|
|
2710
|
+
...needsFile && file ? { file } : {},
|
|
2711
|
+
status
|
|
2712
|
+
};
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
async function completeRun(runs, runId, evidence, _statuses, changedFiles2, identifyWired, verificationStatus, verificationCommand2) {
|
|
2716
|
+
try {
|
|
2717
|
+
await runs.completeRun(runId, {
|
|
2718
|
+
changedFiles: changedFiles2,
|
|
2719
|
+
identifyWired,
|
|
2720
|
+
verificationStatus,
|
|
2721
|
+
...verificationCommand2 ? { verificationCommand: verificationCommand2 } : {},
|
|
2722
|
+
events: evidence
|
|
2723
|
+
});
|
|
2724
|
+
} catch {
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
function buildReportEvents(registered, statuses) {
|
|
2728
|
+
return registered.map((event) => {
|
|
2729
|
+
const status = statuses.get(event.code);
|
|
2730
|
+
const wired = status !== void 0 && status !== "failed" && status !== "unsupported" && status !== "planned";
|
|
2731
|
+
return {
|
|
2732
|
+
event_type: event.code,
|
|
2733
|
+
status: wired ? "wired" : "skipped",
|
|
2734
|
+
...wired ? { file: event.anchorFile } : {},
|
|
2735
|
+
...wired ? {} : { reason: status ?? "not integrated" }
|
|
2736
|
+
};
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
function summarize(registered, statuses) {
|
|
2740
|
+
return registered.map((event) => {
|
|
2741
|
+
const status = statuses.get(event.code) ?? "planned";
|
|
2742
|
+
return `${event.code}: ${status} (${event.anchorFile})`;
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
function patchFiles(patch) {
|
|
2746
|
+
const files = /* @__PURE__ */ new Set();
|
|
2747
|
+
for (const line of patch.split("\n")) {
|
|
2748
|
+
const match = /^diff --git a\/(.+?) b\//.exec(line);
|
|
2749
|
+
if (match?.[1]) {
|
|
2750
|
+
files.add(match[1]);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
return [...files];
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
// src/engine/providers/claude.ts
|
|
2757
|
+
import { createSdkMcpServer, query as query2, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
2758
|
+
|
|
2759
|
+
// src/core/agent.ts
|
|
2760
|
+
import {
|
|
2761
|
+
query
|
|
2762
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
2763
|
+
|
|
2764
|
+
// src/core/git.ts
|
|
2765
|
+
import { spawn as spawn3 } from "child_process";
|
|
2766
|
+
import { createHash as createHash4 } from "crypto";
|
|
2767
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2768
|
+
import { basename as basename2, join as join7 } from "path";
|
|
2769
|
+
async function takeCheckpoint(repoPath) {
|
|
2770
|
+
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
2771
|
+
if (!isRepo) return { isRepo: false };
|
|
2772
|
+
const branch = (await run(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
|
2773
|
+
const head = await run(repoPath, ["rev-parse", "HEAD"]);
|
|
2774
|
+
return {
|
|
2775
|
+
isRepo: true,
|
|
2776
|
+
baseRef: head.ok ? head.stdout.trim() : void 0,
|
|
2777
|
+
branch: branch || void 0
|
|
2778
|
+
};
|
|
2779
|
+
}
|
|
2780
|
+
async function changedFiles(repoPath, checkpoint) {
|
|
2781
|
+
if (!checkpoint.isRepo) return [];
|
|
2782
|
+
const tracked = await run(repoPath, ["diff", "--name-only"]);
|
|
2783
|
+
const untracked = await run(repoPath, [
|
|
2784
|
+
"ls-files",
|
|
2785
|
+
"--others",
|
|
2786
|
+
"--exclude-standard"
|
|
2787
|
+
]);
|
|
2788
|
+
const set = /* @__PURE__ */ new Set();
|
|
2789
|
+
for (const f of `${tracked.stdout}
|
|
2790
|
+
${untracked.stdout}`.split("\n")) {
|
|
2791
|
+
if (f.trim()) set.add(f.trim());
|
|
2792
|
+
}
|
|
2793
|
+
return [...set];
|
|
2794
|
+
}
|
|
2795
|
+
function revertHint(checkpoint) {
|
|
2796
|
+
if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
|
|
2797
|
+
return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
|
|
2798
|
+
}
|
|
2799
|
+
async function isWorkingTreeClean(repoPath) {
|
|
2800
|
+
const status = await run(repoPath, ["status", "--porcelain"]);
|
|
2801
|
+
return status.ok && status.stdout.trim() === "";
|
|
2802
|
+
}
|
|
2803
|
+
async function revertToCheckpoint(repoPath, checkpoint) {
|
|
2804
|
+
if (!checkpoint.isRepo) return false;
|
|
2805
|
+
if (checkpoint.baseRef) {
|
|
2806
|
+
const reset = await run(repoPath, ["reset", "--hard", checkpoint.baseRef]);
|
|
2807
|
+
const clean2 = await run(repoPath, ["clean", "-fd"]);
|
|
2808
|
+
return reset.ok && clean2.ok;
|
|
2809
|
+
}
|
|
2810
|
+
const restore = await run(repoPath, ["restore", "."]);
|
|
2811
|
+
const clean = await run(repoPath, ["clean", "-fd"]);
|
|
2812
|
+
return restore.ok && clean.ok;
|
|
2813
|
+
}
|
|
2814
|
+
async function repoFingerprint(repoPath) {
|
|
2815
|
+
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
2816
|
+
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename2(repoPath)}`;
|
|
2817
|
+
return createHash4("sha256").update(seed).digest("hex").slice(0, 16);
|
|
2818
|
+
}
|
|
2819
|
+
function normalizeRemote(url) {
|
|
2820
|
+
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
2821
|
+
}
|
|
2822
|
+
async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
2823
|
+
const wired = /* @__PURE__ */ new Map();
|
|
2824
|
+
const patterns = eventTypes.map((e) => ({
|
|
2825
|
+
eventType: e,
|
|
2826
|
+
trackRe: new RegExp(
|
|
2827
|
+
`\\btrack\\s*\\(\\s*[\\s\\S]{0,160}?['"\`]${escapeRegExp(e)}['"\`]`
|
|
2828
|
+
),
|
|
2829
|
+
literalRe: quotedLiteralRegExp(e)
|
|
2830
|
+
}));
|
|
2831
|
+
const contents = [];
|
|
2832
|
+
for (const file of files) {
|
|
2833
|
+
try {
|
|
2834
|
+
contents.push({ file, content: await readFile2(join7(repoPath, file), "utf8") });
|
|
2835
|
+
} catch {
|
|
2836
|
+
continue;
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
for (const { file, content } of contents) {
|
|
2840
|
+
for (const { eventType, trackRe } of patterns) {
|
|
2841
|
+
if (!wired.has(eventType) && trackRe.test(content)) {
|
|
2842
|
+
wired.set(eventType, file);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
for (const { file, content } of contents) {
|
|
2847
|
+
for (const { eventType, literalRe } of patterns) {
|
|
2848
|
+
if (!wired.has(eventType) && literalRe.test(content)) {
|
|
2849
|
+
wired.set(eventType, file);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
1594
2852
|
}
|
|
2853
|
+
return wired;
|
|
2854
|
+
}
|
|
2855
|
+
function quotedLiteralRegExp(eventType) {
|
|
2856
|
+
const leftBoundary = /^\w/.test(eventType) ? "\\b" : "";
|
|
2857
|
+
const rightBoundary = /\w$/.test(eventType) ? "\\b" : "";
|
|
2858
|
+
return new RegExp(`(['"\`])${leftBoundary}${escapeRegExp(eventType)}${rightBoundary}\\1`);
|
|
2859
|
+
}
|
|
2860
|
+
function escapeRegExp(s) {
|
|
2861
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2862
|
+
}
|
|
2863
|
+
function run(cwd, args) {
|
|
2864
|
+
return new Promise((resolve4) => {
|
|
2865
|
+
const child = spawn3("git", args, { cwd });
|
|
2866
|
+
let stdout = "";
|
|
2867
|
+
let stderr = "";
|
|
2868
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
2869
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
2870
|
+
child.on("close", (code) => resolve4({ ok: code === 0, stdout, stderr }));
|
|
2871
|
+
child.on("error", () => resolve4({ ok: false, stdout, stderr }));
|
|
2872
|
+
});
|
|
1595
2873
|
}
|
|
1596
2874
|
|
|
1597
2875
|
// src/core/playbooks/shared-prompt.ts
|
|
@@ -1761,64 +3039,60 @@ function renderEventsBrief(m) {
|
|
|
1761
3039
|
if (e.description) lines.push(` ${e.description}`);
|
|
1762
3040
|
if (e.properties?.length) {
|
|
1763
3041
|
lines.push(" properties to capture if available:");
|
|
1764
|
-
for (const
|
|
1765
|
-
const req =
|
|
1766
|
-
const desc =
|
|
1767
|
-
lines.push(` \xB7 ${
|
|
3042
|
+
for (const p3 of e.properties) {
|
|
3043
|
+
const req = p3.required ? " (required)" : "";
|
|
3044
|
+
const desc = p3.description ? ` \u2014 ${p3.description}` : "";
|
|
3045
|
+
lines.push(` \xB7 ${p3.name}${req}${desc}`);
|
|
1768
3046
|
}
|
|
1769
3047
|
}
|
|
1770
3048
|
}
|
|
1771
3049
|
return lines.join("\n");
|
|
1772
3050
|
}
|
|
1773
|
-
function renderOpportunitiesBrief(m
|
|
3051
|
+
function renderOpportunitiesBrief(m) {
|
|
1774
3052
|
const eventCodes = m.events.map((e) => e.eventType);
|
|
1775
3053
|
const interventionCodes = [
|
|
1776
|
-
|
|
3054
|
+
.../* @__PURE__ */ new Set([
|
|
3055
|
+
...m.events.flatMap((e) => e.interventions ?? []).map((i) => i.code),
|
|
3056
|
+
...(m.interventions ?? []).map((i) => i.code)
|
|
3057
|
+
])
|
|
1777
3058
|
];
|
|
1778
3059
|
return [
|
|
1779
|
-
"
|
|
1780
|
-
"
|
|
1781
|
-
"
|
|
1782
|
-
"
|
|
1783
|
-
"
|
|
1784
|
-
"
|
|
1785
|
-
"
|
|
1786
|
-
"NOT cover? Record those as PROPOSALS. Do NOT add track() calls for them.",
|
|
1787
|
-
`Write a single JSON file at the repo root named ${opportunitiesFile}:`,
|
|
3060
|
+
"Rules for `opportunities` \u2014 what this app's plan is MISSING:",
|
|
3061
|
+
"You are reading this app's real lifecycle anyway. Sweep it deliberately:",
|
|
3062
|
+
"walk each surface the plan should care about \u2014 signup and auth,",
|
|
3063
|
+
"billing/payments (renewals, failures, upgrades, downgrades, cancellation),",
|
|
3064
|
+
"the core engagement loop, support/feedback/refund flows, sharing/invites,",
|
|
3065
|
+
"and any expiry or dormancy mechanics you see \u2014 and for each ask: does a",
|
|
3066
|
+
"churn-relevant moment happen here that the plan does NOT cover?",
|
|
1788
3067
|
"",
|
|
1789
|
-
"
|
|
1790
|
-
'
|
|
1791
|
-
' "code": "snake_case_event", "label": "...", "description": "...",',
|
|
3068
|
+
"Entry shapes:",
|
|
3069
|
+
' events: [{"code": "snake_case_event", "label": "...", "description": "...",',
|
|
1792
3070
|
' "side": "frontend|backend|either", "rationale": "what in the code shows this",',
|
|
1793
3071
|
' "expectedEffect": "one sentence: what improves if this is adopted",',
|
|
1794
3072
|
' "confidence": 0.0-1.0,',
|
|
1795
3073
|
' "properties": [{"name": "...", "description": "...", "required": false}],',
|
|
1796
|
-
' "links": [{"interventionCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
1797
|
-
"
|
|
1798
|
-
'
|
|
1799
|
-
' "
|
|
1800
|
-
' "rationale": "...", "confidence": 0.0-1.0,',
|
|
1801
|
-
' "expectedEffect": "one sentence: what improves if this is adopted",',
|
|
1802
|
-
' "links": [{"eventCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
1803
|
-
" }]",
|
|
1804
|
-
"}",
|
|
3074
|
+
' "links": [{"interventionCode": "existing_or_proposed", "weight": 0.0-1.0}]}]',
|
|
3075
|
+
' interventions: [{"code": "snake_case_strategy", "label": "...", "description": "...",',
|
|
3076
|
+
' "rationale": "...", "confidence": 0.0-1.0, "expectedEffect": "...",',
|
|
3077
|
+
' "links": [{"eventCode": "existing_or_proposed", "weight": 0.0-1.0}]}]',
|
|
1805
3078
|
"",
|
|
1806
|
-
"Rules:",
|
|
1807
3079
|
`- The plan already covers these events: ${eventCodes.join(", ") || "(none)"}.`,
|
|
1808
3080
|
` And these interventions: ${interventionCodes.join(", ") || "(none)"}.`,
|
|
1809
3081
|
" Propose ONLY what is genuinely missing \u2014 never re-propose or rename these.",
|
|
1810
|
-
"-
|
|
1811
|
-
"
|
|
1812
|
-
"
|
|
1813
|
-
"
|
|
3082
|
+
"- These go straight into the customer's universe, and the events you propose",
|
|
3083
|
+
" get instrumented in this same run. That raises the bar on evidence, not the",
|
|
3084
|
+
" volume: every proposal needs concrete code backing in its rationale, with",
|
|
3085
|
+
" the file/flow named. VERIFY before you write it \u2014 re-open the file you are",
|
|
3086
|
+
" citing and confirm the flow exists as you describe. Drop anything you",
|
|
3087
|
+
" cannot point at real code; a false proposal costs more trust than a missed one.",
|
|
3088
|
+
"- Say where you saw it in `rationale` precisely enough to wire from later \u2014",
|
|
3089
|
+
" it is the only placement note the wiring pass will have for these.",
|
|
1814
3090
|
"- Be thorough, not shy: a feature-rich product typically yields 3-6 solid",
|
|
1815
|
-
" event proposals and 1-3 interventions.
|
|
1816
|
-
"
|
|
1817
|
-
"
|
|
1818
|
-
"- Every proposal should include expectedEffect: one sentence explaining what adoption should improve.",
|
|
3091
|
+
" event proposals and 1-3 interventions. Empty is only right when the plan",
|
|
3092
|
+
" genuinely covers the product's lifecycle \u2014 never because you stopped early.",
|
|
3093
|
+
"- Every proposal should include expectedEffect: one sentence on what adoption improves.",
|
|
1819
3094
|
"- Link each proposed event to the intervention(s) it should feed (existing",
|
|
1820
|
-
" codes or ones you propose
|
|
1821
|
-
"- This file is metadata for the wizard, not app code \u2014 write it and move on."
|
|
3095
|
+
" codes or ones you propose alongside it)."
|
|
1822
3096
|
].join("\n");
|
|
1823
3097
|
}
|
|
1824
3098
|
function coverageNote(coverage) {
|
|
@@ -2140,36 +3414,56 @@ function tokenizeShellSegment(segment) {
|
|
|
2140
3414
|
|
|
2141
3415
|
// src/core/agent.ts
|
|
2142
3416
|
async function runIntegrationAgent(opts) {
|
|
2143
|
-
const {
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
3417
|
+
const {
|
|
3418
|
+
repoPath,
|
|
3419
|
+
config,
|
|
3420
|
+
session,
|
|
3421
|
+
playbook,
|
|
3422
|
+
manifest,
|
|
3423
|
+
progress,
|
|
3424
|
+
onPlanReady,
|
|
3425
|
+
onOpportunitiesReady
|
|
3426
|
+
} = opts;
|
|
3427
|
+
applyModelAuthEnv(config, session);
|
|
3428
|
+
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
3429
|
+
const started = Date.now();
|
|
2147
3430
|
let costUsd = 0;
|
|
2148
3431
|
const summaries = [];
|
|
2149
3432
|
let repoMap = "";
|
|
2150
3433
|
let eventOutcomes = [];
|
|
2151
3434
|
const phaseTimings = [];
|
|
2152
3435
|
const BUDGET_FLOOR = 0.25;
|
|
3436
|
+
let planning = { repoMap: "", plan: null, opportunities: emptyOpportunities() };
|
|
2153
3437
|
if (config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2154
3438
|
try {
|
|
2155
|
-
const
|
|
2156
|
-
prompt:
|
|
3439
|
+
const pass = await runTimedPass("Planning your integration", {
|
|
3440
|
+
prompt: renderPlanningPrompt(repoPath, playbook, manifest),
|
|
2157
3441
|
systemPrompt: systemPrompt9,
|
|
2158
3442
|
repoPath,
|
|
2159
3443
|
model: config.plannerModel,
|
|
2160
3444
|
effort: "high",
|
|
2161
|
-
maxTurns:
|
|
2162
|
-
budgetUsd: Math.min(config.budgetUsd - costUsd, config.budgetUsd * 0.
|
|
3445
|
+
maxTurns: 60,
|
|
3446
|
+
budgetUsd: Math.min(config.budgetUsd - costUsd, config.budgetUsd * 0.3),
|
|
2163
3447
|
allowedTools: READ_ONLY_TOOLS,
|
|
2164
3448
|
progress
|
|
2165
3449
|
}, phaseTimings);
|
|
2166
|
-
costUsd +=
|
|
2167
|
-
|
|
3450
|
+
costUsd += pass.costUsd;
|
|
3451
|
+
planning = parsePlanningResult(pass.summary);
|
|
2168
3452
|
} catch (err) {
|
|
2169
|
-
debugNote(`
|
|
2170
|
-
|
|
3453
|
+
debugNote(`planning pass failed: ${err.message}`);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
repoMap = planning.repoMap;
|
|
3457
|
+
let scopedPlan = null;
|
|
3458
|
+
if (planning.plan) {
|
|
3459
|
+
scopedPlan = planForManifest(planning.plan, manifest);
|
|
3460
|
+
const reviewedPlan = await onPlanReady?.(scopedPlan);
|
|
3461
|
+
if (reviewedPlan !== void 0 && reviewedPlan !== null) {
|
|
3462
|
+
scopedPlan = planForManifest(reviewedPlan, manifest);
|
|
2171
3463
|
}
|
|
2172
3464
|
}
|
|
3465
|
+
let appliedOpportunityEvents = [];
|
|
3466
|
+
const opportunitiesProposed = planning.opportunities.events.length + planning.opportunities.interventions.length;
|
|
2173
3467
|
const corePrompt = [
|
|
2174
3468
|
`Integrate the Whisperr ${playbook.target.displayName} SDK \u2014 CORE SETUP ONLY.`,
|
|
2175
3469
|
`Project root: ${repoPath}`,
|
|
@@ -2212,6 +3506,13 @@ async function runIntegrationAgent(opts) {
|
|
|
2212
3506
|
costUsd += core.costUsd;
|
|
2213
3507
|
if (core.summary) summaries.push(`Core setup:
|
|
2214
3508
|
${core.summary}`);
|
|
3509
|
+
if (onOpportunitiesReady && opportunitiesProposed > 0 && core.ok) {
|
|
3510
|
+
try {
|
|
3511
|
+
appliedOpportunityEvents = await onOpportunitiesReady(planning.opportunities);
|
|
3512
|
+
} catch (err) {
|
|
3513
|
+
debugNote(`universe additions failed: ${err.message}`);
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
2215
3516
|
let eventsComplete = true;
|
|
2216
3517
|
if (manifest.events.length > 0 && config.budgetUsd - costUsd <= BUDGET_FLOOR) {
|
|
2217
3518
|
eventsComplete = false;
|
|
@@ -2224,20 +3525,7 @@ ${core.summary}`);
|
|
|
2224
3525
|
"Events: skipped \u2014 the spend limit was reached during core setup. Re-run with a higher WHISPERR_WIZARD_BUDGET_USD to instrument events."
|
|
2225
3526
|
);
|
|
2226
3527
|
} else if (manifest.events.length > 0) {
|
|
2227
|
-
|
|
2228
|
-
prompt: renderEventPlanPrompt(repoPath, playbook, manifest, repoMap),
|
|
2229
|
-
systemPrompt: systemPrompt9,
|
|
2230
|
-
repoPath,
|
|
2231
|
-
model: config.plannerModel,
|
|
2232
|
-
effort: "high",
|
|
2233
|
-
maxTurns: 40,
|
|
2234
|
-
budgetUsd: config.budgetUsd - costUsd,
|
|
2235
|
-
allowedTools: READ_ONLY_TOOLS,
|
|
2236
|
-
progress
|
|
2237
|
-
}, phaseTimings);
|
|
2238
|
-
costUsd += planPass.costUsd;
|
|
2239
|
-
const plan = parseEventPlan(planPass.summary);
|
|
2240
|
-
if (!plan) {
|
|
3528
|
+
if (!scopedPlan) {
|
|
2241
3529
|
debugNote("event plan parse failed; falling back to direct event wiring");
|
|
2242
3530
|
progress?.onActivity?.("Event plan was not parseable; falling back to direct wiring");
|
|
2243
3531
|
const direct = await runDirectEventsPass({
|
|
@@ -2257,21 +3545,23 @@ ${core.summary}`);
|
|
|
2257
3545
|
if (direct.pass.summary) summaries.push(`Events:
|
|
2258
3546
|
${direct.pass.summary}`);
|
|
2259
3547
|
} else {
|
|
2260
|
-
let scopedPlan = planForManifest(plan, manifest);
|
|
2261
|
-
const reviewedPlan = await onPlanReady?.(scopedPlan);
|
|
2262
|
-
if (reviewedPlan !== void 0 && reviewedPlan !== null) {
|
|
2263
|
-
scopedPlan = planForManifest(reviewedPlan, manifest);
|
|
2264
|
-
}
|
|
2265
3548
|
const placeEntries = scopedPlan.filter((entry) => entry.decision === "place");
|
|
2266
3549
|
const unsureEntries = scopedPlan.filter((entry) => entry.decision === "unsure");
|
|
2267
3550
|
let wire;
|
|
2268
|
-
|
|
3551
|
+
const hasWorkToWire = placeEntries.length > 0 || appliedOpportunityEvents.length > 0;
|
|
3552
|
+
if (hasWorkToWire && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2269
3553
|
wire = await runTimedPass("Instrumenting planned events", {
|
|
2270
|
-
prompt: renderEventWirePrompt(
|
|
3554
|
+
prompt: renderEventWirePrompt(
|
|
3555
|
+
repoPath,
|
|
3556
|
+
playbook,
|
|
3557
|
+
repoMap,
|
|
3558
|
+
placeEntries,
|
|
3559
|
+
appliedOpportunityEvents
|
|
3560
|
+
),
|
|
2271
3561
|
systemPrompt: systemPrompt9,
|
|
2272
3562
|
repoPath,
|
|
2273
3563
|
model: config.model,
|
|
2274
|
-
effort: "
|
|
3564
|
+
effort: "medium",
|
|
2275
3565
|
maxTurns: config.maxTurns,
|
|
2276
3566
|
budgetUsd: config.budgetUsd - costUsd,
|
|
2277
3567
|
progress
|
|
@@ -2279,7 +3569,7 @@ ${direct.pass.summary}`);
|
|
|
2279
3569
|
costUsd += wire.costUsd;
|
|
2280
3570
|
if (wire.summary) summaries.push(`Events:
|
|
2281
3571
|
${wire.summary}`);
|
|
2282
|
-
} else if (!
|
|
3572
|
+
} else if (!hasWorkToWire) {
|
|
2283
3573
|
summaries.push("Events: no events had a confident placement in this surface.");
|
|
2284
3574
|
}
|
|
2285
3575
|
let wired = await scanManifestEvents(repoPath, manifest);
|
|
@@ -2303,7 +3593,7 @@ ${followUp.summary}`);
|
|
|
2303
3593
|
wired = await scanManifestEvents(repoPath, manifest);
|
|
2304
3594
|
}
|
|
2305
3595
|
eventOutcomes = buildEventOutcomes(manifest, scopedPlan, wired);
|
|
2306
|
-
eventsComplete = !
|
|
3596
|
+
eventsComplete = !(wire?.maxedOut ?? false) && !(followUp?.maxedOut ?? false);
|
|
2307
3597
|
}
|
|
2308
3598
|
}
|
|
2309
3599
|
if (core.ok && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
@@ -2313,7 +3603,7 @@ ${followUp.summary}`);
|
|
|
2313
3603
|
`Project root: ${repoPath}`,
|
|
2314
3604
|
"",
|
|
2315
3605
|
...renderRepoMapSection(repoMap),
|
|
2316
|
-
"Run `git diff` and read the
|
|
3606
|
+
"Run `git diff` and read ONLY the code immediately around each change.",
|
|
2317
3607
|
"For every identify() and track() call, verify \u2014 and FIX in place:",
|
|
2318
3608
|
"1. Subject \u2014 keys to the END USER, never an admin/staff/operator/seller.",
|
|
2319
3609
|
" identify() sits on the SAME surface as account_created (same person).",
|
|
@@ -2327,21 +3617,19 @@ ${followUp.summary}`);
|
|
|
2327
3617
|
" / payment_source / amount / plan). Add the ones you missed.",
|
|
2328
3618
|
"4. Coverage \u2014 every gateway/callback path that should emit an event does;",
|
|
2329
3619
|
" no recurring or secondary path left as a dead zone.",
|
|
2330
|
-
"
|
|
2331
|
-
"
|
|
2332
|
-
"",
|
|
2333
|
-
renderOpportunitiesBrief(manifest, OPPORTUNITIES_FILE),
|
|
3620
|
+
"Work from the diff. Do NOT re-explore the repository, do NOT propose new",
|
|
3621
|
+
"events, and change ONLY what is genuinely wrong or incomplete. Be surgical.",
|
|
2334
3622
|
"",
|
|
2335
3623
|
"End with a one-line, plain-text note of what you corrected (or 'No",
|
|
2336
|
-
"corrections needed.')
|
|
3624
|
+
"corrections needed.')."
|
|
2337
3625
|
].join("\n");
|
|
2338
3626
|
const review = await runTimedPass("Reviewing & correcting placements", {
|
|
2339
3627
|
prompt: reviewPrompt,
|
|
2340
3628
|
systemPrompt: systemPrompt9,
|
|
2341
3629
|
repoPath,
|
|
2342
|
-
model: config.plannerModel,
|
|
3630
|
+
model: config.strictReview ? config.plannerModel : config.model,
|
|
2343
3631
|
effort: "medium",
|
|
2344
|
-
maxTurns: 40,
|
|
3632
|
+
maxTurns: config.strictReview ? 40 : 15,
|
|
2345
3633
|
budgetUsd: config.budgetUsd - costUsd,
|
|
2346
3634
|
progress
|
|
2347
3635
|
}, phaseTimings);
|
|
@@ -2364,57 +3652,11 @@ ${review.summary}`);
|
|
|
2364
3652
|
coreOk: core.ok,
|
|
2365
3653
|
eventsComplete,
|
|
2366
3654
|
eventOutcomes,
|
|
2367
|
-
repoMap: repoMap || void 0
|
|
3655
|
+
repoMap: repoMap || void 0,
|
|
3656
|
+
opportunitiesProposed,
|
|
3657
|
+
opportunitiesApplied: appliedOpportunityEvents.length
|
|
2368
3658
|
};
|
|
2369
3659
|
}
|
|
2370
|
-
async function runAdditionsInstrumentationPass(opts) {
|
|
2371
|
-
const { repoPath, config, session, playbook, acceptedEvents, budgetUsd, repoMap, progress } = opts;
|
|
2372
|
-
if (!acceptedEvents.length || budgetUsd <= 0) {
|
|
2373
|
-
return { summary: "", costUsd: 0, ran: false };
|
|
2374
|
-
}
|
|
2375
|
-
applyModelAuthEnv(config, session);
|
|
2376
|
-
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
2377
|
-
const eventLines = [];
|
|
2378
|
-
for (const e of acceptedEvents) {
|
|
2379
|
-
eventLines.push("");
|
|
2380
|
-
eventLines.push(`\u25A0 ${e.code}${e.label ? ` (${e.label})` : ""}`);
|
|
2381
|
-
if (e.description) eventLines.push(` ${e.description}`);
|
|
2382
|
-
if (e.rationale) eventLines.push(` where: ${e.rationale}`);
|
|
2383
|
-
if (e.properties?.length) {
|
|
2384
|
-
eventLines.push(" properties to capture if available:");
|
|
2385
|
-
for (const prop of e.properties) {
|
|
2386
|
-
eventLines.push(` \xB7 ${prop.name}${prop.required ? " (required)" : ""}${prop.description ? ` \u2014 ${prop.description}` : ""}`);
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
progress?.onPhase?.("Instrumenting the newly added events");
|
|
2391
|
-
const prompt = [
|
|
2392
|
-
"You proposed these events as universe opportunities while reviewing this",
|
|
2393
|
-
"repo, and the user just APPROVED adding them to the plan. Instrument them",
|
|
2394
|
-
"now with track(), exactly like the plan's other events: place each at the",
|
|
2395
|
-
"real call site (your own rationale tells you where you saw it), attach the",
|
|
2396
|
-
"properties available in scope, and skip anything that turns out not to have",
|
|
2397
|
-
"a clear trigger after all. The SDK is already installed and initialized.",
|
|
2398
|
-
`Project root: ${repoPath}`,
|
|
2399
|
-
"",
|
|
2400
|
-
...renderRepoMapSection(repoMap),
|
|
2401
|
-
"----- APPROVED NEW EVENTS -----",
|
|
2402
|
-
...eventLines,
|
|
2403
|
-
"",
|
|
2404
|
-
"----- END -----"
|
|
2405
|
-
].join("\n");
|
|
2406
|
-
const pass = await runPass({
|
|
2407
|
-
prompt,
|
|
2408
|
-
systemPrompt: systemPrompt9,
|
|
2409
|
-
repoPath,
|
|
2410
|
-
model: config.model,
|
|
2411
|
-
effort: config.effort,
|
|
2412
|
-
maxTurns: 25,
|
|
2413
|
-
budgetUsd,
|
|
2414
|
-
progress
|
|
2415
|
-
});
|
|
2416
|
-
return { summary: pass.summary, costUsd: pass.costUsd, ran: true };
|
|
2417
|
-
}
|
|
2418
3660
|
async function runRepairPass(opts) {
|
|
2419
3661
|
const {
|
|
2420
3662
|
repoPath,
|
|
@@ -2458,11 +3700,64 @@ async function runRepairPass(opts) {
|
|
|
2458
3700
|
}
|
|
2459
3701
|
var READ_ONLY_TOOLS = ["Read", "Glob", "Grep"];
|
|
2460
3702
|
var FULL_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
3703
|
+
var PLANNING_JSON_FENCE = /```json\s*([\s\S]*?)```/;
|
|
3704
|
+
function scrubTerminalText(value) {
|
|
3705
|
+
return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
3706
|
+
}
|
|
3707
|
+
function emptyOpportunities() {
|
|
3708
|
+
return { events: [], interventions: [] };
|
|
3709
|
+
}
|
|
3710
|
+
function parsePlanningResult(text) {
|
|
3711
|
+
const fenced = PLANNING_JSON_FENCE.exec(text);
|
|
3712
|
+
const payload = parseJsonObject(fenced?.[1]) ?? parseJsonObject(sliceLastJsonObject(text));
|
|
3713
|
+
const mapText = fenced ? text.slice(0, fenced.index) : text;
|
|
3714
|
+
const repoMap = sliceRepoMap(stripMapMarkers(mapText));
|
|
3715
|
+
if (!payload) {
|
|
3716
|
+
return { repoMap, plan: parseEventPlan(text), opportunities: emptyOpportunities() };
|
|
3717
|
+
}
|
|
3718
|
+
return {
|
|
3719
|
+
repoMap,
|
|
3720
|
+
plan: Array.isArray(payload.plan) ? coerceEventPlan(payload.plan) : null,
|
|
3721
|
+
opportunities: coerceOpportunities(payload.opportunities)
|
|
3722
|
+
};
|
|
3723
|
+
}
|
|
3724
|
+
function parseJsonObject(raw) {
|
|
3725
|
+
if (!raw?.trim()) return null;
|
|
3726
|
+
try {
|
|
3727
|
+
const parsed = JSON.parse(raw);
|
|
3728
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
3729
|
+
const obj = parsed;
|
|
3730
|
+
if ("plan" in obj || "opportunities" in obj) return obj;
|
|
3731
|
+
}
|
|
3732
|
+
} catch {
|
|
3733
|
+
}
|
|
3734
|
+
return null;
|
|
3735
|
+
}
|
|
3736
|
+
function sliceLastJsonObject(text) {
|
|
3737
|
+
const start = text.lastIndexOf("{\n");
|
|
3738
|
+
const from = start >= 0 ? start : text.indexOf("{");
|
|
3739
|
+
if (from < 0) return null;
|
|
3740
|
+
return sliceBalanced(text, from, "{", "}");
|
|
3741
|
+
}
|
|
3742
|
+
function stripMapMarkers(text) {
|
|
3743
|
+
return text.replace(/^-+\s*REPO MAP\s*-+$/gim, "").replace(/^-+\s*END REPO MAP\s*-+$/gim, "").trim();
|
|
3744
|
+
}
|
|
3745
|
+
function coerceOpportunities(raw) {
|
|
3746
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return emptyOpportunities();
|
|
3747
|
+
const doc = raw;
|
|
3748
|
+
return {
|
|
3749
|
+
events: Array.isArray(doc.events) ? doc.events : [],
|
|
3750
|
+
interventions: Array.isArray(doc.interventions) ? doc.interventions : []
|
|
3751
|
+
};
|
|
3752
|
+
}
|
|
2461
3753
|
function parseEventPlan(text) {
|
|
2462
3754
|
const parsed = parseFirstJsonArray(text);
|
|
2463
3755
|
if (!Array.isArray(parsed)) return null;
|
|
3756
|
+
return coerceEventPlan(parsed);
|
|
3757
|
+
}
|
|
3758
|
+
function coerceEventPlan(items) {
|
|
2464
3759
|
const entries = [];
|
|
2465
|
-
for (const item of
|
|
3760
|
+
for (const item of items) {
|
|
2466
3761
|
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2467
3762
|
const obj = item;
|
|
2468
3763
|
const event = typeof obj.event === "string" ? obj.event.trim() : "";
|
|
@@ -2473,7 +3768,10 @@ function parseEventPlan(text) {
|
|
|
2473
3768
|
const entry = {
|
|
2474
3769
|
event,
|
|
2475
3770
|
decision,
|
|
2476
|
-
|
|
3771
|
+
// Model-authored text that ends up in the customer's terminal and in
|
|
3772
|
+
// the run report — scrub it like every other string we take from the
|
|
3773
|
+
// model, so it cannot restyle or spoof the surrounding UI.
|
|
3774
|
+
reason: typeof obj.reason === "string" ? scrubTerminalText(obj.reason) : ""
|
|
2477
3775
|
};
|
|
2478
3776
|
if (typeof obj.file === "string" && obj.file.trim()) {
|
|
2479
3777
|
entry.file = obj.file.trim();
|
|
@@ -2491,7 +3789,7 @@ function parseEventPlan(text) {
|
|
|
2491
3789
|
function parseFirstJsonArray(text) {
|
|
2492
3790
|
const start = text.indexOf("[");
|
|
2493
3791
|
if (start < 0) return null;
|
|
2494
|
-
const json =
|
|
3792
|
+
const json = sliceBalanced(text, start, "[", "]");
|
|
2495
3793
|
if (!json) return null;
|
|
2496
3794
|
try {
|
|
2497
3795
|
return JSON.parse(json);
|
|
@@ -2499,7 +3797,7 @@ function parseFirstJsonArray(text) {
|
|
|
2499
3797
|
return null;
|
|
2500
3798
|
}
|
|
2501
3799
|
}
|
|
2502
|
-
function
|
|
3800
|
+
function sliceBalanced(text, start, open3, close) {
|
|
2503
3801
|
let depth = 0;
|
|
2504
3802
|
let inString = false;
|
|
2505
3803
|
let escaped = false;
|
|
@@ -2517,31 +3815,15 @@ function sliceJsonArray(text, start) {
|
|
|
2517
3815
|
}
|
|
2518
3816
|
if (ch === '"') {
|
|
2519
3817
|
inString = true;
|
|
2520
|
-
} else if (ch ===
|
|
3818
|
+
} else if (ch === open3) {
|
|
2521
3819
|
depth++;
|
|
2522
|
-
} else if (ch ===
|
|
3820
|
+
} else if (ch === close) {
|
|
2523
3821
|
depth--;
|
|
2524
3822
|
if (depth === 0) return text.slice(start, i + 1);
|
|
2525
3823
|
}
|
|
2526
3824
|
}
|
|
2527
3825
|
return null;
|
|
2528
3826
|
}
|
|
2529
|
-
function renderRepoMapPrompt(repoPath) {
|
|
2530
|
-
return [
|
|
2531
|
-
"Map this repository for a Whisperr SDK integration. This is READ-ONLY.",
|
|
2532
|
-
`Project root: ${repoPath}`,
|
|
2533
|
-
"",
|
|
2534
|
-
"Produce a concise structured repo map as final text with these exact sections:",
|
|
2535
|
-
"- framework + entry points",
|
|
2536
|
-
"- auth flows enumerated: END-USER vs admin/staff, with precise file paths",
|
|
2537
|
-
"- billing/payment/webhook surfaces",
|
|
2538
|
-
"- analytics/tracking wrapper if any",
|
|
2539
|
-
"- state management",
|
|
2540
|
-
"- directory guide: which dirs hold pages/controllers/services",
|
|
2541
|
-
"",
|
|
2542
|
-
"This map guides event placement. List file paths precisely. No prose beyond the map."
|
|
2543
|
-
].join("\n");
|
|
2544
|
-
}
|
|
2545
3827
|
function sliceRepoMap(map) {
|
|
2546
3828
|
return map.trim().slice(0, 8e3);
|
|
2547
3829
|
}
|
|
@@ -2550,30 +3832,46 @@ function renderRepoMapSection(repoMap) {
|
|
|
2550
3832
|
if (!map) return [];
|
|
2551
3833
|
return ["----- REPO MAP -----", map, "----- END REPO MAP -----", ""];
|
|
2552
3834
|
}
|
|
2553
|
-
function
|
|
3835
|
+
function renderPlanningPrompt(repoPath, playbook, manifest) {
|
|
2554
3836
|
return [
|
|
2555
|
-
`Plan Whisperr ${playbook.target.displayName}
|
|
3837
|
+
`Plan the Whisperr ${playbook.target.displayName} integration for this repo.`,
|
|
3838
|
+
"This pass is READ-ONLY \u2014 do not edit anything. You are deciding what the",
|
|
3839
|
+
"editing passes will do, so be precise about files and anchors.",
|
|
2556
3840
|
`Project root: ${repoPath}`,
|
|
2557
3841
|
"",
|
|
2558
|
-
|
|
2559
|
-
"Use the repo map and the events brief to decide where each event belongs.",
|
|
2560
|
-
"Return ONLY a JSON code block containing an array with this shape:",
|
|
2561
|
-
'[{"event":"event_name","decision":"place|skip|unsure","file":"path","anchor":"function/route/handler","properties":["prop"],"reason":"short reason"}]',
|
|
3842
|
+
"Produce your answer in TWO parts, in this order.",
|
|
2562
3843
|
"",
|
|
2563
|
-
"
|
|
3844
|
+
"PART 1 \u2014 the repo map, as plain text under these exact sections:",
|
|
3845
|
+
"- framework + entry points",
|
|
3846
|
+
"- auth flows enumerated: END-USER vs admin/staff, with precise file paths",
|
|
3847
|
+
"- billing/payment/webhook surfaces",
|
|
3848
|
+
"- analytics/tracking wrapper if any",
|
|
3849
|
+
"- state management",
|
|
3850
|
+
"- directory guide: which dirs hold pages/controllers/services",
|
|
3851
|
+
"List file paths precisely. No prose beyond the map.",
|
|
3852
|
+
"",
|
|
3853
|
+
"PART 2 \u2014 a single ```json fenced block, and nothing after it:",
|
|
3854
|
+
"{",
|
|
3855
|
+
' "plan": [{"event":"event_name","decision":"place|skip|unsure","file":"path",',
|
|
3856
|
+
' "anchor":"function/route/handler","properties":["prop"],"reason":"short reason"}],',
|
|
3857
|
+
' "opportunities": {"events": [], "interventions": []}',
|
|
3858
|
+
"}",
|
|
3859
|
+
"",
|
|
3860
|
+
"Rules for `plan` \u2014 one entry per event in the brief below:",
|
|
2564
3861
|
"- decision=place only when there is a concrete end-user call site in this repo.",
|
|
2565
3862
|
"- decision=skip when the event clearly lives on another surface or does not exist here.",
|
|
2566
3863
|
"- decision=unsure when there is a plausible surface but the exact anchor is not proven.",
|
|
2567
3864
|
"- For place decisions, include file and anchor precisely.",
|
|
2568
3865
|
"- properties should list only properties likely available at that anchor.",
|
|
2569
|
-
"
|
|
3866
|
+
"",
|
|
3867
|
+
renderOpportunitiesBrief(manifest),
|
|
2570
3868
|
"",
|
|
2571
3869
|
"----- EVENTS -----",
|
|
2572
3870
|
renderEventsBrief(manifest),
|
|
2573
3871
|
"----- END EVENTS -----"
|
|
2574
3872
|
].join("\n");
|
|
2575
3873
|
}
|
|
2576
|
-
function renderEventWirePrompt(repoPath, playbook, repoMap, entries) {
|
|
3874
|
+
function renderEventWirePrompt(repoPath, playbook, repoMap, entries, addedEvents = []) {
|
|
2577
3875
|
return [
|
|
2578
3876
|
`The Whisperr ${playbook.target.displayName} SDK is installed and initialized.`,
|
|
2579
3877
|
"Wire only the planned event placements below with track().",
|
|
@@ -2586,9 +3884,39 @@ function renderEventWirePrompt(repoPath, playbook, repoMap, entries) {
|
|
|
2586
3884
|
"",
|
|
2587
3885
|
"----- EVENT WIRING PLAN -----",
|
|
2588
3886
|
...renderPlanEntryLines(entries),
|
|
2589
|
-
"----- END EVENT WIRING PLAN -----"
|
|
3887
|
+
"----- END EVENT WIRING PLAN -----",
|
|
3888
|
+
// Events the planner proposed and the universe accepted this run. They have
|
|
3889
|
+
// no plan entry (they weren't in the manifest when the plan was written), so
|
|
3890
|
+
// the rationale carries the placement — the planner already saw the site.
|
|
3891
|
+
...addedEvents.length ? [
|
|
3892
|
+
"",
|
|
3893
|
+
"----- NEWLY ADDED EVENTS -----",
|
|
3894
|
+
"You proposed these while planning and they are now part of this app's",
|
|
3895
|
+
"universe. Wire them the same way \u2014 your own rationale says where you",
|
|
3896
|
+
"saw each one. Skip any that turn out not to have a clear trigger.",
|
|
3897
|
+
...renderAddedEventLines(addedEvents),
|
|
3898
|
+
"----- END NEWLY ADDED EVENTS -----"
|
|
3899
|
+
] : []
|
|
2590
3900
|
].join("\n");
|
|
2591
3901
|
}
|
|
3902
|
+
function renderAddedEventLines(events) {
|
|
3903
|
+
const lines = [];
|
|
3904
|
+
for (const event of events) {
|
|
3905
|
+
lines.push("");
|
|
3906
|
+
lines.push(`\u25A0 ${event.code}${event.label ? ` (${event.label})` : ""}`);
|
|
3907
|
+
if (event.description) lines.push(` ${event.description}`);
|
|
3908
|
+
if (event.rationale) lines.push(` where: ${event.rationale}`);
|
|
3909
|
+
if (event.properties?.length) {
|
|
3910
|
+
lines.push(" properties to capture if available:");
|
|
3911
|
+
for (const prop of event.properties) {
|
|
3912
|
+
lines.push(
|
|
3913
|
+
` \xB7 ${prop.name}${prop.required ? " (required)" : ""}${prop.description ? ` \u2014 ${prop.description}` : ""}`
|
|
3914
|
+
);
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
}
|
|
3918
|
+
return lines;
|
|
3919
|
+
}
|
|
2592
3920
|
function renderEventReconcilePrompt(repoPath, playbook, repoMap, entries) {
|
|
2593
3921
|
return [
|
|
2594
3922
|
`One targeted follow-up for Whisperr ${playbook.target.displayName} events.`,
|
|
@@ -2929,37 +4257,670 @@ function firstLine(text) {
|
|
|
2929
4257
|
const line = text.split("\n").find((l) => l.trim()) ?? text;
|
|
2930
4258
|
return short(line.trim(), 100);
|
|
2931
4259
|
}
|
|
2932
|
-
function short(s, max = 60) {
|
|
2933
|
-
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
4260
|
+
function short(s, max = 60) {
|
|
4261
|
+
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
// src/engine/providers/claude.ts
|
|
4265
|
+
var READ_ONLY_TOOLS2 = ["Read", "Glob", "Grep"];
|
|
4266
|
+
var EDIT_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
4267
|
+
function createClaudeProvider(config, session) {
|
|
4268
|
+
return {
|
|
4269
|
+
name: "claude",
|
|
4270
|
+
async invoke(invocation, roleConfig, onProgress) {
|
|
4271
|
+
applyModelAuthEnv(config, session);
|
|
4272
|
+
const hostTools = invocation.tools.map(
|
|
4273
|
+
(engineTool) => tool(engineTool.name, engineTool.description, engineTool.inputSchema, async (args) => {
|
|
4274
|
+
const result = await engineTool.handler(args);
|
|
4275
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
4276
|
+
})
|
|
4277
|
+
);
|
|
4278
|
+
const allowedTools = [
|
|
4279
|
+
...invocation.allowRepoWrite ? EDIT_TOOLS : READ_ONLY_TOOLS2,
|
|
4280
|
+
...invocation.tools.map((engineTool) => `mcp__engine__${engineTool.name}`)
|
|
4281
|
+
];
|
|
4282
|
+
let sessionId = invocation.resumeSessionId;
|
|
4283
|
+
let costUsd = 0;
|
|
4284
|
+
let turns = 0;
|
|
4285
|
+
const deadline = Date.now() + roleConfig.maxMs;
|
|
4286
|
+
const outcome = (kind, text = "", error = "") => kind === "completed" ? { kind, text, sessionId, costUsd, turns } : kind === "failed" ? { kind, error, sessionId, costUsd, turns } : { kind, sessionId, costUsd, turns };
|
|
4287
|
+
try {
|
|
4288
|
+
const response = query2({
|
|
4289
|
+
prompt: invocation.userPrompt,
|
|
4290
|
+
options: {
|
|
4291
|
+
model: roleConfig.model,
|
|
4292
|
+
cwd: invocation.cwd,
|
|
4293
|
+
systemPrompt: invocation.systemPrompt,
|
|
4294
|
+
thinking: { type: "adaptive" },
|
|
4295
|
+
...roleConfig.effort ? { effort: roleConfig.effort } : {},
|
|
4296
|
+
tools: [...allowedTools],
|
|
4297
|
+
...hostTools.length > 0 ? { mcpServers: { engine: createSdkMcpServer({ name: "engine", tools: hostTools }) } } : {},
|
|
4298
|
+
hooks: buildToolPolicyHooks(invocation.cwd),
|
|
4299
|
+
canUseTool: createToolPermissionCallback(invocation.cwd),
|
|
4300
|
+
maxTurns: roleConfig.maxTurns,
|
|
4301
|
+
...invocation.resumeSessionId ? { resume: invocation.resumeSessionId } : {},
|
|
4302
|
+
settingSources: []
|
|
4303
|
+
}
|
|
4304
|
+
});
|
|
4305
|
+
for await (const raw of response) {
|
|
4306
|
+
if (raw.session_id) {
|
|
4307
|
+
sessionId = raw.session_id;
|
|
4308
|
+
}
|
|
4309
|
+
if (raw.type === "assistant") {
|
|
4310
|
+
turns += 1;
|
|
4311
|
+
const block = raw.message?.content?.find(
|
|
4312
|
+
(candidate) => candidate.type === "text" || candidate.type === "tool_use"
|
|
4313
|
+
);
|
|
4314
|
+
if (block?.type === "text" && block.text?.trim()) {
|
|
4315
|
+
onProgress((block.text.trim().split("\n")[0] ?? "").slice(0, 120));
|
|
4316
|
+
} else if (block?.type === "tool_use" && block.name) {
|
|
4317
|
+
onProgress(`using ${block.name}`);
|
|
4318
|
+
}
|
|
4319
|
+
if (Date.now() > deadline) {
|
|
4320
|
+
await response.interrupt?.();
|
|
4321
|
+
return outcome("budget_exhausted");
|
|
4322
|
+
}
|
|
4323
|
+
} else if (raw.type === "result") {
|
|
4324
|
+
costUsd = raw.total_cost_usd ?? 0;
|
|
4325
|
+
const subtype = raw.subtype ?? "";
|
|
4326
|
+
if (subtype === "success") {
|
|
4327
|
+
return outcome("completed", raw.result ?? "");
|
|
4328
|
+
}
|
|
4329
|
+
if (subtype.includes("max_turns") || subtype.includes("max_budget")) {
|
|
4330
|
+
return outcome("budget_exhausted");
|
|
4331
|
+
}
|
|
4332
|
+
const detail = raw.result ?? subtype;
|
|
4333
|
+
if (classifyFailure(detail) === "context_overflow") {
|
|
4334
|
+
return outcome("context_overflow");
|
|
4335
|
+
}
|
|
4336
|
+
return outcome("failed", "", detail || "model run failed");
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
return outcome("failed", "", "model stream ended without a result");
|
|
4340
|
+
} catch (error) {
|
|
4341
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4342
|
+
if (classifyFailure(detail) === "context_overflow") {
|
|
4343
|
+
return outcome("context_overflow");
|
|
4344
|
+
}
|
|
4345
|
+
return outcome("failed", "", detail);
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
// src/engine/providers/openai.ts
|
|
4352
|
+
import { readdirSync, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
4353
|
+
import { join as join8, resolve as resolve2, sep } from "path";
|
|
4354
|
+
function jailedPath(cwd, candidate) {
|
|
4355
|
+
const resolved = resolve2(cwd, candidate);
|
|
4356
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + sep)) {
|
|
4357
|
+
throw new Error(`path ${candidate} escapes the repository`);
|
|
4358
|
+
}
|
|
4359
|
+
return resolved;
|
|
4360
|
+
}
|
|
4361
|
+
function fileTools(cwd, allowWrite) {
|
|
4362
|
+
const tools = [
|
|
4363
|
+
{
|
|
4364
|
+
name: "read_file",
|
|
4365
|
+
description: "Read a repository file (UTF-8, truncated at 40000 chars).",
|
|
4366
|
+
inputSchema: {},
|
|
4367
|
+
jsonSchema: {
|
|
4368
|
+
type: "object",
|
|
4369
|
+
properties: { path: { type: "string" } },
|
|
4370
|
+
required: ["path"]
|
|
4371
|
+
},
|
|
4372
|
+
handler: async (input) => {
|
|
4373
|
+
const content = readFileSync3(jailedPath(cwd, String(input.path ?? "")), "utf8");
|
|
4374
|
+
return { content: content.slice(0, 4e4) };
|
|
4375
|
+
}
|
|
4376
|
+
},
|
|
4377
|
+
{
|
|
4378
|
+
name: "list_dir",
|
|
4379
|
+
description: "List entries of a repository directory.",
|
|
4380
|
+
inputSchema: {},
|
|
4381
|
+
jsonSchema: {
|
|
4382
|
+
type: "object",
|
|
4383
|
+
properties: { path: { type: "string" } },
|
|
4384
|
+
required: ["path"]
|
|
4385
|
+
},
|
|
4386
|
+
handler: async (input) => {
|
|
4387
|
+
const target9 = jailedPath(cwd, String(input.path ?? "."));
|
|
4388
|
+
const entries = readdirSync(target9).map((entry) => {
|
|
4389
|
+
const kind = statSync(join8(target9, entry)).isDirectory() ? "dir" : "file";
|
|
4390
|
+
return `${kind}:${entry}`;
|
|
4391
|
+
});
|
|
4392
|
+
return { entries: entries.slice(0, 500) };
|
|
4393
|
+
}
|
|
4394
|
+
}
|
|
4395
|
+
];
|
|
4396
|
+
if (allowWrite) {
|
|
4397
|
+
tools.push(
|
|
4398
|
+
{
|
|
4399
|
+
name: "write_file",
|
|
4400
|
+
description: "Create or overwrite a repository file with the given content.",
|
|
4401
|
+
inputSchema: {},
|
|
4402
|
+
jsonSchema: {
|
|
4403
|
+
type: "object",
|
|
4404
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
4405
|
+
required: ["path", "content"]
|
|
4406
|
+
},
|
|
4407
|
+
handler: async (input) => {
|
|
4408
|
+
writeFileSync2(jailedPath(cwd, String(input.path ?? "")), String(input.content ?? ""));
|
|
4409
|
+
return { ok: true };
|
|
4410
|
+
}
|
|
4411
|
+
},
|
|
4412
|
+
{
|
|
4413
|
+
name: "replace_in_file",
|
|
4414
|
+
description: "Replace one exact occurrence of a string in a repository file.",
|
|
4415
|
+
inputSchema: {},
|
|
4416
|
+
jsonSchema: {
|
|
4417
|
+
type: "object",
|
|
4418
|
+
properties: {
|
|
4419
|
+
path: { type: "string" },
|
|
4420
|
+
oldText: { type: "string" },
|
|
4421
|
+
newText: { type: "string" }
|
|
4422
|
+
},
|
|
4423
|
+
required: ["path", "oldText", "newText"]
|
|
4424
|
+
},
|
|
4425
|
+
handler: async (input) => {
|
|
4426
|
+
const path = jailedPath(cwd, String(input.path ?? ""));
|
|
4427
|
+
const content = readFileSync3(path, "utf8");
|
|
4428
|
+
const oldText = String(input.oldText ?? "");
|
|
4429
|
+
if (!content.includes(oldText)) {
|
|
4430
|
+
return { ok: false, reason: "oldText not found" };
|
|
4431
|
+
}
|
|
4432
|
+
writeFileSync2(path, content.replace(oldText, String(input.newText ?? "")));
|
|
4433
|
+
return { ok: true };
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
);
|
|
4437
|
+
}
|
|
4438
|
+
return tools;
|
|
4439
|
+
}
|
|
4440
|
+
function createOpenAIProvider(config, session) {
|
|
4441
|
+
let runId;
|
|
4442
|
+
let conversationId;
|
|
4443
|
+
const gatewayFetch = async (path, body) => {
|
|
4444
|
+
if (!runId) {
|
|
4445
|
+
throw new Error("OpenAI provider used before the run was created");
|
|
4446
|
+
}
|
|
4447
|
+
const response = await fetch(
|
|
4448
|
+
`${config.apiBaseUrl}/wizard/openai/runs/${encodeURIComponent(runId)}${path}`,
|
|
4449
|
+
{
|
|
4450
|
+
method: "POST",
|
|
4451
|
+
headers: {
|
|
4452
|
+
"Content-Type": "application/json",
|
|
4453
|
+
Authorization: `Bearer ${session.token}`
|
|
4454
|
+
},
|
|
4455
|
+
body: JSON.stringify(body)
|
|
4456
|
+
}
|
|
4457
|
+
);
|
|
4458
|
+
const text = await response.text();
|
|
4459
|
+
let payload;
|
|
4460
|
+
try {
|
|
4461
|
+
payload = text ? JSON.parse(text) : {};
|
|
4462
|
+
} catch {
|
|
4463
|
+
payload = {};
|
|
4464
|
+
}
|
|
4465
|
+
if (!response.ok) {
|
|
4466
|
+
const detail = payload.message ?? text;
|
|
4467
|
+
const error = new Error(detail || `gateway request failed (${response.status})`);
|
|
4468
|
+
error.code = payload.code;
|
|
4469
|
+
throw error;
|
|
4470
|
+
}
|
|
4471
|
+
return payload;
|
|
4472
|
+
};
|
|
4473
|
+
const ensureConversation = async (resumeId) => {
|
|
4474
|
+
if (conversationId) {
|
|
4475
|
+
return conversationId;
|
|
4476
|
+
}
|
|
4477
|
+
if (resumeId) {
|
|
4478
|
+
conversationId = resumeId;
|
|
4479
|
+
return resumeId;
|
|
4480
|
+
}
|
|
4481
|
+
try {
|
|
4482
|
+
const created = await gatewayFetch("/v1/conversations", {});
|
|
4483
|
+
conversationId = created.id;
|
|
4484
|
+
} catch (error) {
|
|
4485
|
+
if (error.code !== "conversation_already_bound") {
|
|
4486
|
+
throw error;
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
if (!conversationId) {
|
|
4490
|
+
throw new Error("wizard run already has a bound conversation; rerun with its resume state");
|
|
4491
|
+
}
|
|
4492
|
+
return conversationId;
|
|
4493
|
+
};
|
|
4494
|
+
return {
|
|
4495
|
+
name: "openai-gateway",
|
|
4496
|
+
onRunReady(readyRunId) {
|
|
4497
|
+
runId = readyRunId;
|
|
4498
|
+
},
|
|
4499
|
+
async invoke(invocation, roleConfig, onProgress) {
|
|
4500
|
+
let costUsd = 0;
|
|
4501
|
+
let turns = 0;
|
|
4502
|
+
const outcome = (kind, text = "", error = "") => kind === "completed" ? { kind, text, sessionId: conversationId, costUsd, turns } : kind === "failed" ? { kind, error, sessionId: conversationId, costUsd, turns } : { kind, sessionId: conversationId, costUsd, turns };
|
|
4503
|
+
const allTools = [...invocation.tools, ...fileTools(invocation.cwd, invocation.allowRepoWrite)];
|
|
4504
|
+
const toolsByName = new Map(allTools.map((tool2) => [tool2.name, tool2]));
|
|
4505
|
+
const toolDefs = allTools.map((tool2) => ({
|
|
4506
|
+
type: "function",
|
|
4507
|
+
name: tool2.name,
|
|
4508
|
+
description: tool2.description,
|
|
4509
|
+
parameters: tool2.jsonSchema
|
|
4510
|
+
}));
|
|
4511
|
+
try {
|
|
4512
|
+
const conversation = await ensureConversation(invocation.resumeSessionId);
|
|
4513
|
+
const deadline = Date.now() + roleConfig.maxMs;
|
|
4514
|
+
let input = [
|
|
4515
|
+
{ role: "user", content: invocation.userPrompt }
|
|
4516
|
+
];
|
|
4517
|
+
for (let turn = 0; turn < roleConfig.maxTurns; turn += 1) {
|
|
4518
|
+
turns = turn + 1;
|
|
4519
|
+
if (Date.now() > deadline) {
|
|
4520
|
+
return outcome("budget_exhausted");
|
|
4521
|
+
}
|
|
4522
|
+
const response = await gatewayFetch("/v1/responses", {
|
|
4523
|
+
model: roleConfig.model,
|
|
4524
|
+
instructions: invocation.systemPrompt,
|
|
4525
|
+
conversation,
|
|
4526
|
+
input,
|
|
4527
|
+
tools: toolDefs,
|
|
4528
|
+
...roleConfig.effort ? { reasoning: { effort: roleConfig.effort } } : {}
|
|
4529
|
+
});
|
|
4530
|
+
const items = response.output ?? [];
|
|
4531
|
+
const calls = items.filter(
|
|
4532
|
+
(item) => item.type === "function_call"
|
|
4533
|
+
);
|
|
4534
|
+
if (calls.length === 0) {
|
|
4535
|
+
const text = response.output_text ?? items.filter((item) => item.type === "message").map(
|
|
4536
|
+
(item) => Array.isArray(item.content) ? item.content.map((block) => String(block.text ?? "")).join("") : ""
|
|
4537
|
+
).join("\n");
|
|
4538
|
+
return outcome("completed", String(text ?? ""));
|
|
4539
|
+
}
|
|
4540
|
+
const results = [];
|
|
4541
|
+
for (const call of calls) {
|
|
4542
|
+
onProgress(`using ${call.name}`);
|
|
4543
|
+
const tool2 = toolsByName.get(call.name);
|
|
4544
|
+
let output;
|
|
4545
|
+
if (!tool2) {
|
|
4546
|
+
output = { ok: false, reason: `unknown tool ${call.name}` };
|
|
4547
|
+
} else {
|
|
4548
|
+
try {
|
|
4549
|
+
output = await tool2.handler(JSON.parse(call.arguments || "{}"));
|
|
4550
|
+
} catch (error) {
|
|
4551
|
+
output = { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
results.push({
|
|
4555
|
+
type: "function_call_output",
|
|
4556
|
+
call_id: call.call_id,
|
|
4557
|
+
output: JSON.stringify(output)
|
|
4558
|
+
});
|
|
4559
|
+
}
|
|
4560
|
+
input = results;
|
|
4561
|
+
}
|
|
4562
|
+
return outcome("budget_exhausted");
|
|
4563
|
+
} catch (error) {
|
|
4564
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
4565
|
+
if (classifyFailure(detail) === "context_overflow") {
|
|
4566
|
+
return outcome("context_overflow");
|
|
4567
|
+
}
|
|
4568
|
+
return outcome("failed", "", detail);
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
};
|
|
4572
|
+
}
|
|
4573
|
+
|
|
4574
|
+
// src/engine/providers/router.ts
|
|
4575
|
+
function createRoutedProvider(routes, fallback) {
|
|
4576
|
+
const providers = /* @__PURE__ */ new Set([fallback, ...Object.values(routes)]);
|
|
4577
|
+
return {
|
|
4578
|
+
name: "routed",
|
|
4579
|
+
onRunReady(runId) {
|
|
4580
|
+
for (const provider of providers) {
|
|
4581
|
+
provider.onRunReady?.(runId);
|
|
4582
|
+
}
|
|
4583
|
+
},
|
|
4584
|
+
invoke(invocation, config, onProgress) {
|
|
4585
|
+
const provider = routes[invocation.role] ?? fallback;
|
|
4586
|
+
return provider.invoke(invocation, config, onProgress);
|
|
4587
|
+
}
|
|
4588
|
+
};
|
|
4589
|
+
}
|
|
4590
|
+
|
|
4591
|
+
// src/engine/cli.ts
|
|
4592
|
+
var MINUTE = 6e4;
|
|
4593
|
+
function engineModelConfig(config) {
|
|
4594
|
+
const model = (role, fallback) => process.env[`WHISPERR_WIZARD_ENGINE_${role.toUpperCase()}_MODEL`]?.trim() || fallback;
|
|
4595
|
+
return {
|
|
4596
|
+
survey: { model: model("survey", "claude-sonnet-5"), effort: "medium", maxTurns: 40, maxMs: 12 * MINUTE },
|
|
4597
|
+
design: { model: model("design", config.plannerModel), effort: "high", maxTurns: 40, maxMs: 15 * MINUTE },
|
|
4598
|
+
edit: { model: model("edit", config.model), effort: config.effort, maxTurns: 50, maxMs: 15 * MINUTE },
|
|
4599
|
+
review: { model: model("review", config.model), effort: "medium", maxTurns: 15, maxMs: 8 * MINUTE }
|
|
4600
|
+
};
|
|
4601
|
+
}
|
|
4602
|
+
function engineProvider(config, session) {
|
|
4603
|
+
const claude = createClaudeProvider(config, session);
|
|
4604
|
+
const preset = process.env.WHISPERR_WIZARD_PRESET?.trim() ?? "claude-default";
|
|
4605
|
+
if (preset === "sol-design") {
|
|
4606
|
+
const openai = createOpenAIProvider(config, session);
|
|
4607
|
+
return createRoutedProvider({ survey: openai, design: openai, review: openai }, claude);
|
|
4608
|
+
}
|
|
4609
|
+
return claude;
|
|
4610
|
+
}
|
|
4611
|
+
async function tryEngineFlow(options) {
|
|
4612
|
+
const { repoPath, fingerprint, config, session, playbook, theme: theme2 } = options;
|
|
4613
|
+
const startedAt = Date.now();
|
|
4614
|
+
const sink = createProgressSink({
|
|
4615
|
+
json: false,
|
|
4616
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
4617
|
+
write: (line) => p.log.message(theme2.muted(line))
|
|
4618
|
+
});
|
|
4619
|
+
const result = await runEngine({
|
|
4620
|
+
repoPath,
|
|
4621
|
+
repoFingerprint: fingerprint,
|
|
4622
|
+
config,
|
|
4623
|
+
session,
|
|
4624
|
+
provider: engineProvider(config, session),
|
|
4625
|
+
models: engineModelConfig(config),
|
|
4626
|
+
target: playbook.target.id,
|
|
4627
|
+
projectKind: ["node", "python", "php", "go", "express", "django", "fastapi", "laravel", "spring-boot"].includes(
|
|
4628
|
+
playbook.target.id
|
|
4629
|
+
) ? "backend" : "frontend",
|
|
4630
|
+
playbookSystemPrompt: playbook.systemPrompt,
|
|
4631
|
+
sink,
|
|
4632
|
+
callbacks: {
|
|
4633
|
+
reviewEvents: async (proposed) => reviewEventsInTerminal(proposed, theme2),
|
|
4634
|
+
confirmApply: async (patch, summaryLines) => {
|
|
4635
|
+
p.note(summaryLines.join("\n"), "Integration result");
|
|
4636
|
+
const lineCount = patch.split("\n").length;
|
|
4637
|
+
const answer = await p.confirm({
|
|
4638
|
+
message: `Apply these changes to your working tree? (${lineCount} patch lines)`,
|
|
4639
|
+
initialValue: true
|
|
4640
|
+
});
|
|
4641
|
+
return answer === true;
|
|
4642
|
+
}
|
|
4643
|
+
}
|
|
4644
|
+
});
|
|
4645
|
+
if (result.kind === "wrong_mode") {
|
|
4646
|
+
return { handled: false, exitCode: 0 };
|
|
4647
|
+
}
|
|
4648
|
+
if (result.kind === "aborted") {
|
|
4649
|
+
p.cancel(result.reason);
|
|
4650
|
+
return { handled: true, exitCode: 1 };
|
|
4651
|
+
}
|
|
4652
|
+
p.note(result.summary || "Nothing to integrate.", "Run summary");
|
|
4653
|
+
if (result.partialPatchPath) {
|
|
4654
|
+
p.log.warn(`Unapplied changes saved to ${result.partialPatchPath}`);
|
|
4655
|
+
}
|
|
4656
|
+
await postRunReport(config, session, {
|
|
4657
|
+
target: playbook.target.id,
|
|
4658
|
+
repo_fingerprint: fingerprint,
|
|
4659
|
+
identify_wired: result.identifyWired,
|
|
4660
|
+
verified: null,
|
|
4661
|
+
cost_usd: 0,
|
|
4662
|
+
duration_ms: Date.now() - startedAt,
|
|
4663
|
+
summary: result.summary.slice(0, 2e3),
|
|
4664
|
+
events: result.reportEvents,
|
|
4665
|
+
exit_class: result.kind === "completed" ? "engine_completed" : "engine_partial"
|
|
4666
|
+
});
|
|
4667
|
+
if (result.applied && result.registered.length > 0) {
|
|
4668
|
+
const spin = p.spinner();
|
|
4669
|
+
spin.start("Waiting for the first event (deploy or run your app)");
|
|
4670
|
+
const first = await pollFirstEvent(config, session, { timeoutMs: 9e4 });
|
|
4671
|
+
spin.stop(
|
|
4672
|
+
first.received ? theme2.bright(`Whisperr received ${first.eventType ?? "an event"} \u2713`) : theme2.muted("No event yet \u2014 it will show up once the instrumented build runs.")
|
|
4673
|
+
);
|
|
4674
|
+
}
|
|
4675
|
+
return { handled: true, exitCode: result.kind === "completed" ? 0 : 1 };
|
|
4676
|
+
}
|
|
4677
|
+
async function reviewEventsInTerminal(proposed, theme2) {
|
|
4678
|
+
if (proposed.length === 0) {
|
|
4679
|
+
return [];
|
|
4680
|
+
}
|
|
4681
|
+
const lines = proposed.map((event) => {
|
|
4682
|
+
const fields = Object.keys(event.payloadSchema);
|
|
4683
|
+
return [
|
|
4684
|
+
`${theme2.bright(event.code)} \u2014 ${event.name}`,
|
|
4685
|
+
` ${event.reasoning}`,
|
|
4686
|
+
` anchor: ${event.anchorFile}${event.anchorSymbol ? ` (${event.anchorSymbol})` : ""}`,
|
|
4687
|
+
fields.length > 0 ? ` payload: ${fields.join(", ")}` : " payload: none"
|
|
4688
|
+
].join("\n");
|
|
4689
|
+
});
|
|
4690
|
+
p.note(lines.join("\n\n"), `Proposed events (${proposed.length})`);
|
|
4691
|
+
const selection = await p.multiselect({
|
|
4692
|
+
message: "Register these events? Deselect any you don't want.",
|
|
4693
|
+
options: proposed.map((event) => ({ value: event.code, label: event.code, hint: event.name })),
|
|
4694
|
+
initialValues: proposed.map((event) => event.code),
|
|
4695
|
+
required: false
|
|
4696
|
+
});
|
|
4697
|
+
if (p.isCancel(selection)) {
|
|
4698
|
+
return null;
|
|
4699
|
+
}
|
|
4700
|
+
const chosen = new Set(selection);
|
|
4701
|
+
return proposed.filter((event) => chosen.has(event.code));
|
|
4702
|
+
}
|
|
4703
|
+
|
|
4704
|
+
// src/core/opportunities.ts
|
|
4705
|
+
function normalizeCode(value) {
|
|
4706
|
+
return value.trim().toLowerCase().replace(/[ \-./]/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
4707
|
+
}
|
|
4708
|
+
function sanitizeOpportunities(raw, manifest) {
|
|
4709
|
+
const out = { events: [], interventions: [] };
|
|
4710
|
+
if (typeof raw !== "object" || raw === null) return out;
|
|
4711
|
+
const doc = raw;
|
|
4712
|
+
const knownEvents = new Set(manifest.events.map((e) => normalizeCode(e.eventType)));
|
|
4713
|
+
const knownInterventions = /* @__PURE__ */ new Set([
|
|
4714
|
+
...manifest.events.flatMap((e) => e.interventions ?? []).map((i) => normalizeCode(i.code)),
|
|
4715
|
+
...(manifest.interventions ?? []).map((i) => normalizeCode(i.code))
|
|
4716
|
+
]);
|
|
4717
|
+
const seenEvents = /* @__PURE__ */ new Set();
|
|
4718
|
+
for (const entry of asArray(doc.events)) {
|
|
4719
|
+
const ev = coerceEvent(entry);
|
|
4720
|
+
if (!ev) continue;
|
|
4721
|
+
if (knownEvents.has(ev.code) || seenEvents.has(ev.code)) continue;
|
|
4722
|
+
seenEvents.add(ev.code);
|
|
4723
|
+
out.events.push(ev);
|
|
4724
|
+
}
|
|
4725
|
+
const seenInterventions = /* @__PURE__ */ new Set();
|
|
4726
|
+
for (const entry of asArray(doc.interventions)) {
|
|
4727
|
+
const iv = coerceIntervention(entry);
|
|
4728
|
+
if (!iv) continue;
|
|
4729
|
+
if (knownInterventions.has(iv.code) || seenInterventions.has(iv.code)) continue;
|
|
4730
|
+
seenInterventions.add(iv.code);
|
|
4731
|
+
out.interventions.push(iv);
|
|
4732
|
+
}
|
|
4733
|
+
return out;
|
|
4734
|
+
}
|
|
4735
|
+
var SUBMIT_TIMEOUT_MS = 3e4;
|
|
4736
|
+
async function submitAdditions(config, session, target9, repoFingerprint2, opportunities) {
|
|
4737
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/universe/additions`, {
|
|
4738
|
+
method: "POST",
|
|
4739
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
4740
|
+
headers: {
|
|
4741
|
+
"Content-Type": "application/json",
|
|
4742
|
+
Authorization: `Bearer ${session.token}`
|
|
4743
|
+
},
|
|
4744
|
+
body: JSON.stringify({
|
|
4745
|
+
target: target9,
|
|
4746
|
+
repoFingerprint: repoFingerprint2,
|
|
4747
|
+
events: opportunities.events,
|
|
4748
|
+
interventions: opportunities.interventions
|
|
4749
|
+
})
|
|
4750
|
+
});
|
|
4751
|
+
if (!res.ok) {
|
|
4752
|
+
const body = await res.text().catch(() => "");
|
|
4753
|
+
throw new Error(
|
|
4754
|
+
`Submitting universe additions failed (${res.status})${body ? `: ${body.slice(0, 300)}` : ""}`
|
|
4755
|
+
);
|
|
4756
|
+
}
|
|
4757
|
+
return await res.json();
|
|
4758
|
+
}
|
|
4759
|
+
async function submitSuggestions(config, session, target9, repoFingerprint2, opportunities) {
|
|
4760
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/universe/suggestions`, {
|
|
4761
|
+
method: "POST",
|
|
4762
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
4763
|
+
headers: {
|
|
4764
|
+
"Content-Type": "application/json",
|
|
4765
|
+
Authorization: `Bearer ${session.token}`
|
|
4766
|
+
},
|
|
4767
|
+
body: JSON.stringify({
|
|
4768
|
+
target: target9,
|
|
4769
|
+
repoFingerprint: repoFingerprint2,
|
|
4770
|
+
events: opportunities.events,
|
|
4771
|
+
interventions: opportunities.interventions
|
|
4772
|
+
})
|
|
4773
|
+
});
|
|
4774
|
+
if (res.status === 404) return null;
|
|
4775
|
+
if (!res.ok) {
|
|
4776
|
+
const body = await res.text().catch(() => "");
|
|
4777
|
+
throw new Error(
|
|
4778
|
+
`Submitting universe suggestions failed (${res.status})${body ? `: ${body.slice(0, 300)}` : ""}`
|
|
4779
|
+
);
|
|
4780
|
+
}
|
|
4781
|
+
return await res.json();
|
|
4782
|
+
}
|
|
4783
|
+
async function fetchSuggestions(config, session, statuses) {
|
|
4784
|
+
if (config.offline) return [];
|
|
4785
|
+
try {
|
|
4786
|
+
const statusQuery = statuses.map(encodeURIComponent).join(",");
|
|
4787
|
+
const url = `${config.apiBaseUrl}/wizard/universe/suggestions${statusQuery ? `?status=${statusQuery}` : ""}`;
|
|
4788
|
+
const res = await fetch(url, {
|
|
4789
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
4790
|
+
headers: { Authorization: `Bearer ${session.token}` }
|
|
4791
|
+
});
|
|
4792
|
+
if (!res.ok) return [];
|
|
4793
|
+
const body = await res.json();
|
|
4794
|
+
return Array.isArray(body) && body.every(isSuggestionRecord) ? body : [];
|
|
4795
|
+
} catch {
|
|
4796
|
+
return [];
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
function isSuggestionRecord(value) {
|
|
4800
|
+
if (typeof value !== "object" || value === null) return false;
|
|
4801
|
+
const record = value;
|
|
4802
|
+
return typeof record.id === "string" && (record.kind === "event" || record.kind === "intervention") && typeof record.code === "string" && (record.status === "proposed" || record.status === "approved" || record.status === "rejected" || record.status === "integrated") && typeof record.payload === "object" && record.payload !== null;
|
|
4803
|
+
}
|
|
4804
|
+
async function markSuggestionIntegrated(config, session, id, target9, repoFingerprint2) {
|
|
4805
|
+
if (config.offline) return false;
|
|
4806
|
+
try {
|
|
4807
|
+
const res = await fetch(
|
|
4808
|
+
`${config.apiBaseUrl}/wizard/universe/suggestions/${encodeURIComponent(id)}/integrated`,
|
|
4809
|
+
{
|
|
4810
|
+
method: "POST",
|
|
4811
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
4812
|
+
headers: {
|
|
4813
|
+
"Content-Type": "application/json",
|
|
4814
|
+
Authorization: `Bearer ${session.token}`
|
|
4815
|
+
},
|
|
4816
|
+
body: JSON.stringify({ target: target9, repoFingerprint: repoFingerprint2 })
|
|
4817
|
+
}
|
|
4818
|
+
);
|
|
4819
|
+
return res.status === 200;
|
|
4820
|
+
} catch {
|
|
4821
|
+
return false;
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
function asArray(value) {
|
|
4825
|
+
return Array.isArray(value) ? value : [];
|
|
4826
|
+
}
|
|
4827
|
+
function asString(value) {
|
|
4828
|
+
if (typeof value !== "string") return void 0;
|
|
4829
|
+
const cleaned = value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
4830
|
+
return cleaned ? cleaned : void 0;
|
|
4831
|
+
}
|
|
4832
|
+
function asConfidence(value) {
|
|
4833
|
+
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
4834
|
+
return Math.min(1, Math.max(0, value));
|
|
4835
|
+
}
|
|
4836
|
+
function asWeight(value) {
|
|
4837
|
+
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
4838
|
+
if (value < 0 || value > 1) return void 0;
|
|
4839
|
+
return value;
|
|
4840
|
+
}
|
|
4841
|
+
function coerceEvent(entry) {
|
|
4842
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
4843
|
+
const e = entry;
|
|
4844
|
+
const code = normalizeCode(asString(e.code) ?? "");
|
|
4845
|
+
if (!code) return null;
|
|
4846
|
+
const properties = [];
|
|
4847
|
+
for (const prop of asArray(e.properties)) {
|
|
4848
|
+
if (typeof prop === "string") {
|
|
4849
|
+
if (prop.trim()) properties.push({ name: prop.trim() });
|
|
4850
|
+
continue;
|
|
4851
|
+
}
|
|
4852
|
+
if (typeof prop !== "object" || prop === null) continue;
|
|
4853
|
+
const pr = prop;
|
|
4854
|
+
const name = asString(pr.name);
|
|
4855
|
+
if (!name) continue;
|
|
4856
|
+
const desc = asString(pr.description);
|
|
4857
|
+
properties.push({
|
|
4858
|
+
name,
|
|
4859
|
+
...desc ? { description: desc } : {},
|
|
4860
|
+
...pr.required === true ? { required: true } : {}
|
|
4861
|
+
});
|
|
4862
|
+
}
|
|
4863
|
+
const links = [];
|
|
4864
|
+
for (const link of asArray(e.links)) {
|
|
4865
|
+
if (typeof link !== "object" || link === null) continue;
|
|
4866
|
+
const l = link;
|
|
4867
|
+
const interventionCode = normalizeCode(asString(l.interventionCode) ?? "");
|
|
4868
|
+
if (!interventionCode) continue;
|
|
4869
|
+
const weight = asWeight(l.weight);
|
|
4870
|
+
links.push({ interventionCode, ...weight !== void 0 ? { weight } : {} });
|
|
4871
|
+
}
|
|
4872
|
+
const side = normalizeSide(asString(e.side));
|
|
4873
|
+
return {
|
|
4874
|
+
code,
|
|
4875
|
+
label: asString(e.label),
|
|
4876
|
+
description: asString(e.description),
|
|
4877
|
+
...side ? { side } : {},
|
|
4878
|
+
rationale: asString(e.rationale),
|
|
4879
|
+
expectedEffect: asString(e.expectedEffect),
|
|
4880
|
+
confidence: asConfidence(e.confidence),
|
|
4881
|
+
...properties.length ? { properties } : {},
|
|
4882
|
+
...links.length ? { links } : {}
|
|
4883
|
+
};
|
|
4884
|
+
}
|
|
4885
|
+
function coerceIntervention(entry) {
|
|
4886
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
4887
|
+
const i = entry;
|
|
4888
|
+
const code = normalizeCode(asString(i.code) ?? "");
|
|
4889
|
+
if (!code) return null;
|
|
4890
|
+
const links = [];
|
|
4891
|
+
for (const link of asArray(i.links)) {
|
|
4892
|
+
if (typeof link !== "object" || link === null) continue;
|
|
4893
|
+
const l = link;
|
|
4894
|
+
const eventCode = normalizeCode(asString(l.eventCode) ?? "");
|
|
4895
|
+
if (!eventCode) continue;
|
|
4896
|
+
const weight = asWeight(l.weight);
|
|
4897
|
+
links.push({ eventCode, ...weight !== void 0 ? { weight } : {} });
|
|
4898
|
+
}
|
|
4899
|
+
return {
|
|
4900
|
+
code,
|
|
4901
|
+
label: asString(i.label),
|
|
4902
|
+
description: asString(i.description),
|
|
4903
|
+
rationale: asString(i.rationale),
|
|
4904
|
+
expectedEffect: asString(i.expectedEffect),
|
|
4905
|
+
confidence: asConfidence(i.confidence),
|
|
4906
|
+
...links.length ? { links } : {}
|
|
4907
|
+
};
|
|
2934
4908
|
}
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
const res = await fetch(`${config.apiBaseUrl}/wizard/first-event`, {
|
|
2946
|
-
headers: { authorization: `Bearer ${session.token}` }
|
|
2947
|
-
});
|
|
2948
|
-
if (res.ok) {
|
|
2949
|
-
const body = await res.json();
|
|
2950
|
-
if (body.received) {
|
|
2951
|
-
return { received: true, eventType: body.event_type };
|
|
2952
|
-
}
|
|
2953
|
-
}
|
|
2954
|
-
} catch {
|
|
2955
|
-
}
|
|
2956
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
4909
|
+
function normalizeSide(value) {
|
|
4910
|
+
switch (value?.toLowerCase()) {
|
|
4911
|
+
case "frontend":
|
|
4912
|
+
return "frontend";
|
|
4913
|
+
case "backend":
|
|
4914
|
+
return "backend";
|
|
4915
|
+
case "either":
|
|
4916
|
+
return "either";
|
|
4917
|
+
default:
|
|
4918
|
+
return void 0;
|
|
2957
4919
|
}
|
|
2958
|
-
return { received: false };
|
|
2959
4920
|
}
|
|
2960
4921
|
|
|
2961
4922
|
// src/core/postflight.ts
|
|
2962
|
-
import { spawn as
|
|
4923
|
+
import { spawn as spawn4 } from "child_process";
|
|
2963
4924
|
function verdictToVerified(verdict) {
|
|
2964
4925
|
if (verdict.toolMissing || verdict.timedOut) return null;
|
|
2965
4926
|
return verdict.ok;
|
|
@@ -2971,8 +4932,8 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2971
4932
|
return Promise.resolve({ ran: false, ok: true, output: "" });
|
|
2972
4933
|
}
|
|
2973
4934
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2974
|
-
return new Promise((
|
|
2975
|
-
const child =
|
|
4935
|
+
return new Promise((resolve4) => {
|
|
4936
|
+
const child = spawn4(command, { cwd: repoPath, shell: true, env: process.env });
|
|
2976
4937
|
let out = "";
|
|
2977
4938
|
const append = (d) => {
|
|
2978
4939
|
out += d.toString();
|
|
@@ -2982,11 +4943,11 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2982
4943
|
child.stderr?.on("data", append);
|
|
2983
4944
|
const timer = setTimeout(() => {
|
|
2984
4945
|
child.kill("SIGKILL");
|
|
2985
|
-
|
|
4946
|
+
resolve4({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
|
|
2986
4947
|
}, timeoutMs);
|
|
2987
4948
|
child.on("close", (code) => {
|
|
2988
4949
|
clearTimeout(timer);
|
|
2989
|
-
|
|
4950
|
+
resolve4({
|
|
2990
4951
|
ran: true,
|
|
2991
4952
|
ok: code === 0,
|
|
2992
4953
|
command,
|
|
@@ -2998,7 +4959,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2998
4959
|
});
|
|
2999
4960
|
child.on("error", () => {
|
|
3000
4961
|
clearTimeout(timer);
|
|
3001
|
-
|
|
4962
|
+
resolve4({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
|
|
3002
4963
|
});
|
|
3003
4964
|
});
|
|
3004
4965
|
}
|
|
@@ -3007,51 +4968,24 @@ function tail2(s) {
|
|
|
3007
4968
|
return t.length > MAX_OUTPUT ? `\u2026${t.slice(-MAX_OUTPUT)}` : t;
|
|
3008
4969
|
}
|
|
3009
4970
|
|
|
3010
|
-
// src/core/
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
body: JSON.stringify(report)
|
|
3027
|
-
});
|
|
3028
|
-
if (res.ok) return { ok: true };
|
|
3029
|
-
return {
|
|
3030
|
-
ok: false,
|
|
3031
|
-
status: res.status,
|
|
3032
|
-
detail: await responseDetail(res)
|
|
3033
|
-
};
|
|
3034
|
-
} catch (err) {
|
|
3035
|
-
return { ok: false, detail: detailSlice(errorMessage(err)) };
|
|
3036
|
-
}
|
|
3037
|
-
}
|
|
3038
|
-
async function responseDetail(res) {
|
|
3039
|
-
try {
|
|
3040
|
-
return detailSlice(await res.text());
|
|
3041
|
-
} catch (err) {
|
|
3042
|
-
return detailSlice(errorMessage(err));
|
|
4971
|
+
// src/core/version.ts
|
|
4972
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4973
|
+
import { dirname as dirname3, join as join9, parse } from "path";
|
|
4974
|
+
import { fileURLToPath } from "url";
|
|
4975
|
+
var PACKAGE_NAME = "@whisperr/wizard";
|
|
4976
|
+
function packageVersion() {
|
|
4977
|
+
let dir = dirname3(fileURLToPath(import.meta.url));
|
|
4978
|
+
const { root } = parse(dir);
|
|
4979
|
+
while (true) {
|
|
4980
|
+
try {
|
|
4981
|
+
const pkg = JSON.parse(readFileSync4(join9(dir, "package.json"), "utf8"));
|
|
4982
|
+
if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
|
|
4983
|
+
} catch {
|
|
4984
|
+
}
|
|
4985
|
+
if (dir === root) return "0.0.0";
|
|
4986
|
+
dir = dirname3(dir);
|
|
3043
4987
|
}
|
|
3044
4988
|
}
|
|
3045
|
-
function errorMessage(err) {
|
|
3046
|
-
return err instanceof Error ? err.message : String(err);
|
|
3047
|
-
}
|
|
3048
|
-
function detailSlice(detail) {
|
|
3049
|
-
const sliced = detail.slice(0, 200).trim();
|
|
3050
|
-
return sliced || void 0;
|
|
3051
|
-
}
|
|
3052
|
-
function scrubTerminalControls(value) {
|
|
3053
|
-
return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
3054
|
-
}
|
|
3055
4989
|
|
|
3056
4990
|
// src/core/gapreport.ts
|
|
3057
4991
|
function buildGapReport(input) {
|
|
@@ -3182,12 +5116,12 @@ function banner() {
|
|
|
3182
5116
|
|
|
3183
5117
|
// src/cli.ts
|
|
3184
5118
|
async function run2(options) {
|
|
3185
|
-
const repoPath =
|
|
5119
|
+
const repoPath = resolve3(options.path ?? process.cwd());
|
|
3186
5120
|
const config = resolveConfig(options);
|
|
3187
5121
|
console.log(banner());
|
|
3188
|
-
|
|
5122
|
+
p2.intro(theme.signal("Let's wire Whisperr into your app."));
|
|
3189
5123
|
if (config.offline) {
|
|
3190
|
-
|
|
5124
|
+
p2.log.warn(
|
|
3191
5125
|
theme.warn("offline mode") + theme.muted(" \u2014 using a demo manifest, no account needed.")
|
|
3192
5126
|
);
|
|
3193
5127
|
}
|
|
@@ -3197,27 +5131,27 @@ async function run2(options) {
|
|
|
3197
5131
|
);
|
|
3198
5132
|
const chosen = await chooseTarget(detections);
|
|
3199
5133
|
if (!chosen) {
|
|
3200
|
-
|
|
5134
|
+
p2.cancel("No supported stack selected.");
|
|
3201
5135
|
return 1;
|
|
3202
5136
|
}
|
|
3203
5137
|
if (chosen.playbook.target.availability === "planned") {
|
|
3204
|
-
|
|
5138
|
+
p2.note(
|
|
3205
5139
|
`We can detect ${theme.bright(chosen.playbook.target.displayName)} but the Whisperr SDK for it isn't shipped yet.
|
|
3206
5140
|
Flutter is live today; ${theme.bright(
|
|
3207
5141
|
chosen.playbook.target.displayName
|
|
3208
5142
|
)} is next on the roadmap.`,
|
|
3209
5143
|
theme.warn("SDK coming soon")
|
|
3210
5144
|
);
|
|
3211
|
-
const cont = await
|
|
5145
|
+
const cont = await p2.confirm({
|
|
3212
5146
|
message: "Continue anyway and let the agent scaffold against the planned SDK?",
|
|
3213
5147
|
initialValue: false
|
|
3214
5148
|
});
|
|
3215
|
-
if (
|
|
3216
|
-
|
|
5149
|
+
if (p2.isCancel(cont) || !cont) {
|
|
5150
|
+
p2.outro(theme.muted("No problem \u2014 come back when the SDK lands."));
|
|
3217
5151
|
return 0;
|
|
3218
5152
|
}
|
|
3219
5153
|
} else {
|
|
3220
|
-
|
|
5154
|
+
p2.log.success(
|
|
3221
5155
|
`Detected ${theme.bright(chosen.playbook.target.displayName)} ` + theme.muted(`(${chosen.detection?.evidence.join(", ") ?? "selected"})`)
|
|
3222
5156
|
);
|
|
3223
5157
|
}
|
|
@@ -3225,43 +5159,56 @@ Flutter is live today; ${theme.bright(
|
|
|
3225
5159
|
try {
|
|
3226
5160
|
session = config.offline ? await authenticate(config) : await withBrowserAuth(config);
|
|
3227
5161
|
} catch (err) {
|
|
3228
|
-
|
|
5162
|
+
p2.cancel(theme.alert(err.message));
|
|
3229
5163
|
return 1;
|
|
3230
5164
|
}
|
|
3231
5165
|
startSessionKeepalive(config, session);
|
|
3232
5166
|
const fingerprint = await repoFingerprint(repoPath);
|
|
5167
|
+
if (!config.offline) {
|
|
5168
|
+
const engineOutcome = await tryEngineFlow({
|
|
5169
|
+
repoPath,
|
|
5170
|
+
fingerprint,
|
|
5171
|
+
config,
|
|
5172
|
+
session,
|
|
5173
|
+
playbook: chosen.playbook,
|
|
5174
|
+
theme
|
|
5175
|
+
});
|
|
5176
|
+
if (engineOutcome.handled) {
|
|
5177
|
+
return engineOutcome.exitCode;
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
3233
5180
|
const manifest = await withSpinner(
|
|
3234
5181
|
"Loading your onboarding context",
|
|
3235
5182
|
() => fetchManifest(config, session, chosen.playbook.target.id, fingerprint)
|
|
3236
5183
|
);
|
|
3237
5184
|
const approvedSuggestionsPromise = config.offline ? null : fetchSuggestions(config, session, ["approved"]);
|
|
3238
|
-
|
|
5185
|
+
p2.note(
|
|
3239
5186
|
summarizeManifest(manifest),
|
|
3240
5187
|
theme.signal(`Plan for ${manifest.appName ?? manifest.appId}`)
|
|
3241
5188
|
);
|
|
3242
5189
|
const checkpoint = await takeCheckpoint(repoPath);
|
|
3243
5190
|
if (!checkpoint.isRepo) {
|
|
3244
5191
|
if (!options.force) {
|
|
3245
|
-
|
|
5192
|
+
p2.cancel(
|
|
3246
5193
|
theme.alert("Not a git repository.") + " The wizard edits your code and relies on git to undo safely.\n" + theme.muted("Run `git init` and commit first, or re-run with --force to proceed without a safety net.")
|
|
3247
5194
|
);
|
|
3248
5195
|
return 1;
|
|
3249
5196
|
}
|
|
3250
|
-
|
|
5197
|
+
p2.log.warn(
|
|
3251
5198
|
theme.warn("not a git repo") + theme.muted(" \u2014 proceeding with --force; there is no automatic undo.")
|
|
3252
5199
|
);
|
|
3253
5200
|
} else if (!await isWorkingTreeClean(repoPath)) {
|
|
3254
5201
|
if (!options.force) {
|
|
3255
|
-
|
|
5202
|
+
p2.cancel(
|
|
3256
5203
|
theme.alert("Your working tree has uncommitted changes.") + " Commit or stash them first so the wizard's edits stay isolated and reversible.\n" + theme.muted("Or re-run with --force to proceed anyway.")
|
|
3257
5204
|
);
|
|
3258
5205
|
return 1;
|
|
3259
5206
|
}
|
|
3260
|
-
|
|
5207
|
+
p2.log.warn(
|
|
3261
5208
|
theme.warn("uncommitted changes present") + theme.muted(" \u2014 proceeding with --force; a revert would also drop your own changes.")
|
|
3262
5209
|
);
|
|
3263
5210
|
}
|
|
3264
|
-
const spin =
|
|
5211
|
+
const spin = p2.spinner();
|
|
3265
5212
|
const useIntegrationSpinner = Boolean(process.stdout.isTTY);
|
|
3266
5213
|
let phaseLabel = "Integrating";
|
|
3267
5214
|
let lastLine = "";
|
|
@@ -3277,42 +5224,59 @@ Flutter is live today; ${theme.bright(
|
|
|
3277
5224
|
if (useIntegrationSpinner) {
|
|
3278
5225
|
spin.start(theme.bright(phaseLabel));
|
|
3279
5226
|
} else {
|
|
3280
|
-
|
|
5227
|
+
p2.log.step(theme.bright(phaseLabel));
|
|
3281
5228
|
}
|
|
3282
5229
|
const tick = useIntegrationSpinner ? setInterval(updateIntegrationMessage, 1e3) : void 0;
|
|
5230
|
+
const additions = { proposed: 0, applied: [] };
|
|
5231
|
+
const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
3283
5232
|
const outcome = await runIntegrationAgent({
|
|
3284
5233
|
repoPath,
|
|
3285
5234
|
config,
|
|
3286
5235
|
session,
|
|
3287
5236
|
playbook: chosen.playbook,
|
|
3288
5237
|
manifest,
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
}
|
|
3310
|
-
|
|
3311
|
-
spin.start(theme.bright("Continuing integration"));
|
|
3312
|
-
}
|
|
3313
|
-
return filterEventPlan(plan, selectedEvents);
|
|
5238
|
+
// The plan is always shown — it's the last look at what's about to change
|
|
5239
|
+
// in their code. It only becomes a question under --review-plan; the
|
|
5240
|
+
// default run reads it out and keeps going.
|
|
5241
|
+
async onPlanReady(plan) {
|
|
5242
|
+
if (useIntegrationSpinner) {
|
|
5243
|
+
spin.stop(theme.success("Placement plan ready"));
|
|
5244
|
+
}
|
|
5245
|
+
p2.note(renderPlacementPlan(plan), "Placement plan");
|
|
5246
|
+
const placeEntries = plan.filter((entry) => entry.decision === "place");
|
|
5247
|
+
let selectedEvents = placeEntries.map((entry) => entry.event);
|
|
5248
|
+
if (config.reviewPlan && interactive && placeEntries.length) {
|
|
5249
|
+
const selection = await p2.multiselect({
|
|
5250
|
+
message: "Wire these events?",
|
|
5251
|
+
options: placeEntries.map((entry) => ({
|
|
5252
|
+
value: entry.event,
|
|
5253
|
+
label: entry.event,
|
|
5254
|
+
hint: `${entry.file ?? "unknown file"}@${entry.anchor ?? "unknown anchor"}`
|
|
5255
|
+
})),
|
|
5256
|
+
initialValues: selectedEvents,
|
|
5257
|
+
required: false
|
|
5258
|
+
});
|
|
5259
|
+
selectedEvents = p2.isCancel(selection) ? [] : selection;
|
|
3314
5260
|
}
|
|
3315
|
-
|
|
5261
|
+
if (useIntegrationSpinner) {
|
|
5262
|
+
spin.start(theme.bright("Continuing integration"));
|
|
5263
|
+
}
|
|
5264
|
+
return filterEventPlan(plan, selectedEvents);
|
|
5265
|
+
},
|
|
5266
|
+
async onOpportunitiesReady(raw) {
|
|
5267
|
+
if (useIntegrationSpinner) spin.stop(theme.success("Planning done"));
|
|
5268
|
+
const applied = await applyOpportunities({
|
|
5269
|
+
config,
|
|
5270
|
+
session,
|
|
5271
|
+
playbook: chosen.playbook,
|
|
5272
|
+
manifest,
|
|
5273
|
+
fingerprint,
|
|
5274
|
+
raw,
|
|
5275
|
+
additions
|
|
5276
|
+
});
|
|
5277
|
+
if (useIntegrationSpinner) spin.start(theme.bright("Continuing integration"));
|
|
5278
|
+
return applied;
|
|
5279
|
+
},
|
|
3316
5280
|
progress: {
|
|
3317
5281
|
onPhase(label) {
|
|
3318
5282
|
phaseLabel = label;
|
|
@@ -3321,7 +5285,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3321
5285
|
if (useIntegrationSpinner) {
|
|
3322
5286
|
updateIntegrationMessage();
|
|
3323
5287
|
} else {
|
|
3324
|
-
|
|
5288
|
+
p2.log.step(theme.bright(label));
|
|
3325
5289
|
}
|
|
3326
5290
|
},
|
|
3327
5291
|
onActivity(line) {
|
|
@@ -3332,31 +5296,95 @@ Flutter is live today; ${theme.bright(
|
|
|
3332
5296
|
}
|
|
3333
5297
|
}
|
|
3334
5298
|
}).catch((err) => {
|
|
3335
|
-
|
|
5299
|
+
p2.log.error(err.message);
|
|
3336
5300
|
return null;
|
|
3337
5301
|
});
|
|
3338
5302
|
if (tick) clearInterval(tick);
|
|
5303
|
+
let verified = null;
|
|
5304
|
+
let reported = false;
|
|
5305
|
+
const collectReportEvents = async () => {
|
|
5306
|
+
if (!outcome) return [];
|
|
5307
|
+
const eventOutcomeByType = new Map(
|
|
5308
|
+
outcome.eventOutcomes.map((event) => [event.event, event])
|
|
5309
|
+
);
|
|
5310
|
+
const eventTypes = [
|
|
5311
|
+
...manifest.events.map((e) => e.eventType),
|
|
5312
|
+
...additions.applied.map((e) => e.code)
|
|
5313
|
+
];
|
|
5314
|
+
let wired = /* @__PURE__ */ new Map();
|
|
5315
|
+
try {
|
|
5316
|
+
wired = await scanWiredEvents(
|
|
5317
|
+
repoPath,
|
|
5318
|
+
await changedFiles(repoPath, checkpoint),
|
|
5319
|
+
eventTypes
|
|
5320
|
+
);
|
|
5321
|
+
} catch {
|
|
5322
|
+
}
|
|
5323
|
+
return eventTypes.map((eventType) => ({
|
|
5324
|
+
event_type: eventType,
|
|
5325
|
+
status: wired.has(eventType) ? "wired" : "skipped",
|
|
5326
|
+
file: wired.get(eventType),
|
|
5327
|
+
// The agent's own words on why it skipped — the field that answers "why
|
|
5328
|
+
// didn't it wire X" without anyone opening the repo.
|
|
5329
|
+
reason: wired.has(eventType) ? void 0 : eventOutcomeByType.get(eventType)?.reason
|
|
5330
|
+
}));
|
|
5331
|
+
};
|
|
5332
|
+
const reportRun = async (exitClass) => {
|
|
5333
|
+
if (reported) return [];
|
|
5334
|
+
reported = true;
|
|
5335
|
+
const reportEvents2 = await collectReportEvents();
|
|
5336
|
+
const result = await postRunReport(config, session, {
|
|
5337
|
+
target: chosen.playbook.target.id,
|
|
5338
|
+
repo_fingerprint: fingerprint,
|
|
5339
|
+
identify_wired: outcome?.coreOk ?? false,
|
|
5340
|
+
verified,
|
|
5341
|
+
cost_usd: outcome?.costUsd ?? 0,
|
|
5342
|
+
duration_ms: outcome?.durationMs ?? Date.now() - startedAt,
|
|
5343
|
+
summary: scrubSummary(outcome?.summary ?? "").slice(0, 4e3),
|
|
5344
|
+
events: reportEvents2,
|
|
5345
|
+
wizard_version: packageVersion(),
|
|
5346
|
+
planner_model: config.plannerModel,
|
|
5347
|
+
editor_model: config.model,
|
|
5348
|
+
effort: config.effort,
|
|
5349
|
+
exit_class: exitClass,
|
|
5350
|
+
phase_timings: outcome?.phaseTimings ?? [],
|
|
5351
|
+
opportunities_proposed: additions.proposed,
|
|
5352
|
+
opportunities_applied: additions.applied.length
|
|
5353
|
+
});
|
|
5354
|
+
if (!result.ok) {
|
|
5355
|
+
p2.log.warn(
|
|
5356
|
+
theme.warn("Couldn't record this run's coverage on the server") + theme.muted(
|
|
5357
|
+
` (${formatReportFailure(result)}) \u2014 future runs may re-propose events already wired here.`
|
|
5358
|
+
)
|
|
5359
|
+
);
|
|
5360
|
+
}
|
|
5361
|
+
return reportEvents2;
|
|
5362
|
+
};
|
|
3339
5363
|
if (!outcome) {
|
|
3340
5364
|
if (useIntegrationSpinner) {
|
|
3341
5365
|
spin.stop(theme.alert("Integration stopped"));
|
|
3342
5366
|
} else {
|
|
3343
|
-
|
|
5367
|
+
p2.log.error(theme.alert("Integration stopped"));
|
|
3344
5368
|
}
|
|
3345
|
-
await maybeRevert(
|
|
5369
|
+
const reverted = await maybeRevert(
|
|
5370
|
+
repoPath,
|
|
5371
|
+
checkpoint,
|
|
5372
|
+
"The run stopped before finishing."
|
|
5373
|
+
);
|
|
5374
|
+
await reportRun(reverted ? "reverted" : "agent_error");
|
|
3346
5375
|
return 1;
|
|
3347
5376
|
}
|
|
3348
5377
|
const stopLabel = !outcome.coreOk ? theme.warn("Stopped early \u2014 partial setup is in your working tree") : outcome.eventsComplete ? theme.success("Integration complete") : theme.success("Core integration done") + theme.muted(" \u2014 some events left as follow-ups");
|
|
3349
5378
|
if (useIntegrationSpinner) {
|
|
3350
5379
|
spin.stop(stopLabel);
|
|
3351
5380
|
} else if (!outcome.coreOk) {
|
|
3352
|
-
|
|
5381
|
+
p2.log.warn(stopLabel);
|
|
3353
5382
|
} else {
|
|
3354
|
-
|
|
5383
|
+
p2.log.success(stopLabel);
|
|
3355
5384
|
}
|
|
3356
|
-
const opportunities = await collectOpportunities(repoPath, manifest, checkpoint);
|
|
3357
5385
|
let files = await changedFiles(repoPath, checkpoint);
|
|
3358
5386
|
if (files.length) {
|
|
3359
|
-
|
|
5387
|
+
p2.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
3360
5388
|
}
|
|
3361
5389
|
logAgentSummary(outcome.summary);
|
|
3362
5390
|
if (!outcome.coreOk) {
|
|
@@ -3366,14 +5394,15 @@ Flutter is live today; ${theme.bright(
|
|
|
3366
5394
|
"The core setup didn't complete, so these changes may be incomplete."
|
|
3367
5395
|
);
|
|
3368
5396
|
if (reverted) {
|
|
3369
|
-
|
|
5397
|
+
await reportRun("reverted");
|
|
5398
|
+
p2.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
3370
5399
|
return 1;
|
|
3371
5400
|
}
|
|
5401
|
+
await reportRun("core_failed");
|
|
3372
5402
|
}
|
|
3373
|
-
let verified = null;
|
|
3374
5403
|
if (chosen.playbook.verifyCommand && files.length) {
|
|
3375
5404
|
const cmd = chosen.playbook.verifyCommand;
|
|
3376
|
-
const vspin =
|
|
5405
|
+
const vspin = p2.spinner();
|
|
3377
5406
|
vspin.start(`Verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
3378
5407
|
let verdict = await runVerifyCommand(repoPath, cmd);
|
|
3379
5408
|
verified = verdictToVerified(verdict);
|
|
@@ -3393,7 +5422,7 @@ Flutter is live today; ${theme.bright(
|
|
|
3393
5422
|
vspin.stop(
|
|
3394
5423
|
theme.warn(`Verification failed \u2014 ${cmd}`) + theme.muted(" \u2014 attempting one repair pass")
|
|
3395
5424
|
);
|
|
3396
|
-
const repairSpin =
|
|
5425
|
+
const repairSpin = p2.spinner();
|
|
3397
5426
|
repairSpin.start(`Repairing verifier failures \u2014 ${theme.muted(cmd)}`);
|
|
3398
5427
|
try {
|
|
3399
5428
|
const repair = await runRepairPass({
|
|
@@ -3418,7 +5447,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3418
5447
|
);
|
|
3419
5448
|
}
|
|
3420
5449
|
files = await changedFiles(repoPath, checkpoint);
|
|
3421
|
-
const reverifySpin =
|
|
5450
|
+
const reverifySpin = p2.spinner();
|
|
3422
5451
|
reverifySpin.start(`Re-verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
3423
5452
|
verdict = await runVerifyCommand(repoPath, cmd);
|
|
3424
5453
|
verified = verdictToVerified(verdict);
|
|
@@ -3442,7 +5471,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3442
5471
|
}
|
|
3443
5472
|
if (!verdict.ok && !verdict.toolMissing && !verdict.timedOut) {
|
|
3444
5473
|
if (verdict.output) {
|
|
3445
|
-
|
|
5474
|
+
p2.note(verdict.output, theme.warn("Verifier output"));
|
|
3446
5475
|
}
|
|
3447
5476
|
const reverted = await maybeRevert(
|
|
3448
5477
|
repoPath,
|
|
@@ -3450,94 +5479,21 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3450
5479
|
"The integration didn't pass its build/lint check, so the edits may be broken."
|
|
3451
5480
|
);
|
|
3452
5481
|
if (reverted) {
|
|
3453
|
-
|
|
5482
|
+
await reportRun("reverted");
|
|
5483
|
+
p2.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
3454
5484
|
return 1;
|
|
3455
5485
|
}
|
|
5486
|
+
await reportRun("verify_failed");
|
|
3456
5487
|
}
|
|
3457
5488
|
}
|
|
3458
5489
|
}
|
|
3459
|
-
const
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
fingerprint,
|
|
3467
|
-
checkpoint,
|
|
3468
|
-
remainingBudgetUsd: config.budgetUsd - outcome.costUsd,
|
|
3469
|
-
repoMap: outcome.repoMap
|
|
3470
|
-
});
|
|
3471
|
-
if (instrumentationSnapshot && chosen.playbook.verifyCommand) {
|
|
3472
|
-
const cmd = chosen.playbook.verifyCommand;
|
|
3473
|
-
const preInstrumentationVerified = verified;
|
|
3474
|
-
const vspin = p.spinner();
|
|
3475
|
-
vspin.start(`Re-verifying after instrumenting the new events \u2014 ${theme.muted(cmd)}`);
|
|
3476
|
-
const verdict = await runVerifyCommand(repoPath, cmd);
|
|
3477
|
-
verified = verdictToVerified(verdict);
|
|
3478
|
-
if (verdict.toolMissing) {
|
|
3479
|
-
vspin.stop(
|
|
3480
|
-
theme.warn("Couldn't re-verify automatically") + theme.muted(` \u2014 \`${cmd}\` isn't available here. Run it yourself before committing.`)
|
|
3481
|
-
);
|
|
3482
|
-
} else if (verdict.timedOut) {
|
|
3483
|
-
vspin.stop(
|
|
3484
|
-
theme.warn(`Re-verification timed out \u2014 \`${cmd}\``) + theme.muted(" \u2014 run it yourself before committing.")
|
|
3485
|
-
);
|
|
3486
|
-
} else if (verdict.ok) {
|
|
3487
|
-
vspin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
3488
|
-
} else {
|
|
3489
|
-
vspin.stop(theme.alert(`Verification failed after instrumenting the new events \u2014 ${cmd}`));
|
|
3490
|
-
if (verdict.output) {
|
|
3491
|
-
p.note(verdict.output, theme.warn("Verifier output"));
|
|
3492
|
-
}
|
|
3493
|
-
const doRevert = await p.confirm({
|
|
3494
|
-
message: "Instrumenting the new events didn't pass the build/lint check. Revert just those edits? (The events stay in your universe \u2014 wire them on a future run.)",
|
|
3495
|
-
initialValue: true
|
|
3496
|
-
});
|
|
3497
|
-
if (!p.isCancel(doRevert) && doRevert) {
|
|
3498
|
-
if (await restoreToSnapshot(repoPath, checkpoint, instrumentationSnapshot)) {
|
|
3499
|
-
verified = preInstrumentationVerified;
|
|
3500
|
-
p.log.success(theme.success("Reverted the instrumentation edits."));
|
|
3501
|
-
} else {
|
|
3502
|
-
p.log.warn(
|
|
3503
|
-
theme.warn("Couldn't revert automatically \u2014 undo manually: ") + (revertHint(checkpoint) ?? "git reset --hard")
|
|
3504
|
-
);
|
|
3505
|
-
}
|
|
3506
|
-
} else {
|
|
3507
|
-
p.log.info(
|
|
3508
|
-
theme.muted("Left in your working tree \u2014 fix the verifier errors before committing.")
|
|
3509
|
-
);
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
}
|
|
3513
|
-
const filesForScan = acceptedEvents.length ? await changedFiles(repoPath, checkpoint) : files;
|
|
3514
|
-
const eventTypesToScan = [
|
|
3515
|
-
...manifest.events.map((e) => e.eventType),
|
|
3516
|
-
...acceptedEvents.map((e) => e.code)
|
|
3517
|
-
];
|
|
3518
|
-
const wiredMap = await scanWiredEvents(repoPath, filesForScan, eventTypesToScan);
|
|
3519
|
-
const reportEvents = eventTypesToScan.map((eventType) => ({
|
|
3520
|
-
event_type: eventType,
|
|
3521
|
-
status: wiredMap.has(eventType) ? "wired" : "skipped",
|
|
3522
|
-
file: wiredMap.get(eventType)
|
|
3523
|
-
}));
|
|
3524
|
-
const reportResult = await postRunReport(config, session, {
|
|
3525
|
-
target: chosen.playbook.target.id,
|
|
3526
|
-
repo_fingerprint: fingerprint,
|
|
3527
|
-
identify_wired: outcome.coreOk,
|
|
3528
|
-
verified,
|
|
3529
|
-
cost_usd: outcome.costUsd,
|
|
3530
|
-
duration_ms: outcome.durationMs,
|
|
3531
|
-
summary: scrubSummary(outcome.summary).slice(0, 4e3),
|
|
3532
|
-
events: reportEvents
|
|
3533
|
-
});
|
|
3534
|
-
if (!reportResult.ok) {
|
|
3535
|
-
p.log.warn(
|
|
3536
|
-
theme.warn("Couldn't record this run's coverage on the server") + theme.muted(
|
|
3537
|
-
` (${formatReportFailure(reportResult)}) \u2014 future runs may re-propose events already wired here.`
|
|
3538
|
-
)
|
|
3539
|
-
);
|
|
3540
|
-
}
|
|
5490
|
+
const reportedEvents = await reportRun(
|
|
5491
|
+
outcome.eventsComplete ? "completed" : "events_incomplete"
|
|
5492
|
+
);
|
|
5493
|
+
const reportEvents = reportedEvents.length ? reportedEvents : await collectReportEvents();
|
|
5494
|
+
const wiredMap = new Map(
|
|
5495
|
+
reportEvents.filter((event) => event.status === "wired").map((event) => [event.event_type, event.file ?? ""])
|
|
5496
|
+
);
|
|
3541
5497
|
try {
|
|
3542
5498
|
const approvedSuggestions = await approvedSuggestionsPromise;
|
|
3543
5499
|
if (approvedSuggestions?.length) {
|
|
@@ -3564,7 +5520,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3564
5520
|
)
|
|
3565
5521
|
)).filter(Boolean).length;
|
|
3566
5522
|
if (marked > 0) {
|
|
3567
|
-
|
|
5523
|
+
p2.log.info(
|
|
3568
5524
|
theme.muted(
|
|
3569
5525
|
`${marked} approved suggestion${marked === 1 ? "" : "s"} marked integrated.`
|
|
3570
5526
|
)
|
|
@@ -3575,38 +5531,35 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3575
5531
|
}
|
|
3576
5532
|
const eventLines = renderEventOutcomeLines(outcome.eventOutcomes, wiredMap);
|
|
3577
5533
|
if (eventLines) {
|
|
3578
|
-
|
|
5534
|
+
p2.note(eventLines, "Events");
|
|
3579
5535
|
}
|
|
3580
|
-
const eventOutcomeByType = new Map(
|
|
3581
|
-
outcome.eventOutcomes.map((event) => [event.event, event])
|
|
3582
|
-
);
|
|
3583
5536
|
const gapReport = buildGapReport({
|
|
3584
5537
|
manifest,
|
|
3585
5538
|
events: reportEvents.map((event) => ({
|
|
3586
5539
|
eventType: event.event_type,
|
|
3587
5540
|
status: event.status,
|
|
3588
|
-
reason:
|
|
5541
|
+
reason: event.reason
|
|
3589
5542
|
}))
|
|
3590
5543
|
});
|
|
3591
5544
|
if (gapReport.hasContent) {
|
|
3592
|
-
|
|
5545
|
+
p2.note(
|
|
3593
5546
|
renderGapReport(gapReport, theme),
|
|
3594
5547
|
theme.signal("What you could be doing")
|
|
3595
5548
|
);
|
|
3596
5549
|
}
|
|
3597
|
-
const eventDenominator = manifest.universeSummary?.wireableHere ??
|
|
5550
|
+
const eventDenominator = manifest.universeSummary?.wireableHere ?? reportEvents.length;
|
|
3598
5551
|
const eventStatsLabel = manifest.universeSummary ? "events wired here" : "events wired";
|
|
3599
5552
|
const timingDetails = outcome.phaseTimings.map(({ phase, ms }) => `${shortPhaseLabel(phase)} ${formatDuration(ms)}`).join(" \xB7 ");
|
|
3600
|
-
|
|
5553
|
+
p2.log.info(
|
|
3601
5554
|
theme.muted(
|
|
3602
5555
|
`${wiredMap.size}/${eventDenominator} ${eventStatsLabel} \xB7 ${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ` + (verified === true ? "verified \xB7 " : verified === false ? "unverified \xB7 " : "") + formatDuration(outcome.durationMs) + (timingDetails ? ` (${timingDetails})` : "")
|
|
3603
5556
|
)
|
|
3604
5557
|
);
|
|
3605
5558
|
if (!config.offline) {
|
|
3606
|
-
|
|
5559
|
+
p2.log.step(
|
|
3607
5560
|
theme.bright("Run your app once") + theme.muted(" and trigger any tracked action \u2014 I'll watch for the first event.")
|
|
3608
5561
|
);
|
|
3609
|
-
const verifySpin =
|
|
5562
|
+
const verifySpin = p2.spinner();
|
|
3610
5563
|
verifySpin.start("Waiting for the first event from your app");
|
|
3611
5564
|
const first = await pollFirstEvent(config, session, { timeoutMs: 12e4 });
|
|
3612
5565
|
if (first.received) {
|
|
@@ -3630,31 +5583,27 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3630
5583
|
`${theme.signal("3.")} Commit & deploy \u2014 Whisperr starts working on real users.`,
|
|
3631
5584
|
undo ? theme.muted(`Undo everything: ${undo}`) : ""
|
|
3632
5585
|
].filter(Boolean).join("\n");
|
|
3633
|
-
|
|
3634
|
-
|
|
5586
|
+
p2.note(nextSteps, theme.bright("Next"));
|
|
5587
|
+
p2.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
|
|
3635
5588
|
return 0;
|
|
3636
5589
|
}
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
const
|
|
5590
|
+
async function applyOpportunities(opts) {
|
|
5591
|
+
const { config, session, playbook, manifest, fingerprint, raw, additions } = opts;
|
|
5592
|
+
const clean = sanitizeOpportunities(raw, manifest);
|
|
3640
5593
|
const openSuggestions = await fetchSuggestions(config, session, ["proposed", "approved"]);
|
|
3641
|
-
const
|
|
3642
|
-
openSuggestions.map(
|
|
3643
|
-
(suggestion) => `${suggestion.kind}:${normalizeCode(suggestion.code)}`
|
|
3644
|
-
)
|
|
5594
|
+
const openKeys = new Set(
|
|
5595
|
+
openSuggestions.map((s) => `${s.kind}:${normalizeCode(s.code)}`)
|
|
3645
5596
|
);
|
|
3646
5597
|
const opportunities = {
|
|
3647
|
-
events:
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
interventions: opts.opportunities.interventions.filter(
|
|
3651
|
-
(intervention) => !openSuggestionKeys.has(`intervention:${normalizeCode(intervention.code)}`)
|
|
5598
|
+
events: clean.events.filter((e) => !openKeys.has(`event:${normalizeCode(e.code)}`)),
|
|
5599
|
+
interventions: clean.interventions.filter(
|
|
5600
|
+
(i) => !openKeys.has(`intervention:${normalizeCode(i.code)}`)
|
|
3652
5601
|
)
|
|
3653
5602
|
};
|
|
3654
|
-
|
|
3655
|
-
if (
|
|
5603
|
+
additions.proposed = opportunities.events.length + opportunities.interventions.length;
|
|
5604
|
+
if (additions.proposed === 0) return [];
|
|
3656
5605
|
const describe = (kind, code, why) => `${theme.muted(`${kind}: `)}${theme.bright(code)}${why ? theme.muted(` \u2014 ${why}`) : ""}`;
|
|
3657
|
-
|
|
5606
|
+
p2.note(
|
|
3658
5607
|
[
|
|
3659
5608
|
...opportunities.events.map((e) => describe("event", e.code, e.rationale ?? e.description)),
|
|
3660
5609
|
...opportunities.interventions.map(
|
|
@@ -3664,111 +5613,30 @@ async function offerOpportunities(opts) {
|
|
|
3664
5613
|
theme.signal("Opportunities found beyond your onboarding plan")
|
|
3665
5614
|
);
|
|
3666
5615
|
if (config.offline) {
|
|
3667
|
-
|
|
3668
|
-
return
|
|
3669
|
-
}
|
|
3670
|
-
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
3671
|
-
p.log.info(
|
|
3672
|
-
theme.muted("Non-interactive run \u2014 proposals were not submitted. Re-run interactively to add them.")
|
|
3673
|
-
);
|
|
3674
|
-
return NO_ADDITIONS;
|
|
3675
|
-
}
|
|
3676
|
-
const selection = await p.multiselect({
|
|
3677
|
-
message: "Send these to your dashboard for approval?",
|
|
3678
|
-
options: [
|
|
3679
|
-
...opportunities.events.map((e) => ({
|
|
3680
|
-
value: `e:${e.code}`,
|
|
3681
|
-
label: `event \xB7 ${e.code}`,
|
|
3682
|
-
hint: e.description ?? e.rationale
|
|
3683
|
-
})),
|
|
3684
|
-
...opportunities.interventions.map((i) => ({
|
|
3685
|
-
value: `i:${i.code}`,
|
|
3686
|
-
label: `intervention \xB7 ${i.code}`,
|
|
3687
|
-
hint: i.description ?? i.rationale
|
|
3688
|
-
}))
|
|
3689
|
-
],
|
|
3690
|
-
// Opt-in per item: nothing is pre-selected, so submitting a suggestion is
|
|
3691
|
-
// an explicit choice rather than an opt-out the user has to notice and undo.
|
|
3692
|
-
initialValues: [],
|
|
3693
|
-
required: false
|
|
3694
|
-
});
|
|
3695
|
-
if (p.isCancel(selection) || selection.length === 0) {
|
|
3696
|
-
p.log.info(theme.muted("Skipped \u2014 your universe is unchanged."));
|
|
3697
|
-
return NO_ADDITIONS;
|
|
5616
|
+
p2.log.info(theme.muted("Offline mode \u2014 proposals shown only, nothing submitted."));
|
|
5617
|
+
return [];
|
|
3698
5618
|
}
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
);
|
|
3703
|
-
const knownEventCodes = new Set(manifest.events.map((e) => e.eventType));
|
|
3704
|
-
const pickedEvents = opportunities.events.filter((e) => chosenSet.has(`e:${e.code}`));
|
|
3705
|
-
const pickedInterventions = opportunities.interventions.filter(
|
|
3706
|
-
(i) => chosenSet.has(`i:${i.code}`)
|
|
3707
|
-
);
|
|
3708
|
-
const pickedInterventionCodes = new Set(pickedInterventions.map((i) => i.code));
|
|
3709
|
-
const pickedEventCodes = new Set(pickedEvents.map((e) => e.code));
|
|
3710
|
-
const submitted = {
|
|
3711
|
-
events: pickedEvents.map((e) => ({
|
|
3712
|
-
...e,
|
|
3713
|
-
links: e.links?.filter(
|
|
3714
|
-
(l) => knownInterventionCodes.has(l.interventionCode) || pickedInterventionCodes.has(l.interventionCode)
|
|
3715
|
-
)
|
|
3716
|
-
})),
|
|
3717
|
-
interventions: pickedInterventions.map((i) => ({
|
|
3718
|
-
...i,
|
|
3719
|
-
links: i.links?.filter(
|
|
3720
|
-
(l) => knownEventCodes.has(l.eventCode) || pickedEventCodes.has(l.eventCode)
|
|
3721
|
-
)
|
|
3722
|
-
}))
|
|
3723
|
-
};
|
|
3724
|
-
let stageResult;
|
|
3725
|
-
try {
|
|
3726
|
-
stageResult = await withSpinner(
|
|
3727
|
-
"Sending suggestions for approval",
|
|
3728
|
-
() => submitSuggestions(config, session, opts.playbook.target.id, opts.fingerprint, submitted)
|
|
3729
|
-
);
|
|
3730
|
-
} catch (err) {
|
|
3731
|
-
p.log.warn(
|
|
3732
|
-
theme.warn("Couldn't send the proposals") + theme.muted(` \u2014 ${err.message}. Nothing was submitted.`)
|
|
3733
|
-
);
|
|
3734
|
-
return NO_ADDITIONS;
|
|
3735
|
-
}
|
|
3736
|
-
if (stageResult) {
|
|
3737
|
-
const lines2 = stageResult.outcomes.map((outcome) => {
|
|
3738
|
-
const mark = outcome.status === "staged" ? theme.success("\u2713") : outcome.status === "duplicate" ? theme.muted("=") : theme.warn("\u2717");
|
|
3739
|
-
const reason = outcome.reason ?? (outcome.duplicateOf && outcome.duplicateOf !== outcome.code ? `already queued as ${outcome.duplicateOf}` : void 0);
|
|
3740
|
-
const detail = reason ? theme.muted(` (${reason})`) : "";
|
|
3741
|
-
return `${mark} ${outcome.kind} ${theme.bright(outcome.code)}${detail}`;
|
|
3742
|
-
});
|
|
3743
|
-
lines2.push(
|
|
3744
|
-
theme.muted(
|
|
3745
|
-
`${stageResult.staged} staged \xB7 ${stageResult.duplicates} already queued \xB7 ${stageResult.invalid} rejected`
|
|
3746
|
-
)
|
|
3747
|
-
);
|
|
3748
|
-
p.note(lines2.join("\n"), theme.signal("Suggestions submitted"));
|
|
3749
|
-
const where = stageResult.approvalsUrl ? `approve at ${theme.bright(stageResult.approvalsUrl)}` : "approve them in your Whisperr dashboard";
|
|
3750
|
-
p.log.info(
|
|
3751
|
-
theme.muted(
|
|
3752
|
-
`${stageResult.staged} proposal${stageResult.staged === 1 ? "" : "s"} sent \u2014 ${where}. The next wizard run wires whatever you approve.`
|
|
3753
|
-
)
|
|
3754
|
-
);
|
|
3755
|
-
return NO_ADDITIONS;
|
|
5619
|
+
if (config.proposeOnly) {
|
|
5620
|
+
await stageOpportunities(config, session, playbook, fingerprint, opportunities);
|
|
5621
|
+
return [];
|
|
3756
5622
|
}
|
|
3757
5623
|
let result;
|
|
3758
5624
|
try {
|
|
3759
5625
|
result = await withSpinner(
|
|
3760
5626
|
"Adding to your universe",
|
|
3761
|
-
() => submitAdditions(config, session,
|
|
5627
|
+
() => submitAdditions(config, session, playbook.target.id, fingerprint, opportunities)
|
|
3762
5628
|
);
|
|
3763
5629
|
} catch (err) {
|
|
3764
|
-
|
|
5630
|
+
p2.log.warn(
|
|
3765
5631
|
theme.warn("Couldn't add the proposals") + theme.muted(` \u2014 ${err.message}. Your universe is unchanged.`)
|
|
3766
5632
|
);
|
|
3767
|
-
return
|
|
5633
|
+
return [];
|
|
3768
5634
|
}
|
|
3769
5635
|
const lines = result.outcomes.map((o) => {
|
|
3770
5636
|
const mark = o.status === "applied" ? theme.success("\u2713") : o.status === "duplicate" ? theme.muted("=") : theme.warn("\u2717");
|
|
3771
|
-
const detail = o.status === "duplicate" ? theme.muted(
|
|
5637
|
+
const detail = o.status === "duplicate" ? theme.muted(
|
|
5638
|
+
` (already in your universe${o.duplicateOf && o.duplicateOf !== o.code ? ` as ${o.duplicateOf}` : ""})`
|
|
5639
|
+
) : o.status === "invalid" && o.reason ? theme.muted(` (${o.reason})`) : "";
|
|
3772
5640
|
return `${mark} ${o.kind} ${theme.bright(o.code)}${detail}`;
|
|
3773
5641
|
});
|
|
3774
5642
|
lines.push(
|
|
@@ -3776,7 +5644,7 @@ async function offerOpportunities(opts) {
|
|
|
3776
5644
|
`${result.applied} added \xB7 ${result.duplicates} already present \xB7 ${result.invalid} rejected`
|
|
3777
5645
|
)
|
|
3778
5646
|
);
|
|
3779
|
-
if (
|
|
5647
|
+
if (opportunities.interventions.length) {
|
|
3780
5648
|
lines.push(theme.muted("New interventions start paused \u2014 activate them in your dashboard."));
|
|
3781
5649
|
}
|
|
3782
5650
|
if (result.policyRegen?.status === "pending") {
|
|
@@ -3791,68 +5659,76 @@ async function offerOpportunities(opts) {
|
|
|
3791
5659
|
)
|
|
3792
5660
|
);
|
|
3793
5661
|
}
|
|
3794
|
-
|
|
5662
|
+
p2.note(lines.join("\n"), theme.signal("Universe updated"));
|
|
3795
5663
|
if (result.policyRegen?.status === "failed") {
|
|
3796
|
-
|
|
5664
|
+
p2.log.warn(
|
|
3797
5665
|
theme.warn("Live runtime NOT updated") + theme.muted(
|
|
3798
5666
|
" \u2014 the additions were recorded, but the runtime policy wasn't regenerated" + (result.policyRegen.reason ? `: ${result.policyRegen.reason}` : "") + ". They won't drive live decisions until the policy regenerates."
|
|
3799
5667
|
)
|
|
3800
5668
|
);
|
|
3801
5669
|
}
|
|
3802
|
-
const
|
|
5670
|
+
const appliedCodes = new Set(
|
|
3803
5671
|
result.outcomes.filter((o) => o.kind === "event" && o.status === "applied").map((o) => o.code)
|
|
3804
5672
|
);
|
|
3805
|
-
const
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
5673
|
+
const applied = opportunities.events.filter((e) => appliedCodes.has(e.code));
|
|
5674
|
+
additions.applied = applied;
|
|
5675
|
+
return applied;
|
|
5676
|
+
}
|
|
5677
|
+
async function stageOpportunities(config, session, playbook, fingerprint, opportunities) {
|
|
5678
|
+
let stageResult;
|
|
3810
5679
|
try {
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
config,
|
|
3814
|
-
|
|
3815
|
-
playbook: opts.playbook,
|
|
3816
|
-
acceptedEvents,
|
|
3817
|
-
budgetUsd: opts.remainingBudgetUsd,
|
|
3818
|
-
repoMap: opts.repoMap
|
|
3819
|
-
});
|
|
3820
|
-
if (pass.ran) {
|
|
3821
|
-
spin.stop(theme.success("New events instrumented"));
|
|
3822
|
-
logAgentSummary(pass.summary);
|
|
3823
|
-
} else {
|
|
3824
|
-
spin.stop(
|
|
3825
|
-
theme.warn("Skipped instrumenting the new events") + theme.muted(
|
|
3826
|
-
" \u2014 the spend limit was reached. They're in your universe; wire them on a future run."
|
|
3827
|
-
)
|
|
3828
|
-
);
|
|
3829
|
-
}
|
|
5680
|
+
stageResult = await withSpinner(
|
|
5681
|
+
"Sending suggestions for approval",
|
|
5682
|
+
() => submitSuggestions(config, session, playbook.target.id, fingerprint, opportunities)
|
|
5683
|
+
);
|
|
3830
5684
|
} catch (err) {
|
|
3831
|
-
|
|
3832
|
-
theme.warn("Couldn't
|
|
5685
|
+
p2.log.warn(
|
|
5686
|
+
theme.warn("Couldn't send the proposals") + theme.muted(` \u2014 ${err.message}. Nothing was submitted.`)
|
|
5687
|
+
);
|
|
5688
|
+
return;
|
|
5689
|
+
}
|
|
5690
|
+
if (!stageResult) {
|
|
5691
|
+
p2.log.warn(
|
|
5692
|
+
theme.warn("This Whisperr backend has no approval queue") + theme.muted(" \u2014 nothing was submitted. Re-run without --propose-only to add them directly.")
|
|
3833
5693
|
);
|
|
5694
|
+
return;
|
|
3834
5695
|
}
|
|
3835
|
-
const
|
|
3836
|
-
|
|
5696
|
+
const lines = stageResult.outcomes.map((outcome) => {
|
|
5697
|
+
const mark = outcome.status === "staged" ? theme.success("\u2713") : outcome.status === "duplicate" ? theme.muted("=") : theme.warn("\u2717");
|
|
5698
|
+
const reason = outcome.reason ?? (outcome.duplicateOf && outcome.duplicateOf !== outcome.code ? `already queued as ${outcome.duplicateOf}` : void 0);
|
|
5699
|
+
return `${mark} ${outcome.kind} ${theme.bright(outcome.code)}${reason ? theme.muted(` (${reason})`) : ""}`;
|
|
5700
|
+
});
|
|
5701
|
+
lines.push(
|
|
5702
|
+
theme.muted(
|
|
5703
|
+
`${stageResult.staged} staged \xB7 ${stageResult.duplicates} already queued \xB7 ${stageResult.invalid} rejected`
|
|
5704
|
+
)
|
|
5705
|
+
);
|
|
5706
|
+
p2.note(lines.join("\n"), theme.signal("Suggestions submitted"));
|
|
5707
|
+
const where = stageResult.approvalsUrl ? `approve at ${theme.bright(stageResult.approvalsUrl)}` : "approve them in your Whisperr dashboard";
|
|
5708
|
+
p2.log.info(
|
|
5709
|
+
theme.muted(
|
|
5710
|
+
`${stageResult.staged} proposal${stageResult.staged === 1 ? "" : "s"} sent \u2014 ${where}. The next wizard run wires whatever you approve.`
|
|
5711
|
+
)
|
|
5712
|
+
);
|
|
3837
5713
|
}
|
|
3838
5714
|
async function maybeRevert(repoPath, checkpoint, reason) {
|
|
3839
5715
|
if (!checkpoint.isRepo) return false;
|
|
3840
5716
|
const files = await changedFiles(repoPath, checkpoint);
|
|
3841
5717
|
if (files.length === 0) return false;
|
|
3842
|
-
const doRevert = await
|
|
5718
|
+
const doRevert = await p2.confirm({
|
|
3843
5719
|
message: `${reason} Revert all of the wizard's changes now?`,
|
|
3844
5720
|
initialValue: true
|
|
3845
5721
|
});
|
|
3846
|
-
if (
|
|
5722
|
+
if (p2.isCancel(doRevert) || !doRevert) {
|
|
3847
5723
|
const hint = revertHint(checkpoint);
|
|
3848
|
-
if (hint)
|
|
5724
|
+
if (hint) p2.log.info(theme.muted(`Left in your working tree. Undo anytime: ${hint}`));
|
|
3849
5725
|
return false;
|
|
3850
5726
|
}
|
|
3851
5727
|
const ok = await revertToCheckpoint(repoPath, checkpoint);
|
|
3852
5728
|
if (ok) {
|
|
3853
|
-
|
|
5729
|
+
p2.log.success(theme.success("Reverted to your pre-wizard state."));
|
|
3854
5730
|
} else {
|
|
3855
|
-
|
|
5731
|
+
p2.log.warn(
|
|
3856
5732
|
theme.warn("Couldn't revert automatically \u2014 undo manually: ") + (revertHint(checkpoint) ?? "git reset --hard")
|
|
3857
5733
|
);
|
|
3858
5734
|
}
|
|
@@ -3883,11 +5759,11 @@ async function chooseTarget(detections) {
|
|
|
3883
5759
|
hint: pb.target.availability === "available" ? void 0 : "coming soon"
|
|
3884
5760
|
});
|
|
3885
5761
|
}
|
|
3886
|
-
const answer = await
|
|
5762
|
+
const answer = await p2.select({
|
|
3887
5763
|
message: detections.length ? "I found more than one possible stack \u2014 which should I integrate?" : "I couldn't auto-detect your stack. Which are you using?",
|
|
3888
5764
|
options
|
|
3889
5765
|
});
|
|
3890
|
-
if (
|
|
5766
|
+
if (p2.isCancel(answer)) return null;
|
|
3891
5767
|
const playbook = playbookByTargetId(answer);
|
|
3892
5768
|
if (!playbook) return null;
|
|
3893
5769
|
return {
|
|
@@ -3896,11 +5772,11 @@ async function chooseTarget(detections) {
|
|
|
3896
5772
|
};
|
|
3897
5773
|
}
|
|
3898
5774
|
async function withBrowserAuth(config) {
|
|
3899
|
-
|
|
5775
|
+
p2.log.step(
|
|
3900
5776
|
theme.bright("Authenticate") + theme.muted(" \u2014 opening your browser to approve this device\u2026")
|
|
3901
5777
|
);
|
|
3902
5778
|
const auth = await startDeviceAuth(config);
|
|
3903
|
-
|
|
5779
|
+
p2.note(
|
|
3904
5780
|
[
|
|
3905
5781
|
theme.bright(`Your code: ${auth.userCode}`),
|
|
3906
5782
|
`${theme.muted("Approval URL:")} ${auth.verificationUrl}`,
|
|
@@ -3914,7 +5790,7 @@ async function withBrowserAuth(config) {
|
|
|
3914
5790
|
await open2(auth.verificationUrlComplete ?? auth.verificationUrl);
|
|
3915
5791
|
} catch {
|
|
3916
5792
|
}
|
|
3917
|
-
const spin =
|
|
5793
|
+
const spin = p2.spinner();
|
|
3918
5794
|
spin.start("Waiting for you to approve in the browser");
|
|
3919
5795
|
try {
|
|
3920
5796
|
const session = await auth.poll();
|
|
@@ -3926,7 +5802,7 @@ async function withBrowserAuth(config) {
|
|
|
3926
5802
|
}
|
|
3927
5803
|
}
|
|
3928
5804
|
async function withSpinner(label, fn) {
|
|
3929
|
-
const spin =
|
|
5805
|
+
const spin = p2.spinner();
|
|
3930
5806
|
spin.start(label);
|
|
3931
5807
|
try {
|
|
3932
5808
|
const result = await fn();
|
|
@@ -3989,16 +5865,14 @@ function renderEventOutcomeLines(outcomes, wiredMap) {
|
|
|
3989
5865
|
}
|
|
3990
5866
|
function logAgentSummary(summary) {
|
|
3991
5867
|
const cleaned = scrubSummary(summary).trim();
|
|
3992
|
-
if (cleaned)
|
|
5868
|
+
if (cleaned) p2.log.message(cleaned);
|
|
3993
5869
|
}
|
|
3994
5870
|
function shortPhaseLabel(phase) {
|
|
3995
5871
|
switch (phase) {
|
|
3996
|
-
case "
|
|
3997
|
-
return "
|
|
5872
|
+
case "Planning your integration":
|
|
5873
|
+
return "plan";
|
|
3998
5874
|
case "Installing the SDK & wiring identify()":
|
|
3999
5875
|
return "core";
|
|
4000
|
-
case "Planning event placements":
|
|
4001
|
-
return "plan";
|
|
4002
5876
|
case "Instrumenting planned events":
|
|
4003
5877
|
case "Instrumenting your events":
|
|
4004
5878
|
return "wire";
|
|
@@ -4031,12 +5905,7 @@ function formatReportFailure(result) {
|
|
|
4031
5905
|
}
|
|
4032
5906
|
|
|
4033
5907
|
// src/index.ts
|
|
4034
|
-
var modulePath =
|
|
4035
|
-
function packageVersion() {
|
|
4036
|
-
const packagePath = join4(dirname2(modulePath), "..", "package.json");
|
|
4037
|
-
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
4038
|
-
return pkg.version ?? "0.0.0";
|
|
4039
|
-
}
|
|
5908
|
+
var modulePath = fileURLToPath2(import.meta.url);
|
|
4040
5909
|
function parseArgs(argv) {
|
|
4041
5910
|
const args = [...argv];
|
|
4042
5911
|
const opts = {};
|
|
@@ -4056,6 +5925,15 @@ function parseArgs(argv) {
|
|
|
4056
5925
|
case "--force":
|
|
4057
5926
|
opts.force = true;
|
|
4058
5927
|
break;
|
|
5928
|
+
case "--review-plan":
|
|
5929
|
+
opts.reviewPlan = true;
|
|
5930
|
+
break;
|
|
5931
|
+
case "--propose-only":
|
|
5932
|
+
opts.proposeOnly = true;
|
|
5933
|
+
break;
|
|
5934
|
+
case "--strict-review":
|
|
5935
|
+
opts.strictReview = true;
|
|
5936
|
+
break;
|
|
4059
5937
|
case "--api":
|
|
4060
5938
|
opts.apiBaseUrl = args[++i];
|
|
4061
5939
|
break;
|
|
@@ -4082,6 +5960,10 @@ function printHelp() {
|
|
|
4082
5960
|
` ${theme.bright("Options")}`,
|
|
4083
5961
|
" --offline Use a demo manifest, no account/browser needed",
|
|
4084
5962
|
" --force Proceed without a clean git tree (no safe undo)",
|
|
5963
|
+
" --review-plan Confirm the placement plan before anything is wired",
|
|
5964
|
+
" --propose-only Send new events/interventions for dashboard approval",
|
|
5965
|
+
" instead of adding them to your universe now",
|
|
5966
|
+
" --strict-review Review the finished diff with the deeper planner model",
|
|
4085
5967
|
" --api <url> Override the Whisperr API base URL",
|
|
4086
5968
|
" --model <id> Override the coding-agent model",
|
|
4087
5969
|
" -h, --help Show this help",
|