@whisperr/wizard 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +646 -111
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
|
|
@@ -1148,6 +1148,341 @@ function mockManifest(appId, config) {
|
|
|
1148
1148
|
// src/core/agent.ts
|
|
1149
1149
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1150
1150
|
|
|
1151
|
+
// src/core/opportunities.ts
|
|
1152
|
+
import { readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
1153
|
+
import { join as join3 } from "path";
|
|
1154
|
+
|
|
1155
|
+
// src/core/git.ts
|
|
1156
|
+
import { spawn } from "child_process";
|
|
1157
|
+
import { createHash } from "crypto";
|
|
1158
|
+
import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
|
|
1159
|
+
import { basename, dirname, join as join2 } from "path";
|
|
1160
|
+
async function takeCheckpoint(repoPath) {
|
|
1161
|
+
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
1162
|
+
if (!isRepo) return { isRepo: false };
|
|
1163
|
+
const branch = (await run(repoPath, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
|
1164
|
+
const head = await run(repoPath, ["rev-parse", "HEAD"]);
|
|
1165
|
+
return {
|
|
1166
|
+
isRepo: true,
|
|
1167
|
+
baseRef: head.ok ? head.stdout.trim() : void 0,
|
|
1168
|
+
branch: branch || void 0
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
async function changedFiles(repoPath, checkpoint) {
|
|
1172
|
+
if (!checkpoint.isRepo) return [];
|
|
1173
|
+
const tracked = await run(repoPath, ["diff", "--name-only"]);
|
|
1174
|
+
const untracked = await run(repoPath, [
|
|
1175
|
+
"ls-files",
|
|
1176
|
+
"--others",
|
|
1177
|
+
"--exclude-standard"
|
|
1178
|
+
]);
|
|
1179
|
+
const set = /* @__PURE__ */ new Set();
|
|
1180
|
+
for (const f of `${tracked.stdout}
|
|
1181
|
+
${untracked.stdout}`.split("\n")) {
|
|
1182
|
+
if (f.trim()) set.add(f.trim());
|
|
1183
|
+
}
|
|
1184
|
+
return [...set];
|
|
1185
|
+
}
|
|
1186
|
+
function revertHint(checkpoint) {
|
|
1187
|
+
if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
|
|
1188
|
+
return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
|
|
1189
|
+
}
|
|
1190
|
+
async function wasTrackedAtCheckpoint(repoPath, checkpoint, file) {
|
|
1191
|
+
if (!checkpoint.isRepo) return false;
|
|
1192
|
+
if (checkpoint.baseRef) {
|
|
1193
|
+
const res2 = await run(repoPath, [
|
|
1194
|
+
"ls-tree",
|
|
1195
|
+
"--name-only",
|
|
1196
|
+
checkpoint.baseRef,
|
|
1197
|
+
"--",
|
|
1198
|
+
file
|
|
1199
|
+
]);
|
|
1200
|
+
return res2.ok && res2.stdout.trim() !== "";
|
|
1201
|
+
}
|
|
1202
|
+
const res = await run(repoPath, ["ls-files", "--", file]);
|
|
1203
|
+
return res.ok && res.stdout.trim() !== "";
|
|
1204
|
+
}
|
|
1205
|
+
async function isWorkingTreeClean(repoPath) {
|
|
1206
|
+
const status = await run(repoPath, ["status", "--porcelain"]);
|
|
1207
|
+
return status.ok && status.stdout.trim() === "";
|
|
1208
|
+
}
|
|
1209
|
+
async function revertToCheckpoint(repoPath, checkpoint) {
|
|
1210
|
+
if (!checkpoint.isRepo) return false;
|
|
1211
|
+
if (checkpoint.baseRef) {
|
|
1212
|
+
const reset = await run(repoPath, ["reset", "--hard", checkpoint.baseRef]);
|
|
1213
|
+
const clean2 = await run(repoPath, ["clean", "-fd"]);
|
|
1214
|
+
return reset.ok && clean2.ok;
|
|
1215
|
+
}
|
|
1216
|
+
const restore = await run(repoPath, ["restore", "."]);
|
|
1217
|
+
const clean = await run(repoPath, ["clean", "-fd"]);
|
|
1218
|
+
return restore.ok && clean.ok;
|
|
1219
|
+
}
|
|
1220
|
+
async function snapshotChanges(repoPath, checkpoint) {
|
|
1221
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
1222
|
+
for (const file of await changedFiles(repoPath, checkpoint)) {
|
|
1223
|
+
try {
|
|
1224
|
+
snapshot.set(file, await readFile2(join2(repoPath, file)));
|
|
1225
|
+
} catch {
|
|
1226
|
+
snapshot.set(file, null);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return snapshot;
|
|
1230
|
+
}
|
|
1231
|
+
function snapshotsEqual(a, b) {
|
|
1232
|
+
if (a.size !== b.size) return false;
|
|
1233
|
+
for (const [file, content] of a) {
|
|
1234
|
+
const other = b.get(file);
|
|
1235
|
+
if (other === void 0) return false;
|
|
1236
|
+
if (content === null || other === null) {
|
|
1237
|
+
if (content !== other) return false;
|
|
1238
|
+
} else if (!content.equals(other)) {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
async function restoreToSnapshot(repoPath, checkpoint, snapshot) {
|
|
1245
|
+
try {
|
|
1246
|
+
const current = await changedFiles(repoPath, checkpoint);
|
|
1247
|
+
for (const file of /* @__PURE__ */ new Set([...current, ...snapshot.keys()])) {
|
|
1248
|
+
const recorded = snapshot.get(file);
|
|
1249
|
+
const path = join2(repoPath, file);
|
|
1250
|
+
if (recorded === void 0) {
|
|
1251
|
+
if (checkpoint.baseRef) {
|
|
1252
|
+
const co = await run(repoPath, ["checkout", checkpoint.baseRef, "--", file]);
|
|
1253
|
+
if (co.ok) continue;
|
|
1254
|
+
}
|
|
1255
|
+
await rm(path, { force: true });
|
|
1256
|
+
} else if (recorded === null) {
|
|
1257
|
+
await rm(path, { force: true });
|
|
1258
|
+
} else {
|
|
1259
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1260
|
+
await writeFile(path, recorded);
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
return true;
|
|
1264
|
+
} catch {
|
|
1265
|
+
return false;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
async function repoFingerprint(repoPath) {
|
|
1269
|
+
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
1270
|
+
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename(repoPath)}`;
|
|
1271
|
+
return createHash("sha256").update(seed).digest("hex").slice(0, 16);
|
|
1272
|
+
}
|
|
1273
|
+
function normalizeRemote(url) {
|
|
1274
|
+
return url.replace(/^git@([^:]+):/, "$1/").replace(/^https?:\/\//, "").replace(/\.git$/, "").replace(/\/+$/, "").toLowerCase();
|
|
1275
|
+
}
|
|
1276
|
+
async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
1277
|
+
const wired = /* @__PURE__ */ new Map();
|
|
1278
|
+
const patterns = eventTypes.map((e) => ({
|
|
1279
|
+
eventType: e,
|
|
1280
|
+
re: new RegExp(
|
|
1281
|
+
`\\btrack\\s*\\(\\s*[\\s\\S]{0,160}?['"\`]${escapeRegExp(e)}['"\`]`
|
|
1282
|
+
)
|
|
1283
|
+
}));
|
|
1284
|
+
for (const file of files) {
|
|
1285
|
+
let content = "";
|
|
1286
|
+
try {
|
|
1287
|
+
content = await readFile2(join2(repoPath, file), "utf8");
|
|
1288
|
+
} catch {
|
|
1289
|
+
continue;
|
|
1290
|
+
}
|
|
1291
|
+
for (const { eventType, re } of patterns) {
|
|
1292
|
+
if (!wired.has(eventType) && re.test(content)) {
|
|
1293
|
+
wired.set(eventType, file);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return wired;
|
|
1298
|
+
}
|
|
1299
|
+
function escapeRegExp(s) {
|
|
1300
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1301
|
+
}
|
|
1302
|
+
function run(cwd, args) {
|
|
1303
|
+
return new Promise((resolve2) => {
|
|
1304
|
+
const child = spawn("git", args, { cwd });
|
|
1305
|
+
let stdout = "";
|
|
1306
|
+
let stderr = "";
|
|
1307
|
+
child.stdout.on("data", (d) => stdout += d.toString());
|
|
1308
|
+
child.stderr.on("data", (d) => stderr += d.toString());
|
|
1309
|
+
child.on("close", (code) => resolve2({ ok: code === 0, stdout, stderr }));
|
|
1310
|
+
child.on("error", () => resolve2({ ok: false, stdout, stderr }));
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/core/opportunities.ts
|
|
1315
|
+
var OPPORTUNITIES_FILE = "whisperr-opportunities.json";
|
|
1316
|
+
function normalizeCode(value) {
|
|
1317
|
+
return value.trim().toLowerCase().replace(/[ \-./]/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
1318
|
+
}
|
|
1319
|
+
async function collectOpportunities(repoPath, manifest, checkpoint) {
|
|
1320
|
+
const path = join3(repoPath, OPPORTUNITIES_FILE);
|
|
1321
|
+
if (await wasTrackedAtCheckpoint(repoPath, checkpoint, OPPORTUNITIES_FILE)) {
|
|
1322
|
+
return { events: [], interventions: [] };
|
|
1323
|
+
}
|
|
1324
|
+
let rawText;
|
|
1325
|
+
try {
|
|
1326
|
+
rawText = await readFile3(path, "utf8");
|
|
1327
|
+
} catch {
|
|
1328
|
+
return { events: [], interventions: [] };
|
|
1329
|
+
}
|
|
1330
|
+
await rm2(path, { force: true }).catch(() => {
|
|
1331
|
+
});
|
|
1332
|
+
let raw;
|
|
1333
|
+
try {
|
|
1334
|
+
raw = JSON.parse(rawText);
|
|
1335
|
+
} catch {
|
|
1336
|
+
return { events: [], interventions: [] };
|
|
1337
|
+
}
|
|
1338
|
+
return sanitizeOpportunities(raw, manifest);
|
|
1339
|
+
}
|
|
1340
|
+
function sanitizeOpportunities(raw, manifest) {
|
|
1341
|
+
const out = { events: [], interventions: [] };
|
|
1342
|
+
if (typeof raw !== "object" || raw === null) return out;
|
|
1343
|
+
const doc = raw;
|
|
1344
|
+
const knownEvents = new Set(manifest.events.map((e) => normalizeCode(e.eventType)));
|
|
1345
|
+
const knownInterventions = new Set(
|
|
1346
|
+
manifest.events.flatMap((e) => e.interventions ?? []).map((i) => normalizeCode(i.code))
|
|
1347
|
+
);
|
|
1348
|
+
const seenEvents = /* @__PURE__ */ new Set();
|
|
1349
|
+
for (const entry of asArray(doc.events)) {
|
|
1350
|
+
const ev = coerceEvent(entry);
|
|
1351
|
+
if (!ev) continue;
|
|
1352
|
+
if (knownEvents.has(ev.code) || seenEvents.has(ev.code)) continue;
|
|
1353
|
+
seenEvents.add(ev.code);
|
|
1354
|
+
out.events.push(ev);
|
|
1355
|
+
}
|
|
1356
|
+
const seenInterventions = /* @__PURE__ */ new Set();
|
|
1357
|
+
for (const entry of asArray(doc.interventions)) {
|
|
1358
|
+
const iv = coerceIntervention(entry);
|
|
1359
|
+
if (!iv) continue;
|
|
1360
|
+
if (knownInterventions.has(iv.code) || seenInterventions.has(iv.code)) continue;
|
|
1361
|
+
seenInterventions.add(iv.code);
|
|
1362
|
+
out.interventions.push(iv);
|
|
1363
|
+
}
|
|
1364
|
+
return out;
|
|
1365
|
+
}
|
|
1366
|
+
var SUBMIT_TIMEOUT_MS = 3e4;
|
|
1367
|
+
async function submitAdditions(config, session, target9, repoFingerprint2, opportunities) {
|
|
1368
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/universe/additions`, {
|
|
1369
|
+
method: "POST",
|
|
1370
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
1371
|
+
headers: {
|
|
1372
|
+
"Content-Type": "application/json",
|
|
1373
|
+
Authorization: `Bearer ${session.token}`
|
|
1374
|
+
},
|
|
1375
|
+
body: JSON.stringify({
|
|
1376
|
+
target: target9,
|
|
1377
|
+
repoFingerprint: repoFingerprint2,
|
|
1378
|
+
events: opportunities.events,
|
|
1379
|
+
interventions: opportunities.interventions
|
|
1380
|
+
})
|
|
1381
|
+
});
|
|
1382
|
+
if (!res.ok) {
|
|
1383
|
+
const body = await res.text().catch(() => "");
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
`Submitting universe additions failed (${res.status})${body ? `: ${body.slice(0, 300)}` : ""}`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
return await res.json();
|
|
1389
|
+
}
|
|
1390
|
+
function asArray(value) {
|
|
1391
|
+
return Array.isArray(value) ? value : [];
|
|
1392
|
+
}
|
|
1393
|
+
function asString(value) {
|
|
1394
|
+
if (typeof value !== "string") return void 0;
|
|
1395
|
+
const cleaned = value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
1396
|
+
return cleaned ? cleaned : void 0;
|
|
1397
|
+
}
|
|
1398
|
+
function asConfidence(value) {
|
|
1399
|
+
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
1400
|
+
return Math.min(1, Math.max(0, value));
|
|
1401
|
+
}
|
|
1402
|
+
function asWeight(value) {
|
|
1403
|
+
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
1404
|
+
if (value < 0 || value > 1) return void 0;
|
|
1405
|
+
return value;
|
|
1406
|
+
}
|
|
1407
|
+
function coerceEvent(entry) {
|
|
1408
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
1409
|
+
const e = entry;
|
|
1410
|
+
const code = normalizeCode(asString(e.code) ?? "");
|
|
1411
|
+
if (!code) return null;
|
|
1412
|
+
const properties = [];
|
|
1413
|
+
for (const prop of asArray(e.properties)) {
|
|
1414
|
+
if (typeof prop === "string") {
|
|
1415
|
+
if (prop.trim()) properties.push({ name: prop.trim() });
|
|
1416
|
+
continue;
|
|
1417
|
+
}
|
|
1418
|
+
if (typeof prop !== "object" || prop === null) continue;
|
|
1419
|
+
const pr = prop;
|
|
1420
|
+
const name = asString(pr.name);
|
|
1421
|
+
if (!name) continue;
|
|
1422
|
+
const desc = asString(pr.description);
|
|
1423
|
+
properties.push({
|
|
1424
|
+
name,
|
|
1425
|
+
...desc ? { description: desc } : {},
|
|
1426
|
+
...pr.required === true ? { required: true } : {}
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
const links = [];
|
|
1430
|
+
for (const link of asArray(e.links)) {
|
|
1431
|
+
if (typeof link !== "object" || link === null) continue;
|
|
1432
|
+
const l = link;
|
|
1433
|
+
const interventionCode = normalizeCode(asString(l.interventionCode) ?? "");
|
|
1434
|
+
if (!interventionCode) continue;
|
|
1435
|
+
const weight = asWeight(l.weight);
|
|
1436
|
+
links.push({ interventionCode, ...weight !== void 0 ? { weight } : {} });
|
|
1437
|
+
}
|
|
1438
|
+
const side = normalizeSide(asString(e.side));
|
|
1439
|
+
return {
|
|
1440
|
+
code,
|
|
1441
|
+
label: asString(e.label),
|
|
1442
|
+
description: asString(e.description),
|
|
1443
|
+
...side ? { side } : {},
|
|
1444
|
+
rationale: asString(e.rationale),
|
|
1445
|
+
confidence: asConfidence(e.confidence),
|
|
1446
|
+
...properties.length ? { properties } : {},
|
|
1447
|
+
...links.length ? { links } : {}
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function coerceIntervention(entry) {
|
|
1451
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
1452
|
+
const i = entry;
|
|
1453
|
+
const code = normalizeCode(asString(i.code) ?? "");
|
|
1454
|
+
if (!code) return null;
|
|
1455
|
+
const links = [];
|
|
1456
|
+
for (const link of asArray(i.links)) {
|
|
1457
|
+
if (typeof link !== "object" || link === null) continue;
|
|
1458
|
+
const l = link;
|
|
1459
|
+
const eventCode = normalizeCode(asString(l.eventCode) ?? "");
|
|
1460
|
+
if (!eventCode) continue;
|
|
1461
|
+
const weight = asWeight(l.weight);
|
|
1462
|
+
links.push({ eventCode, ...weight !== void 0 ? { weight } : {} });
|
|
1463
|
+
}
|
|
1464
|
+
return {
|
|
1465
|
+
code,
|
|
1466
|
+
label: asString(i.label),
|
|
1467
|
+
description: asString(i.description),
|
|
1468
|
+
rationale: asString(i.rationale),
|
|
1469
|
+
confidence: asConfidence(i.confidence),
|
|
1470
|
+
...links.length ? { links } : {}
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
function normalizeSide(value) {
|
|
1474
|
+
switch (value?.toLowerCase()) {
|
|
1475
|
+
case "frontend":
|
|
1476
|
+
return "frontend";
|
|
1477
|
+
case "backend":
|
|
1478
|
+
return "backend";
|
|
1479
|
+
case "either":
|
|
1480
|
+
return "either";
|
|
1481
|
+
default:
|
|
1482
|
+
return void 0;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1151
1486
|
// src/core/playbooks/shared-prompt.ts
|
|
1152
1487
|
var BASE_WIZARD_PROMPT = `
|
|
1153
1488
|
You are the Whisperr Wizard \u2014 an expert integration agent running INSIDE a
|
|
@@ -1324,6 +1659,46 @@ function renderEventsBrief(m) {
|
|
|
1324
1659
|
}
|
|
1325
1660
|
return lines.join("\n");
|
|
1326
1661
|
}
|
|
1662
|
+
function renderOpportunitiesBrief(m, opportunitiesFile) {
|
|
1663
|
+
const eventCodes = m.events.map((e) => e.eventType);
|
|
1664
|
+
const interventionCodes = [
|
|
1665
|
+
...new Set(m.events.flatMap((e) => e.interventions ?? []).map((i) => i.code))
|
|
1666
|
+
];
|
|
1667
|
+
return [
|
|
1668
|
+
"UNIVERSE OPPORTUNITIES (do this LAST, after your corrections):",
|
|
1669
|
+
"While auditing you read this app's real lifecycle. If you found churn-",
|
|
1670
|
+
"relevant moments the plan does NOT cover \u2014 e.g. a recurring-payment or",
|
|
1671
|
+
"cancellation path with no event, a support/refund flow worth a retention",
|
|
1672
|
+
"play \u2014 record them as PROPOSALS. Do NOT add track() calls for them.",
|
|
1673
|
+
`Write a single JSON file at the repo root named ${opportunitiesFile}:`,
|
|
1674
|
+
"",
|
|
1675
|
+
"{",
|
|
1676
|
+
' "events": [{',
|
|
1677
|
+
' "code": "snake_case_event", "label": "...", "description": "...",',
|
|
1678
|
+
' "side": "frontend|backend|either", "rationale": "what in the code shows this",',
|
|
1679
|
+
' "confidence": 0.0-1.0,',
|
|
1680
|
+
' "properties": [{"name": "...", "description": "...", "required": false}],',
|
|
1681
|
+
' "links": [{"interventionCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
1682
|
+
" }],",
|
|
1683
|
+
' "interventions": [{',
|
|
1684
|
+
' "code": "snake_case_strategy", "label": "...", "description": "...",',
|
|
1685
|
+
' "rationale": "...", "confidence": 0.0-1.0,',
|
|
1686
|
+
' "links": [{"eventCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
1687
|
+
" }]",
|
|
1688
|
+
"}",
|
|
1689
|
+
"",
|
|
1690
|
+
"Rules:",
|
|
1691
|
+
`- The plan already covers these events: ${eventCodes.join(", ") || "(none)"}.`,
|
|
1692
|
+
` And these interventions: ${interventionCodes.join(", ") || "(none)"}.`,
|
|
1693
|
+
" Propose ONLY what is genuinely missing \u2014 never re-propose or rename these.",
|
|
1694
|
+
"- Every proposal needs concrete code evidence in its rationale (file/flow),",
|
|
1695
|
+
" not speculation. 0-3 events and 0-2 interventions is the normal range;",
|
|
1696
|
+
" an empty file or no file at all is a perfectly good outcome.",
|
|
1697
|
+
"- Link each proposed event to the intervention(s) it should feed (existing",
|
|
1698
|
+
" codes or ones you propose in the same file).",
|
|
1699
|
+
"- This file is metadata for the wizard, not app code \u2014 write it and move on."
|
|
1700
|
+
].join("\n");
|
|
1701
|
+
}
|
|
1327
1702
|
function coverageNote(coverage) {
|
|
1328
1703
|
if (!coverage?.length) return "";
|
|
1329
1704
|
const here = coverage.find((c) => c.sameSurface && c.status === "wired");
|
|
@@ -1448,8 +1823,12 @@ ${events.summary}`);
|
|
|
1448
1823
|
"4. Coverage \u2014 every gateway/callback path that should emit an event does;",
|
|
1449
1824
|
" no recurring or secondary path left as a dead zone.",
|
|
1450
1825
|
"Change ONLY what is genuinely wrong or incomplete \u2014 do not churn correct",
|
|
1451
|
-
"calls or re-explore the whole repo. Be surgical.
|
|
1452
|
-
"
|
|
1826
|
+
"calls or re-explore the whole repo. Be surgical.",
|
|
1827
|
+
"",
|
|
1828
|
+
renderOpportunitiesBrief(manifest, OPPORTUNITIES_FILE),
|
|
1829
|
+
"",
|
|
1830
|
+
"End with a one-line, plain-text note of what you corrected (or 'No",
|
|
1831
|
+
"corrections needed.'), plus one line on what you proposed, if anything."
|
|
1453
1832
|
].join("\n");
|
|
1454
1833
|
const review = await runPass({
|
|
1455
1834
|
prompt: reviewPrompt,
|
|
@@ -1473,6 +1852,53 @@ ${review.summary}`);
|
|
|
1473
1852
|
eventsComplete
|
|
1474
1853
|
};
|
|
1475
1854
|
}
|
|
1855
|
+
async function runAdditionsInstrumentationPass(opts) {
|
|
1856
|
+
const { repoPath, config, session, playbook, acceptedEvents, budgetUsd, progress } = opts;
|
|
1857
|
+
if (!acceptedEvents.length || budgetUsd <= 0) {
|
|
1858
|
+
return { summary: "", costUsd: 0, ran: false };
|
|
1859
|
+
}
|
|
1860
|
+
applyModelAuthEnv(config, session);
|
|
1861
|
+
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
1862
|
+
const eventLines = [];
|
|
1863
|
+
for (const e of acceptedEvents) {
|
|
1864
|
+
eventLines.push("");
|
|
1865
|
+
eventLines.push(`\u25A0 ${e.code}${e.label ? ` (${e.label})` : ""}`);
|
|
1866
|
+
if (e.description) eventLines.push(` ${e.description}`);
|
|
1867
|
+
if (e.rationale) eventLines.push(` where: ${e.rationale}`);
|
|
1868
|
+
if (e.properties?.length) {
|
|
1869
|
+
eventLines.push(" properties to capture if available:");
|
|
1870
|
+
for (const prop of e.properties) {
|
|
1871
|
+
eventLines.push(` \xB7 ${prop.name}${prop.required ? " (required)" : ""}${prop.description ? ` \u2014 ${prop.description}` : ""}`);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
progress?.onPhase?.("Instrumenting the newly added events");
|
|
1876
|
+
const prompt = [
|
|
1877
|
+
"You proposed these events as universe opportunities while reviewing this",
|
|
1878
|
+
"repo, and the user just APPROVED adding them to the plan. Instrument them",
|
|
1879
|
+
"now with track(), exactly like the plan's other events: place each at the",
|
|
1880
|
+
"real call site (your own rationale tells you where you saw it), attach the",
|
|
1881
|
+
"properties available in scope, and skip anything that turns out not to have",
|
|
1882
|
+
"a clear trigger after all. The SDK is already installed and initialized.",
|
|
1883
|
+
`Project root: ${repoPath}`,
|
|
1884
|
+
"",
|
|
1885
|
+
"----- APPROVED NEW EVENTS -----",
|
|
1886
|
+
...eventLines,
|
|
1887
|
+
"",
|
|
1888
|
+
"----- END -----"
|
|
1889
|
+
].join("\n");
|
|
1890
|
+
const pass = await runPass({
|
|
1891
|
+
prompt,
|
|
1892
|
+
systemPrompt: systemPrompt9,
|
|
1893
|
+
repoPath,
|
|
1894
|
+
model: config.model,
|
|
1895
|
+
effort: config.effort,
|
|
1896
|
+
maxTurns: 25,
|
|
1897
|
+
budgetUsd,
|
|
1898
|
+
progress
|
|
1899
|
+
});
|
|
1900
|
+
return { summary: pass.summary, costUsd: pass.costUsd, ran: true };
|
|
1901
|
+
}
|
|
1476
1902
|
function isLimitStop(err) {
|
|
1477
1903
|
const msg = err?.message ?? "";
|
|
1478
1904
|
return /max(imum)?[\s_-]*(turn|budget)|budget.*exceeded|number of turns/i.test(msg);
|
|
@@ -1582,102 +2008,6 @@ function short(s, max = 60) {
|
|
|
1582
2008
|
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
1583
2009
|
}
|
|
1584
2010
|
|
|
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
2011
|
// src/core/verify.ts
|
|
1682
2012
|
async function pollFirstEvent(config, session, opts = {}) {
|
|
1683
2013
|
if (config.offline) return { received: false };
|
|
@@ -1705,6 +2035,10 @@ async function pollFirstEvent(config, session, opts = {}) {
|
|
|
1705
2035
|
|
|
1706
2036
|
// src/core/postflight.ts
|
|
1707
2037
|
import { spawn as spawn2 } from "child_process";
|
|
2038
|
+
function verdictToVerified(verdict) {
|
|
2039
|
+
if (verdict.toolMissing || verdict.timedOut) return null;
|
|
2040
|
+
return verdict.ok;
|
|
2041
|
+
}
|
|
1708
2042
|
var MAX_OUTPUT = 4e3;
|
|
1709
2043
|
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
1710
2044
|
function runVerifyCommand(repoPath, command, opts = {}) {
|
|
@@ -1896,6 +2230,7 @@ Flutter is live today; ${theme.bright(
|
|
|
1896
2230
|
}
|
|
1897
2231
|
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
2232
|
spin.stop(stopLabel);
|
|
2233
|
+
const opportunities = await collectOpportunities(repoPath, manifest, checkpoint);
|
|
1899
2234
|
const files = await changedFiles(repoPath, checkpoint);
|
|
1900
2235
|
if (files.length) {
|
|
1901
2236
|
p.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
@@ -1920,6 +2255,7 @@ Flutter is live today; ${theme.bright(
|
|
|
1920
2255
|
const vspin = p.spinner();
|
|
1921
2256
|
vspin.start(`Verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
1922
2257
|
const verdict = await runVerifyCommand(repoPath, cmd);
|
|
2258
|
+
verified = verdictToVerified(verdict);
|
|
1923
2259
|
if (verdict.toolMissing) {
|
|
1924
2260
|
vspin.stop(
|
|
1925
2261
|
theme.warn("Couldn't verify automatically") + theme.muted(` \u2014 \`${cmd}\` isn't available here. Run it yourself before committing.`)
|
|
@@ -1929,10 +2265,8 @@ Flutter is live today; ${theme.bright(
|
|
|
1929
2265
|
theme.warn(`Verification timed out \u2014 \`${cmd}\``) + theme.muted(" \u2014 run it yourself before committing.")
|
|
1930
2266
|
);
|
|
1931
2267
|
} else if (verdict.ok) {
|
|
1932
|
-
verified = true;
|
|
1933
2268
|
vspin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
1934
2269
|
} else {
|
|
1935
|
-
verified = false;
|
|
1936
2270
|
vspin.stop(theme.alert(`Verification failed \u2014 ${cmd}`));
|
|
1937
2271
|
if (verdict.output) {
|
|
1938
2272
|
p.note(verdict.output, theme.warn("Verifier output"));
|
|
@@ -1948,15 +2282,69 @@ Flutter is live today; ${theme.bright(
|
|
|
1948
2282
|
}
|
|
1949
2283
|
}
|
|
1950
2284
|
}
|
|
1951
|
-
const
|
|
2285
|
+
const { acceptedEvents, instrumentationSnapshot } = await offerOpportunities({
|
|
1952
2286
|
repoPath,
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2287
|
+
config,
|
|
2288
|
+
session,
|
|
2289
|
+
playbook: chosen.playbook,
|
|
2290
|
+
manifest,
|
|
2291
|
+
opportunities,
|
|
2292
|
+
fingerprint,
|
|
2293
|
+
checkpoint,
|
|
2294
|
+
remainingBudgetUsd: config.budgetUsd - outcome.costUsd
|
|
2295
|
+
});
|
|
2296
|
+
if (instrumentationSnapshot && chosen.playbook.verifyCommand) {
|
|
2297
|
+
const cmd = chosen.playbook.verifyCommand;
|
|
2298
|
+
const preInstrumentationVerified = verified;
|
|
2299
|
+
const vspin = p.spinner();
|
|
2300
|
+
vspin.start(`Re-verifying after instrumenting the new events \u2014 ${theme.muted(cmd)}`);
|
|
2301
|
+
const verdict = await runVerifyCommand(repoPath, cmd);
|
|
2302
|
+
verified = verdictToVerified(verdict);
|
|
2303
|
+
if (verdict.toolMissing) {
|
|
2304
|
+
vspin.stop(
|
|
2305
|
+
theme.warn("Couldn't re-verify automatically") + theme.muted(` \u2014 \`${cmd}\` isn't available here. Run it yourself before committing.`)
|
|
2306
|
+
);
|
|
2307
|
+
} else if (verdict.timedOut) {
|
|
2308
|
+
vspin.stop(
|
|
2309
|
+
theme.warn(`Re-verification timed out \u2014 \`${cmd}\``) + theme.muted(" \u2014 run it yourself before committing.")
|
|
2310
|
+
);
|
|
2311
|
+
} else if (verdict.ok) {
|
|
2312
|
+
vspin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
2313
|
+
} else {
|
|
2314
|
+
vspin.stop(theme.alert(`Verification failed after instrumenting the new events \u2014 ${cmd}`));
|
|
2315
|
+
if (verdict.output) {
|
|
2316
|
+
p.note(verdict.output, theme.warn("Verifier output"));
|
|
2317
|
+
}
|
|
2318
|
+
const doRevert = await p.confirm({
|
|
2319
|
+
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.)",
|
|
2320
|
+
initialValue: true
|
|
2321
|
+
});
|
|
2322
|
+
if (!p.isCancel(doRevert) && doRevert) {
|
|
2323
|
+
if (await restoreToSnapshot(repoPath, checkpoint, instrumentationSnapshot)) {
|
|
2324
|
+
verified = preInstrumentationVerified;
|
|
2325
|
+
p.log.success(theme.success("Reverted the instrumentation edits."));
|
|
2326
|
+
} else {
|
|
2327
|
+
p.log.warn(
|
|
2328
|
+
theme.warn("Couldn't revert automatically \u2014 undo manually: ") + (revertHint(checkpoint) ?? "git reset --hard")
|
|
2329
|
+
);
|
|
2330
|
+
}
|
|
2331
|
+
} else {
|
|
2332
|
+
p.log.info(
|
|
2333
|
+
theme.muted("Left in your working tree \u2014 fix the verifier errors before committing.")
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
const filesForScan = acceptedEvents.length ? await changedFiles(repoPath, checkpoint) : files;
|
|
2339
|
+
const eventTypesToScan = [
|
|
2340
|
+
...manifest.events.map((e) => e.eventType),
|
|
2341
|
+
...acceptedEvents.map((e) => e.code)
|
|
2342
|
+
];
|
|
2343
|
+
const wiredMap = await scanWiredEvents(repoPath, filesForScan, eventTypesToScan);
|
|
2344
|
+
const reportEvents = eventTypesToScan.map((eventType) => ({
|
|
2345
|
+
event_type: eventType,
|
|
2346
|
+
status: wiredMap.has(eventType) ? "wired" : "skipped",
|
|
2347
|
+
file: wiredMap.get(eventType)
|
|
1960
2348
|
}));
|
|
1961
2349
|
await postRunReport(config, session, {
|
|
1962
2350
|
target: chosen.playbook.target.id,
|
|
@@ -1970,7 +2358,7 @@ Flutter is live today; ${theme.bright(
|
|
|
1970
2358
|
});
|
|
1971
2359
|
p.log.info(
|
|
1972
2360
|
theme.muted(
|
|
1973
|
-
`${wiredMap.size}/${
|
|
2361
|
+
`${wiredMap.size}/${eventTypesToScan.length} events wired \xB7 ${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ` + (verified === true ? "verified \xB7 " : verified === false ? "unverified \xB7 " : "") + `${Math.round(outcome.durationMs / 1e3)}s`
|
|
1974
2362
|
)
|
|
1975
2363
|
);
|
|
1976
2364
|
if (!config.offline) {
|
|
@@ -2005,6 +2393,153 @@ Flutter is live today; ${theme.bright(
|
|
|
2005
2393
|
p.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
|
|
2006
2394
|
return 0;
|
|
2007
2395
|
}
|
|
2396
|
+
var NO_ADDITIONS = { acceptedEvents: [], instrumentationSnapshot: null };
|
|
2397
|
+
async function offerOpportunities(opts) {
|
|
2398
|
+
const { opportunities, manifest, config, session } = opts;
|
|
2399
|
+
const total = opportunities.events.length + opportunities.interventions.length;
|
|
2400
|
+
if (total === 0) return NO_ADDITIONS;
|
|
2401
|
+
const describe = (kind, code, why) => `${theme.muted(`${kind}: `)}${theme.bright(code)}${why ? theme.muted(` \u2014 ${why}`) : ""}`;
|
|
2402
|
+
p.note(
|
|
2403
|
+
[
|
|
2404
|
+
...opportunities.events.map((e) => describe("event", e.code, e.rationale ?? e.description)),
|
|
2405
|
+
...opportunities.interventions.map(
|
|
2406
|
+
(i) => describe("intervention", i.code, i.rationale ?? i.description)
|
|
2407
|
+
)
|
|
2408
|
+
].join("\n"),
|
|
2409
|
+
theme.signal("Opportunities found beyond your onboarding plan")
|
|
2410
|
+
);
|
|
2411
|
+
if (config.offline) {
|
|
2412
|
+
p.log.info(theme.muted("Offline mode \u2014 proposals shown only, nothing submitted."));
|
|
2413
|
+
return NO_ADDITIONS;
|
|
2414
|
+
}
|
|
2415
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
2416
|
+
p.log.info(
|
|
2417
|
+
theme.muted("Non-interactive run \u2014 proposals were not submitted. Re-run interactively to add them.")
|
|
2418
|
+
);
|
|
2419
|
+
return NO_ADDITIONS;
|
|
2420
|
+
}
|
|
2421
|
+
const selection = await p.multiselect({
|
|
2422
|
+
message: "Add these to your Whisperr universe? (new interventions start paused)",
|
|
2423
|
+
options: [
|
|
2424
|
+
...opportunities.events.map((e) => ({
|
|
2425
|
+
value: `e:${e.code}`,
|
|
2426
|
+
label: `event \xB7 ${e.code}`,
|
|
2427
|
+
hint: e.description ?? e.rationale
|
|
2428
|
+
})),
|
|
2429
|
+
...opportunities.interventions.map((i) => ({
|
|
2430
|
+
value: `i:${i.code}`,
|
|
2431
|
+
label: `intervention \xB7 ${i.code}`,
|
|
2432
|
+
hint: i.description ?? i.rationale
|
|
2433
|
+
}))
|
|
2434
|
+
],
|
|
2435
|
+
initialValues: [
|
|
2436
|
+
...opportunities.events.map((e) => `e:${e.code}`),
|
|
2437
|
+
...opportunities.interventions.map((i) => `i:${i.code}`)
|
|
2438
|
+
],
|
|
2439
|
+
required: false
|
|
2440
|
+
});
|
|
2441
|
+
if (p.isCancel(selection) || selection.length === 0) {
|
|
2442
|
+
p.log.info(theme.muted("Skipped \u2014 your universe is unchanged."));
|
|
2443
|
+
return NO_ADDITIONS;
|
|
2444
|
+
}
|
|
2445
|
+
const chosenSet = new Set(selection);
|
|
2446
|
+
const knownInterventionCodes = new Set(
|
|
2447
|
+
manifest.events.flatMap((e) => e.interventions ?? []).map((i) => i.code)
|
|
2448
|
+
);
|
|
2449
|
+
const knownEventCodes = new Set(manifest.events.map((e) => e.eventType));
|
|
2450
|
+
const pickedEvents = opportunities.events.filter((e) => chosenSet.has(`e:${e.code}`));
|
|
2451
|
+
const pickedInterventions = opportunities.interventions.filter(
|
|
2452
|
+
(i) => chosenSet.has(`i:${i.code}`)
|
|
2453
|
+
);
|
|
2454
|
+
const pickedInterventionCodes = new Set(pickedInterventions.map((i) => i.code));
|
|
2455
|
+
const pickedEventCodes = new Set(pickedEvents.map((e) => e.code));
|
|
2456
|
+
const submitted = {
|
|
2457
|
+
events: pickedEvents.map((e) => ({
|
|
2458
|
+
...e,
|
|
2459
|
+
links: e.links?.filter(
|
|
2460
|
+
(l) => knownInterventionCodes.has(l.interventionCode) || pickedInterventionCodes.has(l.interventionCode)
|
|
2461
|
+
)
|
|
2462
|
+
})),
|
|
2463
|
+
interventions: pickedInterventions.map((i) => ({
|
|
2464
|
+
...i,
|
|
2465
|
+
links: i.links?.filter(
|
|
2466
|
+
(l) => knownEventCodes.has(l.eventCode) || pickedEventCodes.has(l.eventCode)
|
|
2467
|
+
)
|
|
2468
|
+
}))
|
|
2469
|
+
};
|
|
2470
|
+
let result;
|
|
2471
|
+
try {
|
|
2472
|
+
result = await withSpinner(
|
|
2473
|
+
"Adding to your universe",
|
|
2474
|
+
() => submitAdditions(config, session, opts.playbook.target.id, opts.fingerprint, submitted)
|
|
2475
|
+
);
|
|
2476
|
+
} catch (err) {
|
|
2477
|
+
p.log.warn(
|
|
2478
|
+
theme.warn("Couldn't add the proposals") + theme.muted(` \u2014 ${err.message}. Your universe is unchanged.`)
|
|
2479
|
+
);
|
|
2480
|
+
return NO_ADDITIONS;
|
|
2481
|
+
}
|
|
2482
|
+
const lines = result.outcomes.map((o) => {
|
|
2483
|
+
const mark = o.status === "applied" ? theme.success("\u2713") : o.status === "duplicate" ? theme.muted("=") : theme.warn("\u2717");
|
|
2484
|
+
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})`) : "";
|
|
2485
|
+
return `${mark} ${o.kind} ${theme.bright(o.code)}${detail}`;
|
|
2486
|
+
});
|
|
2487
|
+
lines.push(
|
|
2488
|
+
theme.muted(
|
|
2489
|
+
`${result.applied} added \xB7 ${result.duplicates} already present \xB7 ${result.invalid} rejected`
|
|
2490
|
+
)
|
|
2491
|
+
);
|
|
2492
|
+
if (submitted.interventions.length) {
|
|
2493
|
+
lines.push(theme.muted("New interventions start paused \u2014 activate them in your dashboard."));
|
|
2494
|
+
}
|
|
2495
|
+
if (result.policyRegen?.status === "pending") {
|
|
2496
|
+
lines.push(
|
|
2497
|
+
theme.muted("Runtime policy update queued \u2014 the additions go live once it completes.")
|
|
2498
|
+
);
|
|
2499
|
+
}
|
|
2500
|
+
p.note(lines.join("\n"), theme.signal("Universe updated"));
|
|
2501
|
+
if (result.policyRegen?.status === "failed") {
|
|
2502
|
+
p.log.warn(
|
|
2503
|
+
theme.warn("Live runtime NOT updated") + theme.muted(
|
|
2504
|
+
" \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."
|
|
2505
|
+
)
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
const appliedEventCodes = new Set(
|
|
2509
|
+
result.outcomes.filter((o) => o.kind === "event" && o.status === "applied").map((o) => o.code)
|
|
2510
|
+
);
|
|
2511
|
+
const acceptedEvents = pickedEvents.filter((e) => appliedEventCodes.has(e.code));
|
|
2512
|
+
if (!acceptedEvents.length) return NO_ADDITIONS;
|
|
2513
|
+
const prePass = await snapshotChanges(opts.repoPath, opts.checkpoint);
|
|
2514
|
+
const spin = p.spinner();
|
|
2515
|
+
spin.start(theme.bright("Instrumenting the newly added events"));
|
|
2516
|
+
try {
|
|
2517
|
+
const pass = await runAdditionsInstrumentationPass({
|
|
2518
|
+
repoPath: opts.repoPath,
|
|
2519
|
+
config,
|
|
2520
|
+
session,
|
|
2521
|
+
playbook: opts.playbook,
|
|
2522
|
+
acceptedEvents,
|
|
2523
|
+
budgetUsd: opts.remainingBudgetUsd
|
|
2524
|
+
});
|
|
2525
|
+
if (pass.ran) {
|
|
2526
|
+
spin.stop(theme.success("New events instrumented"));
|
|
2527
|
+
if (pass.summary.trim()) p.log.message(pass.summary.trim());
|
|
2528
|
+
} else {
|
|
2529
|
+
spin.stop(
|
|
2530
|
+
theme.warn("Skipped instrumenting the new events") + theme.muted(
|
|
2531
|
+
" \u2014 the spend limit was reached. They're in your universe; wire them on a future run."
|
|
2532
|
+
)
|
|
2533
|
+
);
|
|
2534
|
+
}
|
|
2535
|
+
} catch (err) {
|
|
2536
|
+
spin.stop(
|
|
2537
|
+
theme.warn("Couldn't instrument the new events") + theme.muted(` \u2014 ${err.message}. They're in your universe; wire them on a future run.`)
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
const edited = !snapshotsEqual(prePass, await snapshotChanges(opts.repoPath, opts.checkpoint));
|
|
2541
|
+
return { acceptedEvents, instrumentationSnapshot: edited ? prePass : null };
|
|
2542
|
+
}
|
|
2008
2543
|
async function maybeRevert(repoPath, checkpoint, reason) {
|
|
2009
2544
|
if (!checkpoint.isRepo) return false;
|
|
2010
2545
|
const files = await changedFiles(repoPath, checkpoint);
|
|
@@ -2105,7 +2640,7 @@ function summarizeManifest(m) {
|
|
|
2105
2640
|
// src/index.ts
|
|
2106
2641
|
var modulePath = fileURLToPath(import.meta.url);
|
|
2107
2642
|
function packageVersion() {
|
|
2108
|
-
const packagePath =
|
|
2643
|
+
const packagePath = join4(dirname2(modulePath), "..", "package.json");
|
|
2109
2644
|
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
2110
2645
|
return pkg.version ?? "0.0.0";
|
|
2111
2646
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whisperr/wizard",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Whisperr Wizard — one command to integrate the Whisperr SDK into your app. Authenticates with your onboarded account and uses an AI coding agent to wire up identify() and your business events automatically.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|