@whisperr/wizard 0.2.0 → 0.3.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 +48 -10
- package/dist/index.js +1719 -194
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { readFileSync, realpathSync } from "fs";
|
|
5
|
-
import { dirname, join as
|
|
5
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
|
-
import { resolve } from "path";
|
|
9
|
+
import { resolve as resolve2 } from "path";
|
|
10
10
|
import * as p from "@clack/prompts";
|
|
11
11
|
|
|
12
12
|
// src/core/config.ts
|
|
13
13
|
var DEFAULT_API_BASE = "https://api.whisperr.net";
|
|
14
14
|
var DEFAULT_MODEL = "claude-sonnet-5";
|
|
15
|
+
var DEFAULT_PLANNER_MODEL = "claude-opus-4-8";
|
|
15
16
|
var DEFAULT_EFFORT = "high";
|
|
16
17
|
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
17
18
|
function resolveEffort() {
|
|
@@ -29,6 +30,7 @@ function resolveConfig(flags = {}) {
|
|
|
29
30
|
apiBaseUrl,
|
|
30
31
|
llmBaseUrl,
|
|
31
32
|
model: flags.model ?? process.env.WHISPERR_WIZARD_MODEL ?? DEFAULT_MODEL,
|
|
33
|
+
plannerModel: process.env.WHISPERR_WIZARD_PLANNER_MODEL ?? DEFAULT_PLANNER_MODEL,
|
|
32
34
|
effort: resolveEffort(),
|
|
33
35
|
// Cost is the real limiter (budgetUsd below). maxTurns is only a high safety
|
|
34
36
|
// backstop against a stuck loop — it should NOT bind on a normal run. (It
|
|
@@ -36,7 +38,7 @@ function resolveConfig(flags = {}) {
|
|
|
36
38
|
maxTurns: Number(process.env.WHISPERR_WIZARD_MAX_TURNS) || 200,
|
|
37
39
|
// Hard ceiling on TOTAL spend across all phases. If hit, the run stops
|
|
38
40
|
// cleanly and keeps whatever already landed. Real runs finish well under it.
|
|
39
|
-
budgetUsd: Number(process.env.WHISPERR_WIZARD_BUDGET_USD) ||
|
|
41
|
+
budgetUsd: Number(process.env.WHISPERR_WIZARD_BUDGET_USD) || 25,
|
|
40
42
|
directAnthropicKey,
|
|
41
43
|
offline
|
|
42
44
|
};
|
|
@@ -1148,6 +1150,355 @@ function mockManifest(appId, config) {
|
|
|
1148
1150
|
// src/core/agent.ts
|
|
1149
1151
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1150
1152
|
|
|
1153
|
+
// src/core/git.ts
|
|
1154
|
+
import { spawn } from "child_process";
|
|
1155
|
+
import { createHash } from "crypto";
|
|
1156
|
+
import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
|
|
1157
|
+
import { basename, dirname, join as join2 } from "path";
|
|
1158
|
+
async function takeCheckpoint(repoPath) {
|
|
1159
|
+
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
1160
|
+
if (!isRepo) return { isRepo: false };
|
|
1161
|
+
const branch = (await run(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
|
1162
|
+
const head = await run(repoPath, ["rev-parse", "HEAD"]);
|
|
1163
|
+
return {
|
|
1164
|
+
isRepo: true,
|
|
1165
|
+
baseRef: head.ok ? head.stdout.trim() : void 0,
|
|
1166
|
+
branch: branch || void 0
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
async function changedFiles(repoPath, checkpoint) {
|
|
1170
|
+
if (!checkpoint.isRepo) return [];
|
|
1171
|
+
const tracked = await run(repoPath, ["diff", "--name-only"]);
|
|
1172
|
+
const untracked = await run(repoPath, [
|
|
1173
|
+
"ls-files",
|
|
1174
|
+
"--others",
|
|
1175
|
+
"--exclude-standard"
|
|
1176
|
+
]);
|
|
1177
|
+
const set = /* @__PURE__ */ new Set();
|
|
1178
|
+
for (const f of `${tracked.stdout}
|
|
1179
|
+
${untracked.stdout}`.split("\n")) {
|
|
1180
|
+
if (f.trim()) set.add(f.trim());
|
|
1181
|
+
}
|
|
1182
|
+
return [...set];
|
|
1183
|
+
}
|
|
1184
|
+
function revertHint(checkpoint) {
|
|
1185
|
+
if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
|
|
1186
|
+
return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
|
|
1187
|
+
}
|
|
1188
|
+
async function wasTrackedAtCheckpoint(repoPath, checkpoint, file) {
|
|
1189
|
+
if (!checkpoint.isRepo) return false;
|
|
1190
|
+
if (checkpoint.baseRef) {
|
|
1191
|
+
const res2 = await run(repoPath, [
|
|
1192
|
+
"ls-tree",
|
|
1193
|
+
"--name-only",
|
|
1194
|
+
checkpoint.baseRef,
|
|
1195
|
+
"--",
|
|
1196
|
+
file
|
|
1197
|
+
]);
|
|
1198
|
+
return res2.ok && res2.stdout.trim() !== "";
|
|
1199
|
+
}
|
|
1200
|
+
const res = await run(repoPath, ["ls-files", "--", file]);
|
|
1201
|
+
return res.ok && res.stdout.trim() !== "";
|
|
1202
|
+
}
|
|
1203
|
+
async function isWorkingTreeClean(repoPath) {
|
|
1204
|
+
const status = await run(repoPath, ["status", "--porcelain"]);
|
|
1205
|
+
return status.ok && status.stdout.trim() === "";
|
|
1206
|
+
}
|
|
1207
|
+
async function revertToCheckpoint(repoPath, checkpoint) {
|
|
1208
|
+
if (!checkpoint.isRepo) return false;
|
|
1209
|
+
if (checkpoint.baseRef) {
|
|
1210
|
+
const reset = await run(repoPath, ["reset", "--hard", checkpoint.baseRef]);
|
|
1211
|
+
const clean2 = await run(repoPath, ["clean", "-fd"]);
|
|
1212
|
+
return reset.ok && clean2.ok;
|
|
1213
|
+
}
|
|
1214
|
+
const restore = await run(repoPath, ["restore", "."]);
|
|
1215
|
+
const clean = await run(repoPath, ["clean", "-fd"]);
|
|
1216
|
+
return restore.ok && clean.ok;
|
|
1217
|
+
}
|
|
1218
|
+
async function snapshotChanges(repoPath, checkpoint) {
|
|
1219
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
1220
|
+
for (const file of await changedFiles(repoPath, checkpoint)) {
|
|
1221
|
+
try {
|
|
1222
|
+
snapshot.set(file, await readFile2(join2(repoPath, file)));
|
|
1223
|
+
} catch {
|
|
1224
|
+
snapshot.set(file, null);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
return snapshot;
|
|
1228
|
+
}
|
|
1229
|
+
function snapshotsEqual(a, b) {
|
|
1230
|
+
if (a.size !== b.size) return false;
|
|
1231
|
+
for (const [file, content] of a) {
|
|
1232
|
+
const other = b.get(file);
|
|
1233
|
+
if (other === void 0) return false;
|
|
1234
|
+
if (content === null || other === null) {
|
|
1235
|
+
if (content !== other) return false;
|
|
1236
|
+
} else if (!content.equals(other)) {
|
|
1237
|
+
return false;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return true;
|
|
1241
|
+
}
|
|
1242
|
+
async function restoreToSnapshot(repoPath, checkpoint, snapshot) {
|
|
1243
|
+
try {
|
|
1244
|
+
const current = await changedFiles(repoPath, checkpoint);
|
|
1245
|
+
for (const file of /* @__PURE__ */ new Set([...current, ...snapshot.keys()])) {
|
|
1246
|
+
const recorded = snapshot.get(file);
|
|
1247
|
+
const path = join2(repoPath, file);
|
|
1248
|
+
if (recorded === void 0) {
|
|
1249
|
+
if (checkpoint.baseRef) {
|
|
1250
|
+
const co = await run(repoPath, ["checkout", checkpoint.baseRef, "--", file]);
|
|
1251
|
+
if (co.ok) continue;
|
|
1252
|
+
}
|
|
1253
|
+
await rm(path, { force: true });
|
|
1254
|
+
} else if (recorded === null) {
|
|
1255
|
+
await rm(path, { force: true });
|
|
1256
|
+
} else {
|
|
1257
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1258
|
+
await writeFile(path, recorded);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return true;
|
|
1262
|
+
} catch {
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
async function repoFingerprint(repoPath) {
|
|
1267
|
+
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
1268
|
+
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename(repoPath)}`;
|
|
1269
|
+
return createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
1270
|
+
}
|
|
1271
|
+
function normalizeRemote(url) {
|
|
1272
|
+
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
1273
|
+
}
|
|
1274
|
+
async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
1275
|
+
const wired = /* @__PURE__ */ new Map();
|
|
1276
|
+
const patterns = eventTypes.map((e) => ({
|
|
1277
|
+
eventType: e,
|
|
1278
|
+
trackRe: new RegExp(
|
|
1279
|
+
`\\btrack\\s*\\(\\s*[\\s\\S]{0,160}?['"\`]${escapeRegExp(e)}['"\`]`
|
|
1280
|
+
),
|
|
1281
|
+
literalRe: quotedLiteralRegExp(e)
|
|
1282
|
+
}));
|
|
1283
|
+
const contents = [];
|
|
1284
|
+
for (const file of files) {
|
|
1285
|
+
try {
|
|
1286
|
+
contents.push({ file, content: await readFile2(join2(repoPath, file), "utf8") });
|
|
1287
|
+
} catch {
|
|
1288
|
+
continue;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
for (const { file, content } of contents) {
|
|
1292
|
+
for (const { eventType, trackRe } of patterns) {
|
|
1293
|
+
if (!wired.has(eventType) && trackRe.test(content)) {
|
|
1294
|
+
wired.set(eventType, file);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
for (const { file, content } of contents) {
|
|
1299
|
+
for (const { eventType, literalRe } of patterns) {
|
|
1300
|
+
if (!wired.has(eventType) && literalRe.test(content)) {
|
|
1301
|
+
wired.set(eventType, file);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return wired;
|
|
1306
|
+
}
|
|
1307
|
+
function quotedLiteralRegExp(eventType) {
|
|
1308
|
+
const leftBoundary = /^\w/.test(eventType) ? "\\b" : "";
|
|
1309
|
+
const rightBoundary = /\w$/.test(eventType) ? "\\b" : "";
|
|
1310
|
+
return new RegExp(`(['"\`])${leftBoundary}${escapeRegExp(eventType)}${rightBoundary}\\1`);
|
|
1311
|
+
}
|
|
1312
|
+
function escapeRegExp(s) {
|
|
1313
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1314
|
+
}
|
|
1315
|
+
function run(cwd, args) {
|
|
1316
|
+
return new Promise((resolve3) => {
|
|
1317
|
+
const child = spawn("git", args, { cwd });
|
|
1318
|
+
let stdout = "";
|
|
1319
|
+
let stderr = "";
|
|
1320
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
1321
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
1322
|
+
child.on("close", (code) => resolve3({ ok: code === 0, stdout, stderr }));
|
|
1323
|
+
child.on("error", () => resolve3({ ok: false, stdout, stderr }));
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/core/opportunities.ts
|
|
1328
|
+
import { readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
1329
|
+
import { join as join3 } from "path";
|
|
1330
|
+
var OPPORTUNITIES_FILE = "whisperr-opportunities.json";
|
|
1331
|
+
function normalizeCode(value) {
|
|
1332
|
+
return value.trim().toLowerCase().replace(/[ \-./]/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
1333
|
+
}
|
|
1334
|
+
async function collectOpportunities(repoPath, manifest, checkpoint) {
|
|
1335
|
+
const path = join3(repoPath, OPPORTUNITIES_FILE);
|
|
1336
|
+
if (await wasTrackedAtCheckpoint(repoPath, checkpoint, OPPORTUNITIES_FILE)) {
|
|
1337
|
+
return { events: [], interventions: [] };
|
|
1338
|
+
}
|
|
1339
|
+
let rawText;
|
|
1340
|
+
try {
|
|
1341
|
+
rawText = await readFile3(path, "utf8");
|
|
1342
|
+
} catch {
|
|
1343
|
+
return { events: [], interventions: [] };
|
|
1344
|
+
}
|
|
1345
|
+
await rm2(path, { force: true }).catch(() => {
|
|
1346
|
+
});
|
|
1347
|
+
let raw;
|
|
1348
|
+
try {
|
|
1349
|
+
raw = JSON.parse(rawText);
|
|
1350
|
+
} catch {
|
|
1351
|
+
return { events: [], interventions: [] };
|
|
1352
|
+
}
|
|
1353
|
+
return sanitizeOpportunities(raw, manifest);
|
|
1354
|
+
}
|
|
1355
|
+
function sanitizeOpportunities(raw, manifest) {
|
|
1356
|
+
const out = { events: [], interventions: [] };
|
|
1357
|
+
if (typeof raw !== "object" || raw === null) return out;
|
|
1358
|
+
const doc = raw;
|
|
1359
|
+
const knownEvents = new Set(manifest.events.map((e) => normalizeCode(e.eventType)));
|
|
1360
|
+
const knownInterventions = /* @__PURE__ */ new Set([
|
|
1361
|
+
...manifest.events.flatMap((e) => e.interventions ?? []).map((i) => normalizeCode(i.code)),
|
|
1362
|
+
...(manifest.interventions ?? []).map((i) => normalizeCode(i.code))
|
|
1363
|
+
]);
|
|
1364
|
+
const seenEvents = /* @__PURE__ */ new Set();
|
|
1365
|
+
for (const entry of asArray(doc.events)) {
|
|
1366
|
+
const ev = coerceEvent(entry);
|
|
1367
|
+
if (!ev) continue;
|
|
1368
|
+
if (knownEvents.has(ev.code) || seenEvents.has(ev.code)) continue;
|
|
1369
|
+
seenEvents.add(ev.code);
|
|
1370
|
+
out.events.push(ev);
|
|
1371
|
+
}
|
|
1372
|
+
const seenInterventions = /* @__PURE__ */ new Set();
|
|
1373
|
+
for (const entry of asArray(doc.interventions)) {
|
|
1374
|
+
const iv = coerceIntervention(entry);
|
|
1375
|
+
if (!iv) continue;
|
|
1376
|
+
if (knownInterventions.has(iv.code) || seenInterventions.has(iv.code)) continue;
|
|
1377
|
+
seenInterventions.add(iv.code);
|
|
1378
|
+
out.interventions.push(iv);
|
|
1379
|
+
}
|
|
1380
|
+
return out;
|
|
1381
|
+
}
|
|
1382
|
+
var SUBMIT_TIMEOUT_MS = 3e4;
|
|
1383
|
+
async function submitAdditions(config, session, target9, repoFingerprint2, opportunities) {
|
|
1384
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/universe/additions`, {
|
|
1385
|
+
method: "POST",
|
|
1386
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
1387
|
+
headers: {
|
|
1388
|
+
"Content-Type": "application/json",
|
|
1389
|
+
Authorization: `Bearer ${session.token}`
|
|
1390
|
+
},
|
|
1391
|
+
body: JSON.stringify({
|
|
1392
|
+
target: target9,
|
|
1393
|
+
repoFingerprint: repoFingerprint2,
|
|
1394
|
+
events: opportunities.events,
|
|
1395
|
+
interventions: opportunities.interventions
|
|
1396
|
+
})
|
|
1397
|
+
});
|
|
1398
|
+
if (!res.ok) {
|
|
1399
|
+
const body = await res.text().catch(() => "");
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
`Submitting universe additions failed (${res.status})${body ? `: ${body.slice(0, 300)}` : ""}`
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
return await res.json();
|
|
1405
|
+
}
|
|
1406
|
+
function asArray(value) {
|
|
1407
|
+
return Array.isArray(value) ? value : [];
|
|
1408
|
+
}
|
|
1409
|
+
function asString(value) {
|
|
1410
|
+
if (typeof value !== "string") return void 0;
|
|
1411
|
+
const cleaned = value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
1412
|
+
return cleaned ? cleaned : void 0;
|
|
1413
|
+
}
|
|
1414
|
+
function asConfidence(value) {
|
|
1415
|
+
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
1416
|
+
return Math.min(1, Math.max(0, value));
|
|
1417
|
+
}
|
|
1418
|
+
function asWeight(value) {
|
|
1419
|
+
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
1420
|
+
if (value < 0 || value > 1) return void 0;
|
|
1421
|
+
return value;
|
|
1422
|
+
}
|
|
1423
|
+
function coerceEvent(entry) {
|
|
1424
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
1425
|
+
const e = entry;
|
|
1426
|
+
const code = normalizeCode(asString(e.code) ?? "");
|
|
1427
|
+
if (!code) return null;
|
|
1428
|
+
const properties = [];
|
|
1429
|
+
for (const prop of asArray(e.properties)) {
|
|
1430
|
+
if (typeof prop === "string") {
|
|
1431
|
+
if (prop.trim()) properties.push({ name: prop.trim() });
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if (typeof prop !== "object" || prop === null) continue;
|
|
1435
|
+
const pr = prop;
|
|
1436
|
+
const name = asString(pr.name);
|
|
1437
|
+
if (!name) continue;
|
|
1438
|
+
const desc = asString(pr.description);
|
|
1439
|
+
properties.push({
|
|
1440
|
+
name,
|
|
1441
|
+
...desc ? { description: desc } : {},
|
|
1442
|
+
...pr.required === true ? { required: true } : {}
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
const links = [];
|
|
1446
|
+
for (const link of asArray(e.links)) {
|
|
1447
|
+
if (typeof link !== "object" || link === null) continue;
|
|
1448
|
+
const l = link;
|
|
1449
|
+
const interventionCode = normalizeCode(asString(l.interventionCode) ?? "");
|
|
1450
|
+
if (!interventionCode) continue;
|
|
1451
|
+
const weight = asWeight(l.weight);
|
|
1452
|
+
links.push({ interventionCode, ...weight !== void 0 ? { weight } : {} });
|
|
1453
|
+
}
|
|
1454
|
+
const side = normalizeSide(asString(e.side));
|
|
1455
|
+
return {
|
|
1456
|
+
code,
|
|
1457
|
+
label: asString(e.label),
|
|
1458
|
+
description: asString(e.description),
|
|
1459
|
+
...side ? { side } : {},
|
|
1460
|
+
rationale: asString(e.rationale),
|
|
1461
|
+
confidence: asConfidence(e.confidence),
|
|
1462
|
+
...properties.length ? { properties } : {},
|
|
1463
|
+
...links.length ? { links } : {}
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
function coerceIntervention(entry) {
|
|
1467
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
1468
|
+
const i = entry;
|
|
1469
|
+
const code = normalizeCode(asString(i.code) ?? "");
|
|
1470
|
+
if (!code) return null;
|
|
1471
|
+
const links = [];
|
|
1472
|
+
for (const link of asArray(i.links)) {
|
|
1473
|
+
if (typeof link !== "object" || link === null) continue;
|
|
1474
|
+
const l = link;
|
|
1475
|
+
const eventCode = normalizeCode(asString(l.eventCode) ?? "");
|
|
1476
|
+
if (!eventCode) continue;
|
|
1477
|
+
const weight = asWeight(l.weight);
|
|
1478
|
+
links.push({ eventCode, ...weight !== void 0 ? { weight } : {} });
|
|
1479
|
+
}
|
|
1480
|
+
return {
|
|
1481
|
+
code,
|
|
1482
|
+
label: asString(i.label),
|
|
1483
|
+
description: asString(i.description),
|
|
1484
|
+
rationale: asString(i.rationale),
|
|
1485
|
+
confidence: asConfidence(i.confidence),
|
|
1486
|
+
...links.length ? { links } : {}
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
function normalizeSide(value) {
|
|
1490
|
+
switch (value?.toLowerCase()) {
|
|
1491
|
+
case "frontend":
|
|
1492
|
+
return "frontend";
|
|
1493
|
+
case "backend":
|
|
1494
|
+
return "backend";
|
|
1495
|
+
case "either":
|
|
1496
|
+
return "either";
|
|
1497
|
+
default:
|
|
1498
|
+
return void 0;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1151
1502
|
// src/core/playbooks/shared-prompt.ts
|
|
1152
1503
|
var BASE_WIZARD_PROMPT = `
|
|
1153
1504
|
You are the Whisperr Wizard \u2014 an expert integration agent running INSIDE a
|
|
@@ -1324,6 +1675,46 @@ function renderEventsBrief(m) {
|
|
|
1324
1675
|
}
|
|
1325
1676
|
return lines.join("\n");
|
|
1326
1677
|
}
|
|
1678
|
+
function renderOpportunitiesBrief(m, opportunitiesFile) {
|
|
1679
|
+
const eventCodes = m.events.map((e) => e.eventType);
|
|
1680
|
+
const interventionCodes = [
|
|
1681
|
+
...new Set(m.events.flatMap((e) => e.interventions ?? []).map((i) => i.code))
|
|
1682
|
+
];
|
|
1683
|
+
return [
|
|
1684
|
+
"UNIVERSE OPPORTUNITIES (do this LAST, after your corrections):",
|
|
1685
|
+
"While auditing you read this app's real lifecycle. If you found churn-",
|
|
1686
|
+
"relevant moments the plan does NOT cover \u2014 e.g. a recurring-payment or",
|
|
1687
|
+
"cancellation path with no event, a support/refund flow worth a retention",
|
|
1688
|
+
"play \u2014 record them as PROPOSALS. Do NOT add track() calls for them.",
|
|
1689
|
+
`Write a single JSON file at the repo root named ${opportunitiesFile}:`,
|
|
1690
|
+
"",
|
|
1691
|
+
"{",
|
|
1692
|
+
' "events": [{',
|
|
1693
|
+
' "code": "snake_case_event", "label": "...", "description": "...",',
|
|
1694
|
+
' "side": "frontend|backend|either", "rationale": "what in the code shows this",',
|
|
1695
|
+
' "confidence": 0.0-1.0,',
|
|
1696
|
+
' "properties": [{"name": "...", "description": "...", "required": false}],',
|
|
1697
|
+
' "links": [{"interventionCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
1698
|
+
" }],",
|
|
1699
|
+
' "interventions": [{',
|
|
1700
|
+
' "code": "snake_case_strategy", "label": "...", "description": "...",',
|
|
1701
|
+
' "rationale": "...", "confidence": 0.0-1.0,',
|
|
1702
|
+
' "links": [{"eventCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
1703
|
+
" }]",
|
|
1704
|
+
"}",
|
|
1705
|
+
"",
|
|
1706
|
+
"Rules:",
|
|
1707
|
+
`- The plan already covers these events: ${eventCodes.join(", ") || "(none)"}.`,
|
|
1708
|
+
` And these interventions: ${interventionCodes.join(", ") || "(none)"}.`,
|
|
1709
|
+
" Propose ONLY what is genuinely missing \u2014 never re-propose or rename these.",
|
|
1710
|
+
"- Every proposal needs concrete code evidence in its rationale (file/flow),",
|
|
1711
|
+
" not speculation. 0-3 events and 0-2 interventions is the normal range;",
|
|
1712
|
+
" an empty file or no file at all is a perfectly good outcome.",
|
|
1713
|
+
"- Link each proposed event to the intervention(s) it should feed (existing",
|
|
1714
|
+
" codes or ones you propose in the same file).",
|
|
1715
|
+
"- This file is metadata for the wizard, not app code \u2014 write it and move on."
|
|
1716
|
+
].join("\n");
|
|
1717
|
+
}
|
|
1327
1718
|
function coverageNote(coverage) {
|
|
1328
1719
|
if (!coverage?.length) return "";
|
|
1329
1720
|
const here = coverage.find((c) => c.sameSurface && c.status === "wired");
|
|
@@ -1335,15 +1726,319 @@ function coverageNote(coverage) {
|
|
|
1335
1726
|
return "";
|
|
1336
1727
|
}
|
|
1337
1728
|
|
|
1729
|
+
// src/core/toolPolicy.ts
|
|
1730
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
1731
|
+
var SECRET_MATERIAL_DENIAL = "blocked: secret material - reference the variable name in .env.example instead of reading the real file";
|
|
1732
|
+
var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
|
|
1733
|
+
var BASH_ALLOWLIST_DENIAL = "blocked: command is outside the Bash allowlist - use package install/add commands, mkdir, or git status/diff/log only";
|
|
1734
|
+
var CHAINED_COMMAND_DENIAL = "blocked: chained or substituted shell command - run one command at a time";
|
|
1735
|
+
var ENV_EXAMPLES = /* @__PURE__ */ new Set([".env.example", ".env.sample", ".env.template"]);
|
|
1736
|
+
var SECRET_DIRECTORIES = /* @__PURE__ */ new Set([".aws", ".ssh", ".gnupg"]);
|
|
1737
|
+
var SECRET_EXACT_FILES = /* @__PURE__ */ new Set([".netrc", ".npmrc", ".pypirc"]);
|
|
1738
|
+
var SECRET_SUFFIXES = [
|
|
1739
|
+
".pem",
|
|
1740
|
+
".key",
|
|
1741
|
+
".p12",
|
|
1742
|
+
".pfx",
|
|
1743
|
+
".jks",
|
|
1744
|
+
".keystore",
|
|
1745
|
+
".tfvars"
|
|
1746
|
+
];
|
|
1747
|
+
var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
1748
|
+
var PYTHON_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["pip", "pip3", "poetry", "uv"]);
|
|
1749
|
+
var RUBY_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["gem", "bundle"]);
|
|
1750
|
+
function evaluateToolUse(toolName, input, context) {
|
|
1751
|
+
switch (toolName) {
|
|
1752
|
+
case "Read":
|
|
1753
|
+
return evaluateReadLike(readPathInputs(input), readPathInputs(input), context);
|
|
1754
|
+
case "Grep":
|
|
1755
|
+
return evaluateReadLike(grepGlobPathInputs(input), grepPathInputs(input), context);
|
|
1756
|
+
case "Glob":
|
|
1757
|
+
return evaluateReadLike(grepGlobPathInputs(input), grepGlobPathInputs(input), context);
|
|
1758
|
+
case "Bash":
|
|
1759
|
+
return evaluateBash(input);
|
|
1760
|
+
case "Write":
|
|
1761
|
+
case "Edit":
|
|
1762
|
+
return evaluateWrite(input, context);
|
|
1763
|
+
default:
|
|
1764
|
+
return {
|
|
1765
|
+
behavior: "deny",
|
|
1766
|
+
message: `blocked: tool ${toolName} is not allowed by the wizard tool policy`
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
function isSecretPathLike(value) {
|
|
1771
|
+
const parts = normalizePathLike(value).split("/").filter(Boolean);
|
|
1772
|
+
for (const rawPart of parts) {
|
|
1773
|
+
const part = stripOuterQuotes(rawPart.trim().toLowerCase());
|
|
1774
|
+
if (!part || part === "." || part === "**") continue;
|
|
1775
|
+
if (ENV_EXAMPLES.has(part)) continue;
|
|
1776
|
+
if (SECRET_DIRECTORIES.has(part)) return true;
|
|
1777
|
+
if (SECRET_EXACT_FILES.has(part)) return true;
|
|
1778
|
+
if (part === ".env" || part === ".envrc" || part.startsWith(".env.") || part.startsWith(".env*")) {
|
|
1779
|
+
return true;
|
|
1780
|
+
}
|
|
1781
|
+
if (part.startsWith("id_rsa") || part.startsWith("id_ecdsa") || part.startsWith("id_ed25519")) {
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1784
|
+
if (part.includes("credentials")) return true;
|
|
1785
|
+
if (part.startsWith("secrets.")) return true;
|
|
1786
|
+
if (SECRET_SUFFIXES.some((suffix) => part.endsWith(suffix))) return true;
|
|
1787
|
+
}
|
|
1788
|
+
return false;
|
|
1789
|
+
}
|
|
1790
|
+
function isPathInsideRepo(pathValue, repoPath) {
|
|
1791
|
+
if (pathValue.includes("..")) return false;
|
|
1792
|
+
const repoRoot = resolve(repoPath);
|
|
1793
|
+
const candidate = isAbsolute(pathValue) ? resolve(pathValue) : resolve(repoRoot, pathValue);
|
|
1794
|
+
const rel = relative(repoRoot, candidate);
|
|
1795
|
+
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1796
|
+
}
|
|
1797
|
+
function evaluateReadLike(secretValues, repoPathValues, context) {
|
|
1798
|
+
for (const value of secretValues) {
|
|
1799
|
+
if (isSecretPathLike(value)) {
|
|
1800
|
+
return { behavior: "deny", message: SECRET_MATERIAL_DENIAL };
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
for (const value of repoPathValues) {
|
|
1804
|
+
if (!isPathInsideRepo(value, context.repoPath)) {
|
|
1805
|
+
return {
|
|
1806
|
+
behavior: "deny",
|
|
1807
|
+
message: "blocked: reads must stay inside the target repository"
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return { behavior: "allow" };
|
|
1812
|
+
}
|
|
1813
|
+
function evaluateWrite(input, context) {
|
|
1814
|
+
const pathValue = firstString(input, ["file_path", "path"]);
|
|
1815
|
+
if (!pathValue) {
|
|
1816
|
+
return { behavior: "deny", message: "blocked: write target path is missing" };
|
|
1817
|
+
}
|
|
1818
|
+
if (!isPathInsideRepo(pathValue, context.repoPath)) {
|
|
1819
|
+
return { behavior: "deny", message: WRITE_OUTSIDE_REPO_DENIAL };
|
|
1820
|
+
}
|
|
1821
|
+
return { behavior: "allow" };
|
|
1822
|
+
}
|
|
1823
|
+
function evaluateBash(input) {
|
|
1824
|
+
const command = firstString(input, ["command"]);
|
|
1825
|
+
if (!command) {
|
|
1826
|
+
return { behavior: "deny", message: "blocked: Bash command is missing" };
|
|
1827
|
+
}
|
|
1828
|
+
const split = splitShellSegments(command);
|
|
1829
|
+
if (split.hasCommandSubstitution) {
|
|
1830
|
+
return { behavior: "deny", message: CHAINED_COMMAND_DENIAL };
|
|
1831
|
+
}
|
|
1832
|
+
const segmentDecisions = split.segments.map(evaluateBashSegment);
|
|
1833
|
+
const denied = segmentDecisions.find((decision) => decision.behavior === "deny");
|
|
1834
|
+
if (denied) {
|
|
1835
|
+
return split.hadChain ? { behavior: "deny", message: CHAINED_COMMAND_DENIAL } : denied;
|
|
1836
|
+
}
|
|
1837
|
+
return { behavior: "allow" };
|
|
1838
|
+
}
|
|
1839
|
+
function evaluateBashSegment(segment) {
|
|
1840
|
+
const tokens = tokenizeShellSegment(segment);
|
|
1841
|
+
if (!tokens.length) return { behavior: "deny", message: BASH_ALLOWLIST_DENIAL };
|
|
1842
|
+
const command = tokens[0]?.toLowerCase() ?? "";
|
|
1843
|
+
const second = tokens[1]?.toLowerCase() ?? "";
|
|
1844
|
+
const third = tokens[2]?.toLowerCase() ?? "";
|
|
1845
|
+
if (JS_PACKAGE_MANAGERS.has(command)) {
|
|
1846
|
+
if (second === "install" || second === "add" || second === "ci") {
|
|
1847
|
+
return { behavior: "allow" };
|
|
1848
|
+
}
|
|
1849
|
+
if (second === "pkg" && (third === "get" || third === "set")) {
|
|
1850
|
+
return { behavior: "allow" };
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (PYTHON_PACKAGE_MANAGERS.has(command)) {
|
|
1854
|
+
if (second === "install" || second === "add") {
|
|
1855
|
+
return { behavior: "allow" };
|
|
1856
|
+
}
|
|
1857
|
+
if (command === "uv" && second === "pip" && third === "install") {
|
|
1858
|
+
return { behavior: "allow" };
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (command === "composer" && (second === "install" || second === "require")) {
|
|
1862
|
+
return { behavior: "allow" };
|
|
1863
|
+
}
|
|
1864
|
+
if (command === "pod" && second === "install") {
|
|
1865
|
+
return { behavior: "allow" };
|
|
1866
|
+
}
|
|
1867
|
+
if ((command === "flutter" || command === "dart") && second === "pub") {
|
|
1868
|
+
return { behavior: "allow" };
|
|
1869
|
+
}
|
|
1870
|
+
if (RUBY_PACKAGE_MANAGERS.has(command) && (second === "install" || second === "add")) {
|
|
1871
|
+
return { behavior: "allow" };
|
|
1872
|
+
}
|
|
1873
|
+
if (command === "go" && (second === "get" || second === "mod")) {
|
|
1874
|
+
return { behavior: "allow" };
|
|
1875
|
+
}
|
|
1876
|
+
if (command === "mkdir") {
|
|
1877
|
+
return { behavior: "allow" };
|
|
1878
|
+
}
|
|
1879
|
+
if (command === "git" && (second === "status" || second === "diff" || second === "log")) {
|
|
1880
|
+
if (tokens.slice(2).some((token) => isSecretPathLike(token))) {
|
|
1881
|
+
return { behavior: "deny", message: SECRET_MATERIAL_DENIAL };
|
|
1882
|
+
}
|
|
1883
|
+
return { behavior: "allow" };
|
|
1884
|
+
}
|
|
1885
|
+
return { behavior: "deny", message: BASH_ALLOWLIST_DENIAL };
|
|
1886
|
+
}
|
|
1887
|
+
function readPathInputs(input) {
|
|
1888
|
+
return stringInputs(input, ["file_path", "path"]);
|
|
1889
|
+
}
|
|
1890
|
+
function grepGlobPathInputs(input) {
|
|
1891
|
+
return stringInputs(input, ["file_path", "path", "pattern", "glob"]);
|
|
1892
|
+
}
|
|
1893
|
+
function grepPathInputs(input) {
|
|
1894
|
+
return stringInputs(input, ["file_path", "path"]);
|
|
1895
|
+
}
|
|
1896
|
+
function stringInputs(input, keys) {
|
|
1897
|
+
const values = [];
|
|
1898
|
+
for (const key of keys) {
|
|
1899
|
+
const value = input[key];
|
|
1900
|
+
if (typeof value === "string" && value.trim()) values.push(value.trim());
|
|
1901
|
+
}
|
|
1902
|
+
return values;
|
|
1903
|
+
}
|
|
1904
|
+
function firstString(input, keys) {
|
|
1905
|
+
return stringInputs(input, keys)[0];
|
|
1906
|
+
}
|
|
1907
|
+
function normalizePathLike(value) {
|
|
1908
|
+
return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
|
|
1909
|
+
}
|
|
1910
|
+
function stripOuterQuotes(value) {
|
|
1911
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1912
|
+
return value.slice(1, -1);
|
|
1913
|
+
}
|
|
1914
|
+
return value;
|
|
1915
|
+
}
|
|
1916
|
+
function splitShellSegments(command) {
|
|
1917
|
+
const segments = [];
|
|
1918
|
+
let current = "";
|
|
1919
|
+
let quote;
|
|
1920
|
+
let escaped = false;
|
|
1921
|
+
let hadChain = false;
|
|
1922
|
+
let hasCommandSubstitution = false;
|
|
1923
|
+
const push = () => {
|
|
1924
|
+
const segment = current.trim();
|
|
1925
|
+
if (segment) segments.push(segment);
|
|
1926
|
+
current = "";
|
|
1927
|
+
};
|
|
1928
|
+
for (let i = 0; i < command.length; i++) {
|
|
1929
|
+
const ch = command[i];
|
|
1930
|
+
const next = command[i + 1];
|
|
1931
|
+
if (ch === "`" || ch === "$" && next === "(") {
|
|
1932
|
+
hasCommandSubstitution = true;
|
|
1933
|
+
}
|
|
1934
|
+
if (escaped) {
|
|
1935
|
+
current += ch;
|
|
1936
|
+
escaped = false;
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
if (ch === "\\" && quote !== "'") {
|
|
1940
|
+
current += ch;
|
|
1941
|
+
escaped = true;
|
|
1942
|
+
continue;
|
|
1943
|
+
}
|
|
1944
|
+
if (quote) {
|
|
1945
|
+
if (ch === quote) quote = void 0;
|
|
1946
|
+
current += ch;
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
if (ch === '"' || ch === "'") {
|
|
1950
|
+
quote = ch;
|
|
1951
|
+
current += ch;
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
const two = `${ch}${next ?? ""}`;
|
|
1955
|
+
if (two === "&&" || two === "||") {
|
|
1956
|
+
hadChain = true;
|
|
1957
|
+
push();
|
|
1958
|
+
i++;
|
|
1959
|
+
continue;
|
|
1960
|
+
}
|
|
1961
|
+
if (ch === ";" || ch === "|" || ch === "\n") {
|
|
1962
|
+
hadChain = true;
|
|
1963
|
+
push();
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
current += ch;
|
|
1967
|
+
}
|
|
1968
|
+
push();
|
|
1969
|
+
return { segments, hadChain, hasCommandSubstitution };
|
|
1970
|
+
}
|
|
1971
|
+
function tokenizeShellSegment(segment) {
|
|
1972
|
+
const tokens = [];
|
|
1973
|
+
let current = "";
|
|
1974
|
+
let quote;
|
|
1975
|
+
let escaped = false;
|
|
1976
|
+
const push = () => {
|
|
1977
|
+
if (current) tokens.push(current);
|
|
1978
|
+
current = "";
|
|
1979
|
+
};
|
|
1980
|
+
for (let i = 0; i < segment.length; i++) {
|
|
1981
|
+
const ch = segment[i];
|
|
1982
|
+
if (escaped) {
|
|
1983
|
+
current += ch;
|
|
1984
|
+
escaped = false;
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
if (ch === "\\" && quote !== "'") {
|
|
1988
|
+
escaped = true;
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
if (quote) {
|
|
1992
|
+
if (ch === quote) quote = void 0;
|
|
1993
|
+
else current += ch;
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
if (ch === '"' || ch === "'") {
|
|
1997
|
+
quote = ch;
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
if (/\s/.test(ch)) {
|
|
2001
|
+
push();
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
current += ch;
|
|
2005
|
+
}
|
|
2006
|
+
push();
|
|
2007
|
+
return tokens;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1338
2010
|
// src/core/agent.ts
|
|
1339
2011
|
async function runIntegrationAgent(opts) {
|
|
1340
|
-
const { repoPath, config, session, playbook, manifest, progress } = opts;
|
|
2012
|
+
const { repoPath, config, session, playbook, manifest, progress, onPlanReady } = opts;
|
|
1341
2013
|
applyModelAuthEnv(config, session);
|
|
1342
2014
|
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
1343
2015
|
const started = Date.now();
|
|
1344
2016
|
let costUsd = 0;
|
|
1345
2017
|
const summaries = [];
|
|
1346
|
-
|
|
2018
|
+
let repoMap = "";
|
|
2019
|
+
let eventOutcomes = [];
|
|
2020
|
+
const phaseTimings = [];
|
|
2021
|
+
const BUDGET_FLOOR = 0.25;
|
|
2022
|
+
if (config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2023
|
+
try {
|
|
2024
|
+
const map = await runTimedPass("Mapping your codebase", {
|
|
2025
|
+
prompt: renderRepoMapPrompt(repoPath),
|
|
2026
|
+
systemPrompt: systemPrompt9,
|
|
2027
|
+
repoPath,
|
|
2028
|
+
model: config.plannerModel,
|
|
2029
|
+
effort: "high",
|
|
2030
|
+
maxTurns: 40,
|
|
2031
|
+
budgetUsd: Math.min(config.budgetUsd - costUsd, config.budgetUsd * 0.15),
|
|
2032
|
+
allowedTools: READ_ONLY_TOOLS,
|
|
2033
|
+
progress
|
|
2034
|
+
}, phaseTimings);
|
|
2035
|
+
costUsd += map.costUsd;
|
|
2036
|
+
repoMap = sliceRepoMap(map.summary);
|
|
2037
|
+
} catch (err) {
|
|
2038
|
+
debugNote(`repo map pass failed: ${err.message}`);
|
|
2039
|
+
repoMap = "";
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1347
2042
|
const corePrompt = [
|
|
1348
2043
|
`Integrate the Whisperr ${playbook.target.displayName} SDK \u2014 CORE SETUP ONLY.`,
|
|
1349
2044
|
`Project root: ${repoPath}`,
|
|
@@ -1368,11 +2063,12 @@ async function runIntegrationAgent(opts) {
|
|
|
1368
2063
|
"Do NOT instrument product events yet \u2014 that is a separate step. Do not run",
|
|
1369
2064
|
"the analyzer or build; the human will review the diff.",
|
|
1370
2065
|
"",
|
|
2066
|
+
...renderRepoMapSection(repoMap),
|
|
1371
2067
|
"----- IDENTIFY -----",
|
|
1372
2068
|
renderIdentifyBrief(manifest),
|
|
1373
2069
|
"----- END -----"
|
|
1374
2070
|
].join("\n");
|
|
1375
|
-
const core = await
|
|
2071
|
+
const core = await runTimedPass("Installing the SDK & wiring identify()", {
|
|
1376
2072
|
prompt: corePrompt,
|
|
1377
2073
|
systemPrompt: systemPrompt9,
|
|
1378
2074
|
repoPath,
|
|
@@ -1381,58 +2077,111 @@ async function runIntegrationAgent(opts) {
|
|
|
1381
2077
|
maxTurns: 50,
|
|
1382
2078
|
budgetUsd: config.budgetUsd - costUsd,
|
|
1383
2079
|
progress
|
|
1384
|
-
});
|
|
2080
|
+
}, phaseTimings);
|
|
1385
2081
|
costUsd += core.costUsd;
|
|
1386
2082
|
if (core.summary) summaries.push(`Core setup:
|
|
1387
2083
|
${core.summary}`);
|
|
1388
|
-
const BUDGET_FLOOR = 0.25;
|
|
1389
2084
|
let eventsComplete = true;
|
|
1390
2085
|
if (manifest.events.length > 0 && config.budgetUsd - costUsd <= BUDGET_FLOOR) {
|
|
1391
2086
|
eventsComplete = false;
|
|
2087
|
+
eventOutcomes = manifest.events.map((event) => ({
|
|
2088
|
+
event: event.eventType,
|
|
2089
|
+
outcome: "skipped",
|
|
2090
|
+
reason: "Spend limit reached during core setup."
|
|
2091
|
+
}));
|
|
1392
2092
|
summaries.push(
|
|
1393
2093
|
"Events: skipped \u2014 the spend limit was reached during core setup. Re-run with a higher WHISPERR_WIZARD_BUDGET_USD to instrument events."
|
|
1394
2094
|
);
|
|
1395
2095
|
} else if (manifest.events.length > 0) {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
`The Whisperr ${playbook.target.displayName} SDK is already installed and`,
|
|
1399
|
-
"initialized, and identify() is wired. Now instrument the product events",
|
|
1400
|
-
"below with track().",
|
|
1401
|
-
"",
|
|
1402
|
-
"Work in importance order. Place each event at the real call site where the",
|
|
1403
|
-
"END USER performs the action (not an admin/staff path \u2014 see WHO COUNTS AS",
|
|
1404
|
-
`"THE USER"), and attach the properties you can source from what's in scope`,
|
|
1405
|
-
"there (see the property-capture rule). These events describe the same",
|
|
1406
|
-
"person identify() did, so they live on the same end-user surface. Skip fast",
|
|
1407
|
-
"when an event has no clear trigger here \u2014 spend steps placing, not exploring.",
|
|
1408
|
-
"Stop once the events that clearly exist in this app are wired.",
|
|
1409
|
-
"",
|
|
1410
|
-
"----- EVENTS -----",
|
|
1411
|
-
renderEventsBrief(manifest),
|
|
1412
|
-
"----- END -----"
|
|
1413
|
-
].join("\n");
|
|
1414
|
-
const events = await runPass({
|
|
1415
|
-
prompt: eventsPrompt,
|
|
2096
|
+
const planPass = await runTimedPass("Planning event placements", {
|
|
2097
|
+
prompt: renderEventPlanPrompt(repoPath, playbook, manifest, repoMap),
|
|
1416
2098
|
systemPrompt: systemPrompt9,
|
|
1417
2099
|
repoPath,
|
|
1418
|
-
model: config.
|
|
1419
|
-
effort:
|
|
1420
|
-
maxTurns:
|
|
2100
|
+
model: config.plannerModel,
|
|
2101
|
+
effort: "high",
|
|
2102
|
+
maxTurns: 40,
|
|
1421
2103
|
budgetUsd: config.budgetUsd - costUsd,
|
|
2104
|
+
allowedTools: READ_ONLY_TOOLS,
|
|
1422
2105
|
progress
|
|
1423
|
-
});
|
|
1424
|
-
costUsd +=
|
|
1425
|
-
|
|
1426
|
-
if (
|
|
1427
|
-
|
|
2106
|
+
}, phaseTimings);
|
|
2107
|
+
costUsd += planPass.costUsd;
|
|
2108
|
+
const plan = parseEventPlan(planPass.summary);
|
|
2109
|
+
if (!plan) {
|
|
2110
|
+
debugNote("event plan parse failed; falling back to direct event wiring");
|
|
2111
|
+
progress?.onActivity?.("Event plan was not parseable; falling back to direct wiring");
|
|
2112
|
+
const direct = await runDirectEventsPass({
|
|
2113
|
+
repoPath,
|
|
2114
|
+
config,
|
|
2115
|
+
systemPrompt: systemPrompt9,
|
|
2116
|
+
playbook,
|
|
2117
|
+
manifest,
|
|
2118
|
+
repoMap,
|
|
2119
|
+
budgetUsd: config.budgetUsd - costUsd,
|
|
2120
|
+
progress,
|
|
2121
|
+
phaseTimings
|
|
2122
|
+
});
|
|
2123
|
+
costUsd += direct.pass.costUsd;
|
|
2124
|
+
eventsComplete = !direct.pass.maxedOut;
|
|
2125
|
+
eventOutcomes = direct.eventOutcomes;
|
|
2126
|
+
if (direct.pass.summary) summaries.push(`Events:
|
|
2127
|
+
${direct.pass.summary}`);
|
|
2128
|
+
} else {
|
|
2129
|
+
let scopedPlan = planForManifest(plan, manifest);
|
|
2130
|
+
const reviewedPlan = await onPlanReady?.(scopedPlan);
|
|
2131
|
+
if (reviewedPlan !== void 0 && reviewedPlan !== null) {
|
|
2132
|
+
scopedPlan = planForManifest(reviewedPlan, manifest);
|
|
2133
|
+
}
|
|
2134
|
+
const placeEntries = scopedPlan.filter((entry) => entry.decision === "place");
|
|
2135
|
+
const unsureEntries = scopedPlan.filter((entry) => entry.decision === "unsure");
|
|
2136
|
+
let wire;
|
|
2137
|
+
if (placeEntries.length && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2138
|
+
wire = await runTimedPass("Instrumenting planned events", {
|
|
2139
|
+
prompt: renderEventWirePrompt(repoPath, playbook, repoMap, placeEntries),
|
|
2140
|
+
systemPrompt: systemPrompt9,
|
|
2141
|
+
repoPath,
|
|
2142
|
+
model: config.model,
|
|
2143
|
+
effort: "high",
|
|
2144
|
+
maxTurns: config.maxTurns,
|
|
2145
|
+
budgetUsd: config.budgetUsd - costUsd,
|
|
2146
|
+
progress
|
|
2147
|
+
}, phaseTimings);
|
|
2148
|
+
costUsd += wire.costUsd;
|
|
2149
|
+
if (wire.summary) summaries.push(`Events:
|
|
2150
|
+
${wire.summary}`);
|
|
2151
|
+
} else if (!placeEntries.length) {
|
|
2152
|
+
summaries.push("Events: no events had a confident placement in this surface.");
|
|
2153
|
+
}
|
|
2154
|
+
let wired = await scanManifestEvents(repoPath, manifest);
|
|
2155
|
+
const missedPlaceEntries = placeEntries.filter((entry) => !wired.has(entry.event));
|
|
2156
|
+
const followUpEntries = uniquePlanEntries([...missedPlaceEntries, ...unsureEntries]);
|
|
2157
|
+
let followUp;
|
|
2158
|
+
if (followUpEntries.length && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2159
|
+
followUp = await runTimedPass("Reconciling missed events", {
|
|
2160
|
+
prompt: renderEventReconcilePrompt(repoPath, playbook, repoMap, followUpEntries),
|
|
2161
|
+
systemPrompt: systemPrompt9,
|
|
2162
|
+
repoPath,
|
|
2163
|
+
model: config.plannerModel,
|
|
2164
|
+
effort: "medium",
|
|
2165
|
+
maxTurns: 15,
|
|
2166
|
+
budgetUsd: config.budgetUsd - costUsd,
|
|
2167
|
+
progress
|
|
2168
|
+
}, phaseTimings);
|
|
2169
|
+
costUsd += followUp.costUsd;
|
|
2170
|
+
if (followUp.summary) summaries.push(`Event reconciliation:
|
|
2171
|
+
${followUp.summary}`);
|
|
2172
|
+
wired = await scanManifestEvents(repoPath, manifest);
|
|
2173
|
+
}
|
|
2174
|
+
eventOutcomes = buildEventOutcomes(manifest, scopedPlan, wired);
|
|
2175
|
+
eventsComplete = !planPass.maxedOut && !(wire?.maxedOut ?? false) && !(followUp?.maxedOut ?? false);
|
|
2176
|
+
}
|
|
1428
2177
|
}
|
|
1429
2178
|
if (core.ok && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
1430
|
-
progress?.onPhase?.("Reviewing & correcting placements");
|
|
1431
2179
|
const reviewPrompt = [
|
|
1432
2180
|
`You wired the Whisperr ${playbook.target.displayName} SDK into this repo.`,
|
|
1433
2181
|
"Now AUDIT YOUR OWN WORK and fix mistakes before finishing.",
|
|
1434
2182
|
`Project root: ${repoPath}`,
|
|
1435
2183
|
"",
|
|
2184
|
+
...renderRepoMapSection(repoMap),
|
|
1436
2185
|
"Run `git diff` and read the surrounding code to see exactly what you added.",
|
|
1437
2186
|
"For every identify() and track() call, verify \u2014 and FIX in place:",
|
|
1438
2187
|
"1. Subject \u2014 keys to the END USER, never an admin/staff/operator/seller.",
|
|
@@ -1448,41 +2197,469 @@ ${events.summary}`);
|
|
|
1448
2197
|
"4. Coverage \u2014 every gateway/callback path that should emit an event does;",
|
|
1449
2198
|
" no recurring or secondary path left as a dead zone.",
|
|
1450
2199
|
"Change ONLY what is genuinely wrong or incomplete \u2014 do not churn correct",
|
|
1451
|
-
"calls or re-explore the whole repo. Be surgical.
|
|
1452
|
-
"
|
|
2200
|
+
"calls or re-explore the whole repo. Be surgical.",
|
|
2201
|
+
"",
|
|
2202
|
+
renderOpportunitiesBrief(manifest, OPPORTUNITIES_FILE),
|
|
2203
|
+
"",
|
|
2204
|
+
"End with a one-line, plain-text note of what you corrected (or 'No",
|
|
2205
|
+
"corrections needed.'), plus one line on what you proposed, if anything."
|
|
1453
2206
|
].join("\n");
|
|
1454
|
-
const review = await
|
|
2207
|
+
const review = await runTimedPass("Reviewing & correcting placements", {
|
|
1455
2208
|
prompt: reviewPrompt,
|
|
1456
2209
|
systemPrompt: systemPrompt9,
|
|
1457
2210
|
repoPath,
|
|
1458
|
-
model: config.
|
|
1459
|
-
effort:
|
|
2211
|
+
model: config.plannerModel,
|
|
2212
|
+
effort: "medium",
|
|
1460
2213
|
maxTurns: 40,
|
|
1461
2214
|
budgetUsd: config.budgetUsd - costUsd,
|
|
1462
2215
|
progress
|
|
1463
|
-
});
|
|
2216
|
+
}, phaseTimings);
|
|
1464
2217
|
costUsd += review.costUsd;
|
|
1465
2218
|
if (review.summary) summaries.push(`Review:
|
|
1466
2219
|
${review.summary}`);
|
|
2220
|
+
if (manifest.events.length) {
|
|
2221
|
+
eventOutcomes = refreshWiredOutcomes(
|
|
2222
|
+
eventOutcomes,
|
|
2223
|
+
manifest,
|
|
2224
|
+
await scanManifestEvents(repoPath, manifest)
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
1467
2227
|
}
|
|
1468
2228
|
return {
|
|
1469
|
-
summary: summaries.join("\n\n"),
|
|
1470
|
-
costUsd,
|
|
1471
|
-
durationMs: Date.now() - started,
|
|
1472
|
-
|
|
1473
|
-
|
|
2229
|
+
summary: summaries.join("\n\n"),
|
|
2230
|
+
costUsd,
|
|
2231
|
+
durationMs: Date.now() - started,
|
|
2232
|
+
phaseTimings,
|
|
2233
|
+
coreOk: core.ok,
|
|
2234
|
+
eventsComplete,
|
|
2235
|
+
eventOutcomes,
|
|
2236
|
+
repoMap: repoMap || void 0
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
async function runAdditionsInstrumentationPass(opts) {
|
|
2240
|
+
const { repoPath, config, session, playbook, acceptedEvents, budgetUsd, repoMap, progress } = opts;
|
|
2241
|
+
if (!acceptedEvents.length || budgetUsd <= 0) {
|
|
2242
|
+
return { summary: "", costUsd: 0, ran: false };
|
|
2243
|
+
}
|
|
2244
|
+
applyModelAuthEnv(config, session);
|
|
2245
|
+
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
2246
|
+
const eventLines = [];
|
|
2247
|
+
for (const e of acceptedEvents) {
|
|
2248
|
+
eventLines.push("");
|
|
2249
|
+
eventLines.push(`\u25A0 ${e.code}${e.label ? ` (${e.label})` : ""}`);
|
|
2250
|
+
if (e.description) eventLines.push(` ${e.description}`);
|
|
2251
|
+
if (e.rationale) eventLines.push(` where: ${e.rationale}`);
|
|
2252
|
+
if (e.properties?.length) {
|
|
2253
|
+
eventLines.push(" properties to capture if available:");
|
|
2254
|
+
for (const prop of e.properties) {
|
|
2255
|
+
eventLines.push(` \xB7 ${prop.name}${prop.required ? " (required)" : ""}${prop.description ? ` \u2014 ${prop.description}` : ""}`);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
progress?.onPhase?.("Instrumenting the newly added events");
|
|
2260
|
+
const prompt = [
|
|
2261
|
+
"You proposed these events as universe opportunities while reviewing this",
|
|
2262
|
+
"repo, and the user just APPROVED adding them to the plan. Instrument them",
|
|
2263
|
+
"now with track(), exactly like the plan's other events: place each at the",
|
|
2264
|
+
"real call site (your own rationale tells you where you saw it), attach the",
|
|
2265
|
+
"properties available in scope, and skip anything that turns out not to have",
|
|
2266
|
+
"a clear trigger after all. The SDK is already installed and initialized.",
|
|
2267
|
+
`Project root: ${repoPath}`,
|
|
2268
|
+
"",
|
|
2269
|
+
...renderRepoMapSection(repoMap),
|
|
2270
|
+
"----- APPROVED NEW EVENTS -----",
|
|
2271
|
+
...eventLines,
|
|
2272
|
+
"",
|
|
2273
|
+
"----- END -----"
|
|
2274
|
+
].join("\n");
|
|
2275
|
+
const pass = await runPass({
|
|
2276
|
+
prompt,
|
|
2277
|
+
systemPrompt: systemPrompt9,
|
|
2278
|
+
repoPath,
|
|
2279
|
+
model: config.model,
|
|
2280
|
+
effort: config.effort,
|
|
2281
|
+
maxTurns: 25,
|
|
2282
|
+
budgetUsd,
|
|
2283
|
+
progress
|
|
2284
|
+
});
|
|
2285
|
+
return { summary: pass.summary, costUsd: pass.costUsd, ran: true };
|
|
2286
|
+
}
|
|
2287
|
+
async function runRepairPass(opts) {
|
|
2288
|
+
const {
|
|
2289
|
+
repoPath,
|
|
2290
|
+
config,
|
|
2291
|
+
session,
|
|
2292
|
+
playbook,
|
|
2293
|
+
verifyCommand,
|
|
2294
|
+
verifyOutput,
|
|
2295
|
+
budgetUsd,
|
|
2296
|
+
progress
|
|
2297
|
+
} = opts;
|
|
2298
|
+
if (budgetUsd <= 0) return { summary: "", costUsd: 0, ran: false };
|
|
2299
|
+
applyModelAuthEnv(config, session);
|
|
2300
|
+
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
2301
|
+
progress?.onPhase?.("Repairing verifier failures");
|
|
2302
|
+
const prompt = [
|
|
2303
|
+
"The Whisperr integration was applied, but the verifier command failed.",
|
|
2304
|
+
`Project root: ${repoPath}`,
|
|
2305
|
+
"",
|
|
2306
|
+
"Verifier command:",
|
|
2307
|
+
verifyCommand,
|
|
2308
|
+
"",
|
|
2309
|
+
"Verifier output tail:",
|
|
2310
|
+
tail(verifyOutput, 4e3) || "(no output)",
|
|
2311
|
+
"",
|
|
2312
|
+
"Fix ONLY what the verifier reports; do not refactor. Keep the existing",
|
|
2313
|
+
"Whisperr placement intent unless a verifier error directly requires a small",
|
|
2314
|
+
"syntax/import/type correction. Do not explore unrelated code."
|
|
2315
|
+
].join("\n");
|
|
2316
|
+
const pass = await runPass({
|
|
2317
|
+
prompt,
|
|
2318
|
+
systemPrompt: systemPrompt9,
|
|
2319
|
+
repoPath,
|
|
2320
|
+
model: config.model,
|
|
2321
|
+
effort: "high",
|
|
2322
|
+
maxTurns: 20,
|
|
2323
|
+
budgetUsd,
|
|
2324
|
+
progress
|
|
2325
|
+
});
|
|
2326
|
+
return { summary: pass.summary, costUsd: pass.costUsd, ran: true };
|
|
2327
|
+
}
|
|
2328
|
+
var READ_ONLY_TOOLS = ["Read", "Glob", "Grep"];
|
|
2329
|
+
var FULL_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
2330
|
+
function parseEventPlan(text) {
|
|
2331
|
+
const parsed = parseFirstJsonArray(text);
|
|
2332
|
+
if (!Array.isArray(parsed)) return null;
|
|
2333
|
+
const entries = [];
|
|
2334
|
+
for (const item of parsed) {
|
|
2335
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2336
|
+
const obj = item;
|
|
2337
|
+
const event = typeof obj.event === "string" ? obj.event.trim() : "";
|
|
2338
|
+
const decision = obj.decision;
|
|
2339
|
+
if (!event || decision !== "place" && decision !== "skip" && decision !== "unsure") {
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
const entry = {
|
|
2343
|
+
event,
|
|
2344
|
+
decision,
|
|
2345
|
+
reason: typeof obj.reason === "string" ? obj.reason.trim() : ""
|
|
2346
|
+
};
|
|
2347
|
+
if (typeof obj.file === "string" && obj.file.trim()) {
|
|
2348
|
+
entry.file = obj.file.trim();
|
|
2349
|
+
}
|
|
2350
|
+
if (typeof obj.anchor === "string" && obj.anchor.trim()) {
|
|
2351
|
+
entry.anchor = obj.anchor.trim();
|
|
2352
|
+
}
|
|
2353
|
+
if (Array.isArray(obj.properties)) {
|
|
2354
|
+
entry.properties = obj.properties.filter((prop) => typeof prop === "string" && !!prop.trim()).map((prop) => prop.trim());
|
|
2355
|
+
}
|
|
2356
|
+
entries.push(entry);
|
|
2357
|
+
}
|
|
2358
|
+
return entries;
|
|
2359
|
+
}
|
|
2360
|
+
function parseFirstJsonArray(text) {
|
|
2361
|
+
const start = text.indexOf("[");
|
|
2362
|
+
if (start < 0) return null;
|
|
2363
|
+
const json = sliceJsonArray(text, start);
|
|
2364
|
+
if (!json) return null;
|
|
2365
|
+
try {
|
|
2366
|
+
return JSON.parse(json);
|
|
2367
|
+
} catch {
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
function sliceJsonArray(text, start) {
|
|
2372
|
+
let depth = 0;
|
|
2373
|
+
let inString = false;
|
|
2374
|
+
let escaped = false;
|
|
2375
|
+
for (let i = start; i < text.length; i++) {
|
|
2376
|
+
const ch = text[i];
|
|
2377
|
+
if (inString) {
|
|
2378
|
+
if (escaped) {
|
|
2379
|
+
escaped = false;
|
|
2380
|
+
} else if (ch === "\\") {
|
|
2381
|
+
escaped = true;
|
|
2382
|
+
} else if (ch === '"') {
|
|
2383
|
+
inString = false;
|
|
2384
|
+
}
|
|
2385
|
+
continue;
|
|
2386
|
+
}
|
|
2387
|
+
if (ch === '"') {
|
|
2388
|
+
inString = true;
|
|
2389
|
+
} else if (ch === "[") {
|
|
2390
|
+
depth++;
|
|
2391
|
+
} else if (ch === "]") {
|
|
2392
|
+
depth--;
|
|
2393
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
return null;
|
|
2397
|
+
}
|
|
2398
|
+
function renderRepoMapPrompt(repoPath) {
|
|
2399
|
+
return [
|
|
2400
|
+
"Map this repository for a Whisperr SDK integration. This is READ-ONLY.",
|
|
2401
|
+
`Project root: ${repoPath}`,
|
|
2402
|
+
"",
|
|
2403
|
+
"Produce a concise structured repo map as final text with these exact sections:",
|
|
2404
|
+
"- framework + entry points",
|
|
2405
|
+
"- auth flows enumerated: END-USER vs admin/staff, with precise file paths",
|
|
2406
|
+
"- billing/payment/webhook surfaces",
|
|
2407
|
+
"- analytics/tracking wrapper if any",
|
|
2408
|
+
"- state management",
|
|
2409
|
+
"- directory guide: which dirs hold pages/controllers/services",
|
|
2410
|
+
"",
|
|
2411
|
+
"This map guides event placement. List file paths precisely. No prose beyond the map."
|
|
2412
|
+
].join("\n");
|
|
2413
|
+
}
|
|
2414
|
+
function sliceRepoMap(map) {
|
|
2415
|
+
return map.trim().slice(0, 8e3);
|
|
2416
|
+
}
|
|
2417
|
+
function renderRepoMapSection(repoMap) {
|
|
2418
|
+
const map = repoMap?.trim();
|
|
2419
|
+
if (!map) return [];
|
|
2420
|
+
return ["----- REPO MAP -----", map, "----- END REPO MAP -----", ""];
|
|
2421
|
+
}
|
|
2422
|
+
function renderEventPlanPrompt(repoPath, playbook, manifest, repoMap) {
|
|
2423
|
+
return [
|
|
2424
|
+
`Plan Whisperr ${playbook.target.displayName} event placements. This is READ-ONLY.`,
|
|
2425
|
+
`Project root: ${repoPath}`,
|
|
2426
|
+
"",
|
|
2427
|
+
...renderRepoMapSection(repoMap),
|
|
2428
|
+
"Use the repo map and the events brief to decide where each event belongs.",
|
|
2429
|
+
"Return ONLY a JSON code block containing an array with this shape:",
|
|
2430
|
+
'[{"event":"event_name","decision":"place|skip|unsure","file":"path","anchor":"function/route/handler","properties":["prop"],"reason":"short reason"}]',
|
|
2431
|
+
"",
|
|
2432
|
+
"Rules:",
|
|
2433
|
+
"- decision=place only when there is a concrete end-user call site in this repo.",
|
|
2434
|
+
"- decision=skip when the event clearly lives on another surface or does not exist here.",
|
|
2435
|
+
"- decision=unsure when there is a plausible surface but the exact anchor is not proven.",
|
|
2436
|
+
"- For place decisions, include file and anchor precisely.",
|
|
2437
|
+
"- properties should list only properties likely available at that anchor.",
|
|
2438
|
+
"- No prose beyond the JSON code block.",
|
|
2439
|
+
"",
|
|
2440
|
+
"----- EVENTS -----",
|
|
2441
|
+
renderEventsBrief(manifest),
|
|
2442
|
+
"----- END EVENTS -----"
|
|
2443
|
+
].join("\n");
|
|
2444
|
+
}
|
|
2445
|
+
function renderEventWirePrompt(repoPath, playbook, repoMap, entries) {
|
|
2446
|
+
return [
|
|
2447
|
+
`The Whisperr ${playbook.target.displayName} SDK is installed and initialized.`,
|
|
2448
|
+
"Wire only the planned event placements below with track().",
|
|
2449
|
+
`Project root: ${repoPath}`,
|
|
2450
|
+
"",
|
|
2451
|
+
...renderRepoMapSection(repoMap),
|
|
2452
|
+
"For each: open the file, find the anchor, add track() with the listed",
|
|
2453
|
+
"properties. If an anchor is genuinely absent, mark it unsure in your",
|
|
2454
|
+
"summary and move on. Do not re-plan skipped or unsure events in this pass.",
|
|
2455
|
+
"",
|
|
2456
|
+
"----- EVENT WIRING PLAN -----",
|
|
2457
|
+
...renderPlanEntryLines(entries),
|
|
2458
|
+
"----- END EVENT WIRING PLAN -----"
|
|
2459
|
+
].join("\n");
|
|
2460
|
+
}
|
|
2461
|
+
function renderEventReconcilePrompt(repoPath, playbook, repoMap, entries) {
|
|
2462
|
+
return [
|
|
2463
|
+
`One targeted follow-up for Whisperr ${playbook.target.displayName} events.`,
|
|
2464
|
+
`Project root: ${repoPath}`,
|
|
2465
|
+
"",
|
|
2466
|
+
...renderRepoMapSection(repoMap),
|
|
2467
|
+
"The events below were either planned for placement but not detected in the",
|
|
2468
|
+
"changed files, or were marked unsure. For each one, make one focused attempt",
|
|
2469
|
+
"at the listed file/anchor. If the anchor is absent or still not clear, skip",
|
|
2470
|
+
"it and say why in the summary. Do not search broadly or refactor.",
|
|
2471
|
+
"",
|
|
2472
|
+
"----- TARGET EVENTS -----",
|
|
2473
|
+
...renderPlanEntryLines(entries),
|
|
2474
|
+
"----- END TARGET EVENTS -----"
|
|
2475
|
+
].join("\n");
|
|
2476
|
+
}
|
|
2477
|
+
function renderPlanEntryLines(entries) {
|
|
2478
|
+
const lines = [];
|
|
2479
|
+
for (const entry of entries) {
|
|
2480
|
+
lines.push("");
|
|
2481
|
+
lines.push(`- event: ${entry.event}`);
|
|
2482
|
+
lines.push(` decision: ${entry.decision}`);
|
|
2483
|
+
if (entry.file) lines.push(` file: ${entry.file}`);
|
|
2484
|
+
if (entry.anchor) lines.push(` anchor: ${entry.anchor}`);
|
|
2485
|
+
if (entry.properties?.length) {
|
|
2486
|
+
lines.push(` properties: ${entry.properties.join(", ")}`);
|
|
2487
|
+
}
|
|
2488
|
+
if (entry.reason) lines.push(` reason: ${entry.reason}`);
|
|
2489
|
+
}
|
|
2490
|
+
return lines;
|
|
2491
|
+
}
|
|
2492
|
+
async function runDirectEventsPass(opts) {
|
|
2493
|
+
const {
|
|
2494
|
+
repoPath,
|
|
2495
|
+
config,
|
|
2496
|
+
systemPrompt: systemPrompt9,
|
|
2497
|
+
playbook,
|
|
2498
|
+
manifest,
|
|
2499
|
+
repoMap,
|
|
2500
|
+
budgetUsd,
|
|
2501
|
+
progress,
|
|
2502
|
+
phaseTimings
|
|
2503
|
+
} = opts;
|
|
2504
|
+
const eventsPrompt = [
|
|
2505
|
+
`The Whisperr ${playbook.target.displayName} SDK is already installed and`,
|
|
2506
|
+
"initialized, and identify() is wired. Now instrument the product events",
|
|
2507
|
+
"below with track().",
|
|
2508
|
+
"",
|
|
2509
|
+
...renderRepoMapSection(repoMap),
|
|
2510
|
+
"Work in importance order. Place each event at the real call site where the",
|
|
2511
|
+
"END USER performs the action (not an admin/staff path \u2014 see WHO COUNTS AS",
|
|
2512
|
+
`"THE USER"), and attach the properties you can source from what's in scope`,
|
|
2513
|
+
"there (see the property-capture rule). These events describe the same",
|
|
2514
|
+
"person identify() did, so they live on the same end-user surface. Skip fast",
|
|
2515
|
+
"when an event has no clear trigger here \u2014 spend steps placing, not exploring.",
|
|
2516
|
+
"Stop once the events that clearly exist in this app are wired.",
|
|
2517
|
+
"",
|
|
2518
|
+
"----- EVENTS -----",
|
|
2519
|
+
renderEventsBrief(manifest),
|
|
2520
|
+
"----- END -----"
|
|
2521
|
+
].join("\n");
|
|
2522
|
+
const pass = await runTimedPass("Instrumenting your events", {
|
|
2523
|
+
prompt: eventsPrompt,
|
|
2524
|
+
systemPrompt: systemPrompt9,
|
|
2525
|
+
repoPath,
|
|
2526
|
+
model: config.model,
|
|
2527
|
+
effort: config.effort,
|
|
2528
|
+
maxTurns: config.maxTurns,
|
|
2529
|
+
budgetUsd,
|
|
2530
|
+
progress
|
|
2531
|
+
}, phaseTimings);
|
|
2532
|
+
const wired = await scanManifestEvents(repoPath, manifest);
|
|
2533
|
+
return {
|
|
2534
|
+
pass,
|
|
2535
|
+
eventOutcomes: manifest.events.map(
|
|
2536
|
+
(event) => wired.has(event.eventType) ? { event: event.eventType, outcome: "wired" } : {
|
|
2537
|
+
event: event.eventType,
|
|
2538
|
+
outcome: "skipped",
|
|
2539
|
+
reason: "No track() call was detected after the direct wiring pass."
|
|
2540
|
+
}
|
|
2541
|
+
)
|
|
1474
2542
|
};
|
|
1475
2543
|
}
|
|
2544
|
+
function planForManifest(plan, manifest) {
|
|
2545
|
+
const known = new Set(manifest.events.map((event) => event.eventType));
|
|
2546
|
+
return uniquePlanEntries(plan.filter((entry) => known.has(entry.event)));
|
|
2547
|
+
}
|
|
2548
|
+
function uniquePlanEntries(entries) {
|
|
2549
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2550
|
+
const unique = [];
|
|
2551
|
+
for (const entry of entries) {
|
|
2552
|
+
if (seen.has(entry.event)) continue;
|
|
2553
|
+
seen.add(entry.event);
|
|
2554
|
+
unique.push(entry);
|
|
2555
|
+
}
|
|
2556
|
+
return unique;
|
|
2557
|
+
}
|
|
2558
|
+
async function scanManifestEvents(repoPath, manifest) {
|
|
2559
|
+
return scanEvents(repoPath, manifest.events.map((event) => event.eventType));
|
|
2560
|
+
}
|
|
2561
|
+
async function scanEvents(repoPath, eventTypes) {
|
|
2562
|
+
let files = [];
|
|
2563
|
+
try {
|
|
2564
|
+
files = await changedFiles(repoPath, { isRepo: true });
|
|
2565
|
+
} catch {
|
|
2566
|
+
return /* @__PURE__ */ new Map();
|
|
2567
|
+
}
|
|
2568
|
+
if (!files.length) return /* @__PURE__ */ new Map();
|
|
2569
|
+
return scanWiredEvents(repoPath, files, eventTypes);
|
|
2570
|
+
}
|
|
2571
|
+
function buildEventOutcomes(manifest, plan, wired) {
|
|
2572
|
+
const byEvent = new Map(plan.map((entry) => [entry.event, entry]));
|
|
2573
|
+
return manifest.events.map((event) => {
|
|
2574
|
+
if (wired.has(event.eventType)) {
|
|
2575
|
+
return { event: event.eventType, outcome: "wired" };
|
|
2576
|
+
}
|
|
2577
|
+
const entry = byEvent.get(event.eventType);
|
|
2578
|
+
if (entry?.decision === "skip") {
|
|
2579
|
+
return {
|
|
2580
|
+
event: event.eventType,
|
|
2581
|
+
outcome: "skipped",
|
|
2582
|
+
reason: entry.reason || "Planner skipped this event for this surface."
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
if (entry?.decision === "unsure") {
|
|
2586
|
+
return {
|
|
2587
|
+
event: event.eventType,
|
|
2588
|
+
outcome: "skipped",
|
|
2589
|
+
reason: entry.reason || "Planner could not confirm a concrete anchor."
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
if (entry?.decision === "place") {
|
|
2593
|
+
return {
|
|
2594
|
+
event: event.eventType,
|
|
2595
|
+
outcome: "skipped",
|
|
2596
|
+
reason: "No track() call was detected after wiring and one targeted follow-up."
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
return {
|
|
2600
|
+
event: event.eventType,
|
|
2601
|
+
outcome: "skipped",
|
|
2602
|
+
reason: "No placement decision was returned."
|
|
2603
|
+
};
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
function refreshWiredOutcomes(current, manifest, wired) {
|
|
2607
|
+
const byEvent = new Map(current.map((outcome) => [outcome.event, outcome]));
|
|
2608
|
+
return manifest.events.map((event) => {
|
|
2609
|
+
if (wired.has(event.eventType)) {
|
|
2610
|
+
return { event: event.eventType, outcome: "wired" };
|
|
2611
|
+
}
|
|
2612
|
+
return byEvent.get(event.eventType) ?? {
|
|
2613
|
+
event: event.eventType,
|
|
2614
|
+
outcome: "skipped",
|
|
2615
|
+
reason: "No track() call was detected."
|
|
2616
|
+
};
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
function tail(s, max) {
|
|
2620
|
+
const trimmed = s.trim();
|
|
2621
|
+
return trimmed.length > max ? trimmed.slice(-max) : trimmed;
|
|
2622
|
+
}
|
|
2623
|
+
function debugNote(message) {
|
|
2624
|
+
if (process.env.WHISPERR_WIZARD_DEBUG) {
|
|
2625
|
+
console.error(`[whisperr-wizard] ${message}`);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
async function runTimedPass(phase, opts, phaseTimings) {
|
|
2629
|
+
opts.progress?.onPhase?.(phase);
|
|
2630
|
+
const started = Date.now();
|
|
2631
|
+
try {
|
|
2632
|
+
return await runPass(opts);
|
|
2633
|
+
} finally {
|
|
2634
|
+
phaseTimings.push({ phase, ms: Date.now() - started });
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
1476
2637
|
function isLimitStop(err) {
|
|
1477
2638
|
const msg = err?.message ?? "";
|
|
1478
2639
|
return /max(imum)?[\s_-]*(turn|budget)|budget.*exceeded|number of turns/i.test(msg);
|
|
1479
2640
|
}
|
|
1480
2641
|
async function runPass(opts) {
|
|
1481
|
-
const {
|
|
2642
|
+
const {
|
|
2643
|
+
prompt,
|
|
2644
|
+
systemPrompt: systemPrompt9,
|
|
2645
|
+
repoPath,
|
|
2646
|
+
model,
|
|
2647
|
+
effort,
|
|
2648
|
+
maxTurns,
|
|
2649
|
+
budgetUsd,
|
|
2650
|
+
allowedTools = FULL_TOOLS,
|
|
2651
|
+
progress
|
|
2652
|
+
} = opts;
|
|
1482
2653
|
let summary = "";
|
|
1483
2654
|
let costUsd = 0;
|
|
1484
2655
|
let ok = false;
|
|
1485
2656
|
let maxedOut = false;
|
|
2657
|
+
let lastActivityLine = "";
|
|
2658
|
+
const emitActivity = (line) => {
|
|
2659
|
+
if (!line || line === lastActivityLine) return;
|
|
2660
|
+
lastActivityLine = line;
|
|
2661
|
+
progress?.onActivity?.(line);
|
|
2662
|
+
};
|
|
1486
2663
|
const response = query({
|
|
1487
2664
|
prompt,
|
|
1488
2665
|
options: {
|
|
@@ -1494,8 +2671,8 @@ async function runPass(opts) {
|
|
|
1494
2671
|
// defaults so the behavior is pinned regardless of SDK version.
|
|
1495
2672
|
thinking: { type: "adaptive" },
|
|
1496
2673
|
effort,
|
|
1497
|
-
|
|
1498
|
-
|
|
2674
|
+
tools: [...allowedTools],
|
|
2675
|
+
canUseTool: createToolPermissionCallback(repoPath),
|
|
1499
2676
|
maxTurns,
|
|
1500
2677
|
// Native SDK hard cost cap. Stops this pass with an `error_max_budget_usd`
|
|
1501
2678
|
// result once spend exceeds the remaining budget — this, not maxTurns, is
|
|
@@ -1509,9 +2686,9 @@ async function runPass(opts) {
|
|
|
1509
2686
|
if (message.type === "assistant") {
|
|
1510
2687
|
for (const block of message.message?.content ?? []) {
|
|
1511
2688
|
if (isTextBlock(block) && block.text?.trim()) {
|
|
1512
|
-
|
|
2689
|
+
emitActivity(firstLine(block.text));
|
|
1513
2690
|
} else if (isToolUseBlock(block)) {
|
|
1514
|
-
|
|
2691
|
+
emitActivity(describeToolUse(block));
|
|
1515
2692
|
}
|
|
1516
2693
|
}
|
|
1517
2694
|
} else if (message.type === "result") {
|
|
@@ -1531,6 +2708,16 @@ async function runPass(opts) {
|
|
|
1531
2708
|
}
|
|
1532
2709
|
return { summary, costUsd, ok, maxedOut };
|
|
1533
2710
|
}
|
|
2711
|
+
function createToolPermissionCallback(repoPath) {
|
|
2712
|
+
return async (toolName, input, options) => {
|
|
2713
|
+
const decision = evaluateToolUse(toolName, input, { repoPath });
|
|
2714
|
+
return decision.behavior === "allow" ? { behavior: "allow", toolUseID: options.toolUseID } : {
|
|
2715
|
+
behavior: "deny",
|
|
2716
|
+
message: decision.message,
|
|
2717
|
+
toolUseID: options.toolUseID
|
|
2718
|
+
};
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
1534
2721
|
function applyModelAuthEnv(config, session) {
|
|
1535
2722
|
if (config.directAnthropicKey) {
|
|
1536
2723
|
process.env.ANTHROPIC_API_KEY = config.directAnthropicKey;
|
|
@@ -1582,102 +2769,6 @@ function short(s, max = 60) {
|
|
|
1582
2769
|
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
1583
2770
|
}
|
|
1584
2771
|
|
|
1585
|
-
// src/core/git.ts
|
|
1586
|
-
import { spawn } from "child_process";
|
|
1587
|
-
import { createHash } from "crypto";
|
|
1588
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
1589
|
-
import { basename, join as join2 } from "path";
|
|
1590
|
-
async function takeCheckpoint(repoPath) {
|
|
1591
|
-
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
1592
|
-
if (!isRepo) return { isRepo: false };
|
|
1593
|
-
const branch = (await run(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
|
1594
|
-
const head = await run(repoPath, ["rev-parse", "HEAD"]);
|
|
1595
|
-
return {
|
|
1596
|
-
isRepo: true,
|
|
1597
|
-
baseRef: head.ok ? head.stdout.trim() : void 0,
|
|
1598
|
-
branch: branch || void 0
|
|
1599
|
-
};
|
|
1600
|
-
}
|
|
1601
|
-
async function changedFiles(repoPath, checkpoint) {
|
|
1602
|
-
if (!checkpoint.isRepo) return [];
|
|
1603
|
-
const tracked = await run(repoPath, ["diff", "--name-only"]);
|
|
1604
|
-
const untracked = await run(repoPath, [
|
|
1605
|
-
"ls-files",
|
|
1606
|
-
"--others",
|
|
1607
|
-
"--exclude-standard"
|
|
1608
|
-
]);
|
|
1609
|
-
const set = /* @__PURE__ */ new Set();
|
|
1610
|
-
for (const f of `${tracked.stdout}
|
|
1611
|
-
${untracked.stdout}`.split("\n")) {
|
|
1612
|
-
if (f.trim()) set.add(f.trim());
|
|
1613
|
-
}
|
|
1614
|
-
return [...set];
|
|
1615
|
-
}
|
|
1616
|
-
function revertHint(checkpoint) {
|
|
1617
|
-
if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
|
|
1618
|
-
return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
|
|
1619
|
-
}
|
|
1620
|
-
async function isWorkingTreeClean(repoPath) {
|
|
1621
|
-
const status = await run(repoPath, ["status", "--porcelain"]);
|
|
1622
|
-
return status.ok && status.stdout.trim() === "";
|
|
1623
|
-
}
|
|
1624
|
-
async function revertToCheckpoint(repoPath, checkpoint) {
|
|
1625
|
-
if (!checkpoint.isRepo) return false;
|
|
1626
|
-
if (checkpoint.baseRef) {
|
|
1627
|
-
const reset = await run(repoPath, ["reset", "--hard", checkpoint.baseRef]);
|
|
1628
|
-
const clean2 = await run(repoPath, ["clean", "-fd"]);
|
|
1629
|
-
return reset.ok && clean2.ok;
|
|
1630
|
-
}
|
|
1631
|
-
const restore = await run(repoPath, ["restore", "."]);
|
|
1632
|
-
const clean = await run(repoPath, ["clean", "-fd"]);
|
|
1633
|
-
return restore.ok && clean.ok;
|
|
1634
|
-
}
|
|
1635
|
-
async function repoFingerprint(repoPath) {
|
|
1636
|
-
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
1637
|
-
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename(repoPath)}`;
|
|
1638
|
-
return createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
1639
|
-
}
|
|
1640
|
-
function normalizeRemote(url) {
|
|
1641
|
-
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
1642
|
-
}
|
|
1643
|
-
async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
1644
|
-
const wired = /* @__PURE__ */ new Map();
|
|
1645
|
-
const patterns = eventTypes.map((e) => ({
|
|
1646
|
-
eventType: e,
|
|
1647
|
-
re: new RegExp(
|
|
1648
|
-
`\\btrack\\s*\\(\\s*[\\s\\S]{0,160}?['"\`]${escapeRegExp(e)}['"\`]`
|
|
1649
|
-
)
|
|
1650
|
-
}));
|
|
1651
|
-
for (const file of files) {
|
|
1652
|
-
let content = "";
|
|
1653
|
-
try {
|
|
1654
|
-
content = await readFile2(join2(repoPath, file), "utf8");
|
|
1655
|
-
} catch {
|
|
1656
|
-
continue;
|
|
1657
|
-
}
|
|
1658
|
-
for (const { eventType, re } of patterns) {
|
|
1659
|
-
if (!wired.has(eventType) && re.test(content)) {
|
|
1660
|
-
wired.set(eventType, file);
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
}
|
|
1664
|
-
return wired;
|
|
1665
|
-
}
|
|
1666
|
-
function escapeRegExp(s) {
|
|
1667
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1668
|
-
}
|
|
1669
|
-
function run(cwd, args) {
|
|
1670
|
-
return new Promise((resolve2) => {
|
|
1671
|
-
const child = spawn("git", args, { cwd });
|
|
1672
|
-
let stdout = "";
|
|
1673
|
-
let stderr = "";
|
|
1674
|
-
child.stdout.on("data", (d) => stdout += d.toString());
|
|
1675
|
-
child.stderr.on("data", (d) => stderr += d.toString());
|
|
1676
|
-
child.on("close", (code) => resolve2({ ok: code === 0, stdout, stderr }));
|
|
1677
|
-
child.on("error", () => resolve2({ ok: false, stdout, stderr }));
|
|
1678
|
-
});
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
2772
|
// src/core/verify.ts
|
|
1682
2773
|
async function pollFirstEvent(config, session, opts = {}) {
|
|
1683
2774
|
if (config.offline) return { received: false };
|
|
@@ -1705,6 +2796,10 @@ async function pollFirstEvent(config, session, opts = {}) {
|
|
|
1705
2796
|
|
|
1706
2797
|
// src/core/postflight.ts
|
|
1707
2798
|
import { spawn as spawn2 } from "child_process";
|
|
2799
|
+
function verdictToVerified(verdict) {
|
|
2800
|
+
if (verdict.toolMissing || verdict.timedOut) return null;
|
|
2801
|
+
return verdict.ok;
|
|
2802
|
+
}
|
|
1708
2803
|
var MAX_OUTPUT = 4e3;
|
|
1709
2804
|
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
1710
2805
|
function runVerifyCommand(repoPath, command, opts = {}) {
|
|
@@ -1712,7 +2807,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
1712
2807
|
return Promise.resolve({ ran: false, ok: true, output: "" });
|
|
1713
2808
|
}
|
|
1714
2809
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
1715
|
-
return new Promise((
|
|
2810
|
+
return new Promise((resolve3) => {
|
|
1716
2811
|
const child = spawn2(command, { cwd: repoPath, shell: true, env: process.env });
|
|
1717
2812
|
let out = "";
|
|
1718
2813
|
const append = (d) => {
|
|
@@ -1723,36 +2818,36 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
1723
2818
|
child.stderr?.on("data", append);
|
|
1724
2819
|
const timer = setTimeout(() => {
|
|
1725
2820
|
child.kill("SIGKILL");
|
|
1726
|
-
|
|
2821
|
+
resolve3({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
|
|
1727
2822
|
}, timeoutMs);
|
|
1728
2823
|
child.on("close", (code) => {
|
|
1729
2824
|
clearTimeout(timer);
|
|
1730
|
-
|
|
2825
|
+
resolve3({
|
|
1731
2826
|
ran: true,
|
|
1732
2827
|
ok: code === 0,
|
|
1733
2828
|
command,
|
|
1734
2829
|
exitCode: code,
|
|
1735
|
-
output:
|
|
2830
|
+
output: tail2(out),
|
|
1736
2831
|
// 127 = "command not found": the verify tool isn't on PATH here.
|
|
1737
2832
|
toolMissing: code === 127
|
|
1738
2833
|
});
|
|
1739
2834
|
});
|
|
1740
2835
|
child.on("error", () => {
|
|
1741
2836
|
clearTimeout(timer);
|
|
1742
|
-
|
|
2837
|
+
resolve3({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
|
|
1743
2838
|
});
|
|
1744
2839
|
});
|
|
1745
2840
|
}
|
|
1746
|
-
function
|
|
2841
|
+
function tail2(s) {
|
|
1747
2842
|
const t = s.trim();
|
|
1748
2843
|
return t.length > MAX_OUTPUT ? `\u2026${t.slice(-MAX_OUTPUT)}` : t;
|
|
1749
2844
|
}
|
|
1750
2845
|
|
|
1751
2846
|
// src/core/report.ts
|
|
1752
2847
|
async function postRunReport(config, session, report) {
|
|
1753
|
-
if (config.offline) return;
|
|
2848
|
+
if (config.offline) return { ok: true };
|
|
1754
2849
|
try {
|
|
1755
|
-
await fetch(`${config.apiBaseUrl}/wizard/report`, {
|
|
2850
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/report`, {
|
|
1756
2851
|
method: "POST",
|
|
1757
2852
|
headers: {
|
|
1758
2853
|
"Content-Type": "application/json",
|
|
@@ -1760,9 +2855,30 @@ async function postRunReport(config, session, report) {
|
|
|
1760
2855
|
},
|
|
1761
2856
|
body: JSON.stringify(report)
|
|
1762
2857
|
});
|
|
1763
|
-
|
|
2858
|
+
if (res.ok) return { ok: true };
|
|
2859
|
+
return {
|
|
2860
|
+
ok: false,
|
|
2861
|
+
status: res.status,
|
|
2862
|
+
detail: await responseDetail(res)
|
|
2863
|
+
};
|
|
2864
|
+
} catch (err) {
|
|
2865
|
+
return { ok: false, detail: detailSlice(errorMessage(err)) };
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
async function responseDetail(res) {
|
|
2869
|
+
try {
|
|
2870
|
+
return detailSlice(await res.text());
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
return detailSlice(errorMessage(err));
|
|
1764
2873
|
}
|
|
1765
2874
|
}
|
|
2875
|
+
function errorMessage(err) {
|
|
2876
|
+
return err instanceof Error ? err.message : String(err);
|
|
2877
|
+
}
|
|
2878
|
+
function detailSlice(detail) {
|
|
2879
|
+
const sliced = detail.slice(0, 200).trim();
|
|
2880
|
+
return sliced || void 0;
|
|
2881
|
+
}
|
|
1766
2882
|
|
|
1767
2883
|
// src/ui/banner.ts
|
|
1768
2884
|
function banner() {
|
|
@@ -1781,7 +2897,7 @@ function banner() {
|
|
|
1781
2897
|
|
|
1782
2898
|
// src/cli.ts
|
|
1783
2899
|
async function run2(options) {
|
|
1784
|
-
const repoPath =
|
|
2900
|
+
const repoPath = resolve2(options.path ?? process.cwd());
|
|
1785
2901
|
const config = resolveConfig(options);
|
|
1786
2902
|
console.log(banner());
|
|
1787
2903
|
p.intro(theme.signal("Let's wire Whisperr into your app."));
|
|
@@ -1859,44 +2975,99 @@ Flutter is live today; ${theme.bright(
|
|
|
1859
2975
|
);
|
|
1860
2976
|
}
|
|
1861
2977
|
const spin = p.spinner();
|
|
2978
|
+
const useIntegrationSpinner = Boolean(process.stdout.isTTY);
|
|
1862
2979
|
let phaseLabel = "Integrating";
|
|
1863
2980
|
let lastLine = "";
|
|
2981
|
+
let lastActivityLine = "";
|
|
1864
2982
|
const startedAt = Date.now();
|
|
1865
|
-
|
|
1866
|
-
const
|
|
2983
|
+
const nextIntegrationMessage = createMessageDeduper();
|
|
2984
|
+
const updateIntegrationMessage = () => {
|
|
1867
2985
|
const secs = Math.round((Date.now() - startedAt) / 1e3);
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
);
|
|
1871
|
-
}
|
|
2986
|
+
const message = theme.bright(phaseLabel) + theme.muted(` \xB7 ${secs}s`) + (lastLine ? theme.muted(` \u2014 ${lastLine}`) : "");
|
|
2987
|
+
const next = nextIntegrationMessage(message);
|
|
2988
|
+
if (next) spin.message(next);
|
|
2989
|
+
};
|
|
2990
|
+
if (useIntegrationSpinner) {
|
|
2991
|
+
spin.start(theme.bright(phaseLabel));
|
|
2992
|
+
} else {
|
|
2993
|
+
p.log.step(theme.bright(phaseLabel));
|
|
2994
|
+
}
|
|
2995
|
+
const tick = useIntegrationSpinner ? setInterval(updateIntegrationMessage, 1e3) : void 0;
|
|
1872
2996
|
const outcome = await runIntegrationAgent({
|
|
1873
2997
|
repoPath,
|
|
1874
2998
|
config,
|
|
1875
2999
|
session,
|
|
1876
3000
|
playbook: chosen.playbook,
|
|
1877
3001
|
manifest,
|
|
3002
|
+
...process.stdout.isTTY && process.stdin.isTTY ? {
|
|
3003
|
+
async onPlanReady(plan) {
|
|
3004
|
+
if (useIntegrationSpinner) {
|
|
3005
|
+
spin.stop(theme.success("Placement plan ready"));
|
|
3006
|
+
}
|
|
3007
|
+
p.note(renderPlacementPlan(plan), "Placement plan");
|
|
3008
|
+
const placeEntries = plan.filter((entry) => entry.decision === "place");
|
|
3009
|
+
let selectedEvents = placeEntries.map((entry) => entry.event);
|
|
3010
|
+
if (placeEntries.length) {
|
|
3011
|
+
const selection = await p.multiselect({
|
|
3012
|
+
message: "Wire these events?",
|
|
3013
|
+
options: placeEntries.map((entry) => ({
|
|
3014
|
+
value: entry.event,
|
|
3015
|
+
label: entry.event,
|
|
3016
|
+
hint: `${entry.file ?? "unknown file"}@${entry.anchor ?? "unknown anchor"}`
|
|
3017
|
+
})),
|
|
3018
|
+
initialValues: selectedEvents,
|
|
3019
|
+
required: false
|
|
3020
|
+
});
|
|
3021
|
+
selectedEvents = p.isCancel(selection) ? [] : selection;
|
|
3022
|
+
}
|
|
3023
|
+
if (useIntegrationSpinner) {
|
|
3024
|
+
spin.start(theme.bright("Continuing integration"));
|
|
3025
|
+
}
|
|
3026
|
+
return filterEventPlan(plan, selectedEvents);
|
|
3027
|
+
}
|
|
3028
|
+
} : {},
|
|
1878
3029
|
progress: {
|
|
1879
3030
|
onPhase(label) {
|
|
1880
3031
|
phaseLabel = label;
|
|
1881
3032
|
lastLine = "";
|
|
3033
|
+
lastActivityLine = "";
|
|
3034
|
+
if (useIntegrationSpinner) {
|
|
3035
|
+
updateIntegrationMessage();
|
|
3036
|
+
} else {
|
|
3037
|
+
p.log.step(theme.bright(label));
|
|
3038
|
+
}
|
|
1882
3039
|
},
|
|
1883
3040
|
onActivity(line) {
|
|
3041
|
+
if (line === lastActivityLine) return;
|
|
3042
|
+
lastActivityLine = line;
|
|
1884
3043
|
lastLine = line;
|
|
3044
|
+
if (useIntegrationSpinner) updateIntegrationMessage();
|
|
1885
3045
|
}
|
|
1886
3046
|
}
|
|
1887
3047
|
}).catch((err) => {
|
|
1888
3048
|
p.log.error(err.message);
|
|
1889
3049
|
return null;
|
|
1890
3050
|
});
|
|
1891
|
-
clearInterval(tick);
|
|
3051
|
+
if (tick) clearInterval(tick);
|
|
1892
3052
|
if (!outcome) {
|
|
1893
|
-
|
|
3053
|
+
if (useIntegrationSpinner) {
|
|
3054
|
+
spin.stop(theme.alert("Integration stopped"));
|
|
3055
|
+
} else {
|
|
3056
|
+
p.log.error(theme.alert("Integration stopped"));
|
|
3057
|
+
}
|
|
1894
3058
|
await maybeRevert(repoPath, checkpoint, "The run stopped before finishing.");
|
|
1895
3059
|
return 1;
|
|
1896
3060
|
}
|
|
1897
3061
|
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");
|
|
1898
|
-
|
|
1899
|
-
|
|
3062
|
+
if (useIntegrationSpinner) {
|
|
3063
|
+
spin.stop(stopLabel);
|
|
3064
|
+
} else if (!outcome.coreOk) {
|
|
3065
|
+
p.log.warn(stopLabel);
|
|
3066
|
+
} else {
|
|
3067
|
+
p.log.success(stopLabel);
|
|
3068
|
+
}
|
|
3069
|
+
const opportunities = await collectOpportunities(repoPath, manifest, checkpoint);
|
|
3070
|
+
let files = await changedFiles(repoPath, checkpoint);
|
|
1900
3071
|
if (files.length) {
|
|
1901
3072
|
p.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
1902
3073
|
}
|
|
@@ -1919,7 +3090,8 @@ Flutter is live today; ${theme.bright(
|
|
|
1919
3090
|
const cmd = chosen.playbook.verifyCommand;
|
|
1920
3091
|
const vspin = p.spinner();
|
|
1921
3092
|
vspin.start(`Verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
1922
|
-
|
|
3093
|
+
let verdict = await runVerifyCommand(repoPath, cmd);
|
|
3094
|
+
verified = verdictToVerified(verdict);
|
|
1923
3095
|
if (verdict.toolMissing) {
|
|
1924
3096
|
vspin.stop(
|
|
1925
3097
|
theme.warn("Couldn't verify automatically") + theme.muted(` \u2014 \`${cmd}\` isn't available here. Run it yourself before committing.`)
|
|
@@ -1929,36 +3101,142 @@ Flutter is live today; ${theme.bright(
|
|
|
1929
3101
|
theme.warn(`Verification timed out \u2014 \`${cmd}\``) + theme.muted(" \u2014 run it yourself before committing.")
|
|
1930
3102
|
);
|
|
1931
3103
|
} else if (verdict.ok) {
|
|
1932
|
-
verified = true;
|
|
1933
3104
|
vspin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
1934
3105
|
} else {
|
|
1935
|
-
|
|
1936
|
-
|
|
3106
|
+
const remainingBudgetUsd = config.budgetUsd - outcome.costUsd;
|
|
3107
|
+
if (remainingBudgetUsd >= 0.5) {
|
|
3108
|
+
vspin.stop(
|
|
3109
|
+
theme.warn(`Verification failed \u2014 ${cmd}`) + theme.muted(" \u2014 attempting one repair pass")
|
|
3110
|
+
);
|
|
3111
|
+
const repairSpin = p.spinner();
|
|
3112
|
+
repairSpin.start(`Repairing verifier failures \u2014 ${theme.muted(cmd)}`);
|
|
3113
|
+
try {
|
|
3114
|
+
const repair = await runRepairPass({
|
|
3115
|
+
repoPath,
|
|
3116
|
+
config,
|
|
3117
|
+
session,
|
|
3118
|
+
playbook: chosen.playbook,
|
|
3119
|
+
verifyCommand: cmd,
|
|
3120
|
+
verifyOutput: verdict.output.slice(-4e3),
|
|
3121
|
+
budgetUsd: remainingBudgetUsd
|
|
3122
|
+
});
|
|
3123
|
+
outcome.costUsd += repair.costUsd;
|
|
3124
|
+
if (repair.summary.trim()) {
|
|
3125
|
+
outcome.summary = [outcome.summary, `Repair:
|
|
3126
|
+
${repair.summary}`].filter(Boolean).join("\n\n");
|
|
3127
|
+
p.log.message(repair.summary.trim());
|
|
3128
|
+
}
|
|
3129
|
+
repairSpin.stop(theme.success("Repair pass finished"));
|
|
3130
|
+
} catch (err) {
|
|
3131
|
+
repairSpin.stop(
|
|
3132
|
+
theme.warn("Repair pass failed") + theme.muted(` \u2014 ${err.message}`)
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
files = await changedFiles(repoPath, checkpoint);
|
|
3136
|
+
const reverifySpin = p.spinner();
|
|
3137
|
+
reverifySpin.start(`Re-verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
3138
|
+
verdict = await runVerifyCommand(repoPath, cmd);
|
|
3139
|
+
verified = verdictToVerified(verdict);
|
|
3140
|
+
if (verdict.toolMissing) {
|
|
3141
|
+
reverifySpin.stop(
|
|
3142
|
+
theme.warn("Couldn't verify automatically") + theme.muted(` \u2014 \`${cmd}\` isn't available here. Run it yourself before committing.`)
|
|
3143
|
+
);
|
|
3144
|
+
} else if (verdict.timedOut) {
|
|
3145
|
+
reverifySpin.stop(
|
|
3146
|
+
theme.warn(`Verification timed out \u2014 \`${cmd}\``) + theme.muted(" \u2014 run it yourself before committing.")
|
|
3147
|
+
);
|
|
3148
|
+
} else if (verdict.ok) {
|
|
3149
|
+
reverifySpin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
3150
|
+
} else {
|
|
3151
|
+
reverifySpin.stop(theme.alert(`Verification still failed \u2014 ${cmd}`));
|
|
3152
|
+
}
|
|
3153
|
+
} else {
|
|
3154
|
+
vspin.stop(
|
|
3155
|
+
theme.alert(`Verification failed \u2014 ${cmd}`) + theme.muted(" \u2014 skipped repair because less than $0.50 budget remains")
|
|
3156
|
+
);
|
|
3157
|
+
}
|
|
3158
|
+
if (!verdict.ok && !verdict.toolMissing && !verdict.timedOut) {
|
|
3159
|
+
if (verdict.output) {
|
|
3160
|
+
p.note(verdict.output, theme.warn("Verifier output"));
|
|
3161
|
+
}
|
|
3162
|
+
const reverted = await maybeRevert(
|
|
3163
|
+
repoPath,
|
|
3164
|
+
checkpoint,
|
|
3165
|
+
"The integration didn't pass its build/lint check, so the edits may be broken."
|
|
3166
|
+
);
|
|
3167
|
+
if (reverted) {
|
|
3168
|
+
p.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
3169
|
+
return 1;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
const { acceptedEvents, instrumentationSnapshot } = await offerOpportunities({
|
|
3175
|
+
repoPath,
|
|
3176
|
+
config,
|
|
3177
|
+
session,
|
|
3178
|
+
playbook: chosen.playbook,
|
|
3179
|
+
manifest,
|
|
3180
|
+
opportunities,
|
|
3181
|
+
fingerprint,
|
|
3182
|
+
checkpoint,
|
|
3183
|
+
remainingBudgetUsd: config.budgetUsd - outcome.costUsd,
|
|
3184
|
+
repoMap: outcome.repoMap
|
|
3185
|
+
});
|
|
3186
|
+
if (instrumentationSnapshot && chosen.playbook.verifyCommand) {
|
|
3187
|
+
const cmd = chosen.playbook.verifyCommand;
|
|
3188
|
+
const preInstrumentationVerified = verified;
|
|
3189
|
+
const vspin = p.spinner();
|
|
3190
|
+
vspin.start(`Re-verifying after instrumenting the new events \u2014 ${theme.muted(cmd)}`);
|
|
3191
|
+
const verdict = await runVerifyCommand(repoPath, cmd);
|
|
3192
|
+
verified = verdictToVerified(verdict);
|
|
3193
|
+
if (verdict.toolMissing) {
|
|
3194
|
+
vspin.stop(
|
|
3195
|
+
theme.warn("Couldn't re-verify automatically") + theme.muted(` \u2014 \`${cmd}\` isn't available here. Run it yourself before committing.`)
|
|
3196
|
+
);
|
|
3197
|
+
} else if (verdict.timedOut) {
|
|
3198
|
+
vspin.stop(
|
|
3199
|
+
theme.warn(`Re-verification timed out \u2014 \`${cmd}\``) + theme.muted(" \u2014 run it yourself before committing.")
|
|
3200
|
+
);
|
|
3201
|
+
} else if (verdict.ok) {
|
|
3202
|
+
vspin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
3203
|
+
} else {
|
|
3204
|
+
vspin.stop(theme.alert(`Verification failed after instrumenting the new events \u2014 ${cmd}`));
|
|
1937
3205
|
if (verdict.output) {
|
|
1938
3206
|
p.note(verdict.output, theme.warn("Verifier output"));
|
|
1939
3207
|
}
|
|
1940
|
-
const
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
)
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
3208
|
+
const doRevert = await p.confirm({
|
|
3209
|
+
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.)",
|
|
3210
|
+
initialValue: true
|
|
3211
|
+
});
|
|
3212
|
+
if (!p.isCancel(doRevert) && doRevert) {
|
|
3213
|
+
if (await restoreToSnapshot(repoPath, checkpoint, instrumentationSnapshot)) {
|
|
3214
|
+
verified = preInstrumentationVerified;
|
|
3215
|
+
p.log.success(theme.success("Reverted the instrumentation edits."));
|
|
3216
|
+
} else {
|
|
3217
|
+
p.log.warn(
|
|
3218
|
+
theme.warn("Couldn't revert automatically \u2014 undo manually: ") + (revertHint(checkpoint) ?? "git reset --hard")
|
|
3219
|
+
);
|
|
3220
|
+
}
|
|
3221
|
+
} else {
|
|
3222
|
+
p.log.info(
|
|
3223
|
+
theme.muted("Left in your working tree \u2014 fix the verifier errors before committing.")
|
|
3224
|
+
);
|
|
1948
3225
|
}
|
|
1949
3226
|
}
|
|
1950
3227
|
}
|
|
1951
|
-
const
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
const
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
3228
|
+
const filesForScan = acceptedEvents.length ? await changedFiles(repoPath, checkpoint) : files;
|
|
3229
|
+
const eventTypesToScan = [
|
|
3230
|
+
...manifest.events.map((e) => e.eventType),
|
|
3231
|
+
...acceptedEvents.map((e) => e.code)
|
|
3232
|
+
];
|
|
3233
|
+
const wiredMap = await scanWiredEvents(repoPath, filesForScan, eventTypesToScan);
|
|
3234
|
+
const reportEvents = eventTypesToScan.map((eventType) => ({
|
|
3235
|
+
event_type: eventType,
|
|
3236
|
+
status: wiredMap.has(eventType) ? "wired" : "skipped",
|
|
3237
|
+
file: wiredMap.get(eventType)
|
|
1960
3238
|
}));
|
|
1961
|
-
await postRunReport(config, session, {
|
|
3239
|
+
const reportResult = await postRunReport(config, session, {
|
|
1962
3240
|
target: chosen.playbook.target.id,
|
|
1963
3241
|
repo_fingerprint: fingerprint,
|
|
1964
3242
|
identify_wired: outcome.coreOk,
|
|
@@ -1968,9 +3246,23 @@ Flutter is live today; ${theme.bright(
|
|
|
1968
3246
|
summary: outcome.summary.slice(0, 4e3),
|
|
1969
3247
|
events: reportEvents
|
|
1970
3248
|
});
|
|
3249
|
+
if (!reportResult.ok) {
|
|
3250
|
+
p.log.warn(
|
|
3251
|
+
theme.warn("Couldn't record this run's coverage on the server") + theme.muted(
|
|
3252
|
+
` (${formatReportFailure(reportResult)}) \u2014 future runs may re-propose events already wired here.`
|
|
3253
|
+
)
|
|
3254
|
+
);
|
|
3255
|
+
}
|
|
3256
|
+
const eventLines = renderEventOutcomeLines(outcome.eventOutcomes, wiredMap);
|
|
3257
|
+
if (eventLines) {
|
|
3258
|
+
p.note(eventLines, "Events");
|
|
3259
|
+
}
|
|
3260
|
+
const eventDenominator = manifest.universeSummary?.wireableHere ?? eventTypesToScan.length;
|
|
3261
|
+
const eventStatsLabel = manifest.universeSummary ? "events wired here" : "events wired";
|
|
3262
|
+
const timingDetails = outcome.phaseTimings.map(({ phase, ms }) => `${shortPhaseLabel(phase)} ${formatDuration(ms)}`).join(" \xB7 ");
|
|
1971
3263
|
p.log.info(
|
|
1972
3264
|
theme.muted(
|
|
1973
|
-
`${wiredMap.size}/${
|
|
3265
|
+
`${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})` : "")
|
|
1974
3266
|
)
|
|
1975
3267
|
);
|
|
1976
3268
|
if (!config.offline) {
|
|
@@ -2005,6 +3297,160 @@ Flutter is live today; ${theme.bright(
|
|
|
2005
3297
|
p.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
|
|
2006
3298
|
return 0;
|
|
2007
3299
|
}
|
|
3300
|
+
var NO_ADDITIONS = { acceptedEvents: [], instrumentationSnapshot: null };
|
|
3301
|
+
async function offerOpportunities(opts) {
|
|
3302
|
+
const { opportunities, manifest, config, session } = opts;
|
|
3303
|
+
const total = opportunities.events.length + opportunities.interventions.length;
|
|
3304
|
+
if (total === 0) return NO_ADDITIONS;
|
|
3305
|
+
const describe = (kind, code, why) => `${theme.muted(`${kind}: `)}${theme.bright(code)}${why ? theme.muted(` \u2014 ${why}`) : ""}`;
|
|
3306
|
+
p.note(
|
|
3307
|
+
[
|
|
3308
|
+
...opportunities.events.map((e) => describe("event", e.code, e.rationale ?? e.description)),
|
|
3309
|
+
...opportunities.interventions.map(
|
|
3310
|
+
(i) => describe("intervention", i.code, i.rationale ?? i.description)
|
|
3311
|
+
)
|
|
3312
|
+
].join("\n"),
|
|
3313
|
+
theme.signal("Opportunities found beyond your onboarding plan")
|
|
3314
|
+
);
|
|
3315
|
+
if (config.offline) {
|
|
3316
|
+
p.log.info(theme.muted("Offline mode \u2014 proposals shown only, nothing submitted."));
|
|
3317
|
+
return NO_ADDITIONS;
|
|
3318
|
+
}
|
|
3319
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
3320
|
+
p.log.info(
|
|
3321
|
+
theme.muted("Non-interactive run \u2014 proposals were not submitted. Re-run interactively to add them.")
|
|
3322
|
+
);
|
|
3323
|
+
return NO_ADDITIONS;
|
|
3324
|
+
}
|
|
3325
|
+
const selection = await p.multiselect({
|
|
3326
|
+
message: "Add these to your Whisperr universe? (new interventions start paused)",
|
|
3327
|
+
options: [
|
|
3328
|
+
...opportunities.events.map((e) => ({
|
|
3329
|
+
value: `e:${e.code}`,
|
|
3330
|
+
label: `event \xB7 ${e.code}`,
|
|
3331
|
+
hint: e.description ?? e.rationale
|
|
3332
|
+
})),
|
|
3333
|
+
...opportunities.interventions.map((i) => ({
|
|
3334
|
+
value: `i:${i.code}`,
|
|
3335
|
+
label: `intervention \xB7 ${i.code}`,
|
|
3336
|
+
hint: i.description ?? i.rationale
|
|
3337
|
+
}))
|
|
3338
|
+
],
|
|
3339
|
+
// Opt-in per item: nothing is pre-selected, so adding to the universe is an
|
|
3340
|
+
// explicit choice rather than an opt-out the user has to notice and undo.
|
|
3341
|
+
initialValues: [],
|
|
3342
|
+
required: false
|
|
3343
|
+
});
|
|
3344
|
+
if (p.isCancel(selection) || selection.length === 0) {
|
|
3345
|
+
p.log.info(theme.muted("Skipped \u2014 your universe is unchanged."));
|
|
3346
|
+
return NO_ADDITIONS;
|
|
3347
|
+
}
|
|
3348
|
+
const chosenSet = new Set(selection);
|
|
3349
|
+
const knownInterventionCodes = new Set(
|
|
3350
|
+
manifest.events.flatMap((e) => e.interventions ?? []).map((i) => i.code)
|
|
3351
|
+
);
|
|
3352
|
+
const knownEventCodes = new Set(manifest.events.map((e) => e.eventType));
|
|
3353
|
+
const pickedEvents = opportunities.events.filter((e) => chosenSet.has(`e:${e.code}`));
|
|
3354
|
+
const pickedInterventions = opportunities.interventions.filter(
|
|
3355
|
+
(i) => chosenSet.has(`i:${i.code}`)
|
|
3356
|
+
);
|
|
3357
|
+
const pickedInterventionCodes = new Set(pickedInterventions.map((i) => i.code));
|
|
3358
|
+
const pickedEventCodes = new Set(pickedEvents.map((e) => e.code));
|
|
3359
|
+
const submitted = {
|
|
3360
|
+
events: pickedEvents.map((e) => ({
|
|
3361
|
+
...e,
|
|
3362
|
+
links: e.links?.filter(
|
|
3363
|
+
(l) => knownInterventionCodes.has(l.interventionCode) || pickedInterventionCodes.has(l.interventionCode)
|
|
3364
|
+
)
|
|
3365
|
+
})),
|
|
3366
|
+
interventions: pickedInterventions.map((i) => ({
|
|
3367
|
+
...i,
|
|
3368
|
+
links: i.links?.filter(
|
|
3369
|
+
(l) => knownEventCodes.has(l.eventCode) || pickedEventCodes.has(l.eventCode)
|
|
3370
|
+
)
|
|
3371
|
+
}))
|
|
3372
|
+
};
|
|
3373
|
+
let result;
|
|
3374
|
+
try {
|
|
3375
|
+
result = await withSpinner(
|
|
3376
|
+
"Adding to your universe",
|
|
3377
|
+
() => submitAdditions(config, session, opts.playbook.target.id, opts.fingerprint, submitted)
|
|
3378
|
+
);
|
|
3379
|
+
} catch (err) {
|
|
3380
|
+
p.log.warn(
|
|
3381
|
+
theme.warn("Couldn't add the proposals") + theme.muted(` \u2014 ${err.message}. Your universe is unchanged.`)
|
|
3382
|
+
);
|
|
3383
|
+
return NO_ADDITIONS;
|
|
3384
|
+
}
|
|
3385
|
+
const lines = result.outcomes.map((o) => {
|
|
3386
|
+
const mark = o.status === "applied" ? theme.success("\u2713") : o.status === "duplicate" ? theme.muted("=") : theme.warn("\u2717");
|
|
3387
|
+
const detail = o.status === "duplicate" ? theme.muted(` (already in your universe${o.duplicateOf && o.duplicateOf !== o.code ? ` as ${o.duplicateOf}` : ""})`) : o.status === "invalid" && o.reason ? theme.muted(` (${o.reason})`) : "";
|
|
3388
|
+
return `${mark} ${o.kind} ${theme.bright(o.code)}${detail}`;
|
|
3389
|
+
});
|
|
3390
|
+
lines.push(
|
|
3391
|
+
theme.muted(
|
|
3392
|
+
`${result.applied} added \xB7 ${result.duplicates} already present \xB7 ${result.invalid} rejected`
|
|
3393
|
+
)
|
|
3394
|
+
);
|
|
3395
|
+
if (submitted.interventions.length) {
|
|
3396
|
+
lines.push(theme.muted("New interventions start paused \u2014 activate them in your dashboard."));
|
|
3397
|
+
}
|
|
3398
|
+
if (result.policyRegen?.status === "pending") {
|
|
3399
|
+
lines.push(
|
|
3400
|
+
theme.muted("Runtime policy update queued \u2014 the additions go live once it completes.")
|
|
3401
|
+
);
|
|
3402
|
+
}
|
|
3403
|
+
if (result.policyRegen?.status === "draft") {
|
|
3404
|
+
lines.push(
|
|
3405
|
+
theme.muted(
|
|
3406
|
+
"A review policy draft was queued \u2014 activate it in your dashboard once it finishes generating to take the additions live (your current live policy was left untouched)."
|
|
3407
|
+
)
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
3410
|
+
p.note(lines.join("\n"), theme.signal("Universe updated"));
|
|
3411
|
+
if (result.policyRegen?.status === "failed") {
|
|
3412
|
+
p.log.warn(
|
|
3413
|
+
theme.warn("Live runtime NOT updated") + theme.muted(
|
|
3414
|
+
" \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."
|
|
3415
|
+
)
|
|
3416
|
+
);
|
|
3417
|
+
}
|
|
3418
|
+
const appliedEventCodes = new Set(
|
|
3419
|
+
result.outcomes.filter((o) => o.kind === "event" && o.status === "applied").map((o) => o.code)
|
|
3420
|
+
);
|
|
3421
|
+
const acceptedEvents = pickedEvents.filter((e) => appliedEventCodes.has(e.code));
|
|
3422
|
+
if (!acceptedEvents.length) return NO_ADDITIONS;
|
|
3423
|
+
const prePass = await snapshotChanges(opts.repoPath, opts.checkpoint);
|
|
3424
|
+
const spin = p.spinner();
|
|
3425
|
+
spin.start(theme.bright("Instrumenting the newly added events"));
|
|
3426
|
+
try {
|
|
3427
|
+
const pass = await runAdditionsInstrumentationPass({
|
|
3428
|
+
repoPath: opts.repoPath,
|
|
3429
|
+
config,
|
|
3430
|
+
session,
|
|
3431
|
+
playbook: opts.playbook,
|
|
3432
|
+
acceptedEvents,
|
|
3433
|
+
budgetUsd: opts.remainingBudgetUsd,
|
|
3434
|
+
repoMap: opts.repoMap
|
|
3435
|
+
});
|
|
3436
|
+
if (pass.ran) {
|
|
3437
|
+
spin.stop(theme.success("New events instrumented"));
|
|
3438
|
+
if (pass.summary.trim()) p.log.message(pass.summary.trim());
|
|
3439
|
+
} else {
|
|
3440
|
+
spin.stop(
|
|
3441
|
+
theme.warn("Skipped instrumenting the new events") + theme.muted(
|
|
3442
|
+
" \u2014 the spend limit was reached. They're in your universe; wire them on a future run."
|
|
3443
|
+
)
|
|
3444
|
+
);
|
|
3445
|
+
}
|
|
3446
|
+
} catch (err) {
|
|
3447
|
+
spin.stop(
|
|
3448
|
+
theme.warn("Couldn't instrument the new events") + theme.muted(` \u2014 ${err.message}. They're in your universe; wire them on a future run.`)
|
|
3449
|
+
);
|
|
3450
|
+
}
|
|
3451
|
+
const edited = !snapshotsEqual(prePass, await snapshotChanges(opts.repoPath, opts.checkpoint));
|
|
3452
|
+
return { acceptedEvents, instrumentationSnapshot: edited ? prePass : null };
|
|
3453
|
+
}
|
|
2008
3454
|
async function maybeRevert(repoPath, checkpoint, reason) {
|
|
2009
3455
|
if (!checkpoint.isRepo) return false;
|
|
2010
3456
|
const files = await changedFiles(repoPath, checkpoint);
|
|
@@ -2092,20 +3538,99 @@ async function withSpinner(label, fn) {
|
|
|
2092
3538
|
throw err;
|
|
2093
3539
|
}
|
|
2094
3540
|
}
|
|
3541
|
+
function createMessageDeduper() {
|
|
3542
|
+
let last = "";
|
|
3543
|
+
return (message) => {
|
|
3544
|
+
if (message === last) return null;
|
|
3545
|
+
last = message;
|
|
3546
|
+
return message;
|
|
3547
|
+
};
|
|
3548
|
+
}
|
|
3549
|
+
function formatDuration(ms) {
|
|
3550
|
+
const safeMs = Math.max(0, ms);
|
|
3551
|
+
const totalSeconds = Math.round(safeMs / 1e3);
|
|
3552
|
+
if (safeMs < 6e4) return `${totalSeconds}s`;
|
|
3553
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
3554
|
+
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
3555
|
+
return `${minutes}m${seconds}s`;
|
|
3556
|
+
}
|
|
3557
|
+
function filterEventPlan(plan, selectedEvents) {
|
|
3558
|
+
const selected = new Set(selectedEvents);
|
|
3559
|
+
return plan.map(
|
|
3560
|
+
(entry) => entry.decision === "place" && !selected.has(entry.event) ? { ...entry, decision: "skip", reason: "Deselected in plan review." } : entry
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3563
|
+
function renderPlacementPlan(plan) {
|
|
3564
|
+
return plan.map((entry) => {
|
|
3565
|
+
if (entry.decision === "place") {
|
|
3566
|
+
return theme.success("\u2713 ") + entry.event + theme.muted(
|
|
3567
|
+
` \u2014 ${entry.file ?? "unknown file"} @ ${entry.anchor ?? "unknown anchor"}`
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3570
|
+
const reason = entry.reason || "No reason provided.";
|
|
3571
|
+
const line = `${entry.decision === "skip" ? "\u25CB" : "?"} ${entry.event} \u2014 ${reason}`;
|
|
3572
|
+
return entry.decision === "skip" ? theme.muted(line) : theme.warn(line);
|
|
3573
|
+
}).join("\n");
|
|
3574
|
+
}
|
|
3575
|
+
function renderEventOutcomeLines(outcomes, wiredMap) {
|
|
3576
|
+
const maxLines = 40;
|
|
3577
|
+
const visibleCount = outcomes.length > maxLines ? maxLines - 1 : outcomes.length;
|
|
3578
|
+
const lines = outcomes.slice(0, visibleCount).map((outcome) => {
|
|
3579
|
+
if (outcome.outcome === "wired") {
|
|
3580
|
+
const file = wiredMap.get(outcome.event);
|
|
3581
|
+
return theme.success("\u2713 ") + outcome.event + (file ? theme.muted(` \u2014 ${file}`) : "");
|
|
3582
|
+
}
|
|
3583
|
+
return theme.muted(
|
|
3584
|
+
`\u25CB ${outcome.event} \u2014 ${outcome.reason || "No track() call was detected."}`
|
|
3585
|
+
);
|
|
3586
|
+
});
|
|
3587
|
+
const remainder = outcomes.length - visibleCount;
|
|
3588
|
+
if (remainder > 0) lines.push(theme.muted(`\u2026 ${remainder} more events`));
|
|
3589
|
+
return lines.join("\n");
|
|
3590
|
+
}
|
|
3591
|
+
function shortPhaseLabel(phase) {
|
|
3592
|
+
switch (phase) {
|
|
3593
|
+
case "Mapping your codebase":
|
|
3594
|
+
return "map";
|
|
3595
|
+
case "Installing the SDK & wiring identify()":
|
|
3596
|
+
return "core";
|
|
3597
|
+
case "Planning event placements":
|
|
3598
|
+
return "plan";
|
|
3599
|
+
case "Instrumenting planned events":
|
|
3600
|
+
case "Instrumenting your events":
|
|
3601
|
+
return "wire";
|
|
3602
|
+
case "Reconciling missed events":
|
|
3603
|
+
return "reconcile";
|
|
3604
|
+
case "Reviewing & correcting placements":
|
|
3605
|
+
return "review";
|
|
3606
|
+
default:
|
|
3607
|
+
return phase;
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
2095
3610
|
function summarizeManifest(m) {
|
|
2096
3611
|
const events = [...m.events].sort((a, b) => (b.importance ?? 0) - (a.importance ?? 0)).map((e) => theme.muted("\u2022 ") + e.eventType);
|
|
2097
3612
|
const channels = m.identify.channels?.length ? theme.muted(`channels: ${m.identify.channels.join(", ")}`) : "";
|
|
3613
|
+
const firstLine2 = m.universeSummary ? theme.bright(`${m.universeSummary.total} events in your universe`) + theme.muted(
|
|
3614
|
+
` \u2014 ${m.universeSummary.wireableHere} wireable here \xB7 ${m.universeSummary.derived} computed by Whisperr from your raw events \xB7 ${m.universeSummary.otherSurface} live on other surfaces`
|
|
3615
|
+
) : theme.bright("identify()") + theme.muted(" + ") + theme.bright(`${m.events.length} events`);
|
|
2098
3616
|
return [
|
|
2099
|
-
|
|
3617
|
+
firstLine2,
|
|
2100
3618
|
...events,
|
|
2101
3619
|
channels
|
|
2102
3620
|
].filter(Boolean).join("\n");
|
|
2103
3621
|
}
|
|
3622
|
+
function formatReportFailure(result) {
|
|
3623
|
+
const detail = result.detail?.replace(/\s+/g, " ").trim();
|
|
3624
|
+
if (result.status !== void 0) {
|
|
3625
|
+
return `HTTP ${result.status}${detail ? `: ${detail}` : ""}`;
|
|
3626
|
+
}
|
|
3627
|
+
return detail || "request failed";
|
|
3628
|
+
}
|
|
2104
3629
|
|
|
2105
3630
|
// src/index.ts
|
|
2106
3631
|
var modulePath = fileURLToPath(import.meta.url);
|
|
2107
3632
|
function packageVersion() {
|
|
2108
|
-
const packagePath =
|
|
3633
|
+
const packagePath = join4(dirname2(modulePath), "..", "package.json");
|
|
2109
3634
|
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
2110
3635
|
return pkg.version ?? "0.0.0";
|
|
2111
3636
|
}
|