@whisperr/wizard 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -8
- package/dist/index.js +663 -733
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { fileURLToPath } from "url";
|
|
4
|
+
import { realpathSync } from "fs";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
6
|
|
|
8
7
|
// src/cli.ts
|
|
9
8
|
import { resolve as resolve2 } from "path";
|
|
@@ -13,7 +12,7 @@ import open2 from "open";
|
|
|
13
12
|
// src/core/config.ts
|
|
14
13
|
var DEFAULT_API_BASE = "https://api.whisperr.net";
|
|
15
14
|
var DEFAULT_MODEL = "claude-sonnet-5";
|
|
16
|
-
var DEFAULT_PLANNER_MODEL = "claude-opus-
|
|
15
|
+
var DEFAULT_PLANNER_MODEL = "claude-opus-5";
|
|
17
16
|
var DEFAULT_EFFORT = "high";
|
|
18
17
|
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
19
18
|
function resolveEffort() {
|
|
@@ -41,7 +40,10 @@ function resolveConfig(flags = {}) {
|
|
|
41
40
|
// cleanly and keeps whatever already landed. Real runs finish well under it.
|
|
42
41
|
budgetUsd: Number(process.env.WHISPERR_WIZARD_BUDGET_USD) || 25,
|
|
43
42
|
directAnthropicKey,
|
|
44
|
-
offline
|
|
43
|
+
offline,
|
|
44
|
+
reviewPlan: flags.reviewPlan ?? false,
|
|
45
|
+
proposeOnly: flags.proposeOnly ?? false,
|
|
46
|
+
strictReview: flags.strictReview ?? false
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
|
|
@@ -1124,7 +1126,18 @@ function mockManifest(appId, config) {
|
|
|
1124
1126
|
appName: "Acme (dev)",
|
|
1125
1127
|
ingestionApiKey: "wrk_offline_demo_key_do_not_use",
|
|
1126
1128
|
ingestionBaseUrl: config.apiBaseUrl,
|
|
1127
|
-
|
|
1129
|
+
// Same labelled shape whisperr-go renders from the app's onboarding
|
|
1130
|
+
// business context, so an offline run exercises the real prompt.
|
|
1131
|
+
businessContext: [
|
|
1132
|
+
"Product: Acme (Acme Inc)",
|
|
1133
|
+
"What it does: B2C subscription app; a free trial converts to a paid monthly plan",
|
|
1134
|
+
"Industry: Software / Productivity",
|
|
1135
|
+
"Who the paying end user is: individual consumers on the trial-to-paid path",
|
|
1136
|
+
"How it charges: monthly subscription",
|
|
1137
|
+
"Activation moment: first project created after signup",
|
|
1138
|
+
"Churn-risk signals onboarding identified: trial ending unused; first failed payment",
|
|
1139
|
+
"Healthy-usage signals: weekly active project edits"
|
|
1140
|
+
].join("\n"),
|
|
1128
1141
|
identify: {
|
|
1129
1142
|
traits: [
|
|
1130
1143
|
{ name: "plan", description: "free | trial | pro" },
|
|
@@ -1181,8 +1194,8 @@ import {
|
|
|
1181
1194
|
// src/core/git.ts
|
|
1182
1195
|
import { spawn } from "child_process";
|
|
1183
1196
|
import { createHash } from "crypto";
|
|
1184
|
-
import {
|
|
1185
|
-
import { basename,
|
|
1197
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1198
|
+
import { basename, join as join2 } from "path";
|
|
1186
1199
|
async function takeCheckpoint(repoPath) {
|
|
1187
1200
|
const isRepo = (await run(repoPath, ["rev-parse", "--is-inside-work-tree"])).ok;
|
|
1188
1201
|
if (!isRepo) return { isRepo: false };
|
|
@@ -1213,21 +1226,6 @@ function revertHint(checkpoint) {
|
|
|
1213
1226
|
if (!checkpoint.isRepo || !checkpoint.baseRef) return void 0;
|
|
1214
1227
|
return `git restore . && git clean -fd (back to ${checkpoint.baseRef.slice(0, 7)})`;
|
|
1215
1228
|
}
|
|
1216
|
-
async function wasTrackedAtCheckpoint(repoPath, checkpoint, file) {
|
|
1217
|
-
if (!checkpoint.isRepo) return false;
|
|
1218
|
-
if (checkpoint.baseRef) {
|
|
1219
|
-
const res2 = await run(repoPath, [
|
|
1220
|
-
"ls-tree",
|
|
1221
|
-
"--name-only",
|
|
1222
|
-
checkpoint.baseRef,
|
|
1223
|
-
"--",
|
|
1224
|
-
file
|
|
1225
|
-
]);
|
|
1226
|
-
return res2.ok && res2.stdout.trim() !== "";
|
|
1227
|
-
}
|
|
1228
|
-
const res = await run(repoPath, ["ls-files", "--", file]);
|
|
1229
|
-
return res.ok && res.stdout.trim() !== "";
|
|
1230
|
-
}
|
|
1231
1229
|
async function isWorkingTreeClean(repoPath) {
|
|
1232
1230
|
const status = await run(repoPath, ["status", "--porcelain"]);
|
|
1233
1231
|
return status.ok && status.stdout.trim() === "";
|
|
@@ -1243,54 +1241,6 @@ async function revertToCheckpoint(repoPath, checkpoint) {
|
|
|
1243
1241
|
const clean = await run(repoPath, ["clean", "-fd"]);
|
|
1244
1242
|
return restore.ok && clean.ok;
|
|
1245
1243
|
}
|
|
1246
|
-
async function snapshotChanges(repoPath, checkpoint) {
|
|
1247
|
-
const snapshot = /* @__PURE__ */ new Map();
|
|
1248
|
-
for (const file of await changedFiles(repoPath, checkpoint)) {
|
|
1249
|
-
try {
|
|
1250
|
-
snapshot.set(file, await readFile2(join2(repoPath, file)));
|
|
1251
|
-
} catch {
|
|
1252
|
-
snapshot.set(file, null);
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
return snapshot;
|
|
1256
|
-
}
|
|
1257
|
-
function snapshotsEqual(a, b) {
|
|
1258
|
-
if (a.size !== b.size) return false;
|
|
1259
|
-
for (const [file, content] of a) {
|
|
1260
|
-
const other = b.get(file);
|
|
1261
|
-
if (other === void 0) return false;
|
|
1262
|
-
if (content === null || other === null) {
|
|
1263
|
-
if (content !== other) return false;
|
|
1264
|
-
} else if (!content.equals(other)) {
|
|
1265
|
-
return false;
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
return true;
|
|
1269
|
-
}
|
|
1270
|
-
async function restoreToSnapshot(repoPath, checkpoint, snapshot) {
|
|
1271
|
-
try {
|
|
1272
|
-
const current = await changedFiles(repoPath, checkpoint);
|
|
1273
|
-
for (const file of /* @__PURE__ */ new Set([...current, ...snapshot.keys()])) {
|
|
1274
|
-
const recorded = snapshot.get(file);
|
|
1275
|
-
const path = join2(repoPath, file);
|
|
1276
|
-
if (recorded === void 0) {
|
|
1277
|
-
if (checkpoint.baseRef) {
|
|
1278
|
-
const co = await run(repoPath, ["checkout", checkpoint.baseRef, "--", file]);
|
|
1279
|
-
if (co.ok) continue;
|
|
1280
|
-
}
|
|
1281
|
-
await rm(path, { force: true });
|
|
1282
|
-
} else if (recorded === null) {
|
|
1283
|
-
await rm(path, { force: true });
|
|
1284
|
-
} else {
|
|
1285
|
-
await mkdir(dirname(path), { recursive: true });
|
|
1286
|
-
await writeFile(path, recorded);
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
return true;
|
|
1290
|
-
} catch {
|
|
1291
|
-
return false;
|
|
1292
|
-
}
|
|
1293
|
-
}
|
|
1294
1244
|
async function repoFingerprint(repoPath) {
|
|
1295
1245
|
const remote = await run(repoPath, ["remote", "get-url", "origin"]);
|
|
1296
1246
|
const seed = remote.ok && remote.stdout.trim() ? normalizeRemote(remote.stdout.trim()) : `dir:${basename(repoPath)}`;
|
|
@@ -1352,248 +1302,6 @@ function run(cwd, args) {
|
|
|
1352
1302
|
});
|
|
1353
1303
|
}
|
|
1354
1304
|
|
|
1355
|
-
// src/core/opportunities.ts
|
|
1356
|
-
import { readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
1357
|
-
import { join as join3 } from "path";
|
|
1358
|
-
var OPPORTUNITIES_FILE = "whisperr-opportunities.json";
|
|
1359
|
-
function normalizeCode(value) {
|
|
1360
|
-
return value.trim().toLowerCase().replace(/[ \-./]/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
1361
|
-
}
|
|
1362
|
-
async function collectOpportunities(repoPath, manifest, checkpoint) {
|
|
1363
|
-
const path = join3(repoPath, OPPORTUNITIES_FILE);
|
|
1364
|
-
if (await wasTrackedAtCheckpoint(repoPath, checkpoint, OPPORTUNITIES_FILE)) {
|
|
1365
|
-
return { events: [], interventions: [] };
|
|
1366
|
-
}
|
|
1367
|
-
let rawText;
|
|
1368
|
-
try {
|
|
1369
|
-
rawText = await readFile3(path, "utf8");
|
|
1370
|
-
} catch {
|
|
1371
|
-
return { events: [], interventions: [] };
|
|
1372
|
-
}
|
|
1373
|
-
await rm2(path, { force: true }).catch(() => {
|
|
1374
|
-
});
|
|
1375
|
-
let raw;
|
|
1376
|
-
try {
|
|
1377
|
-
raw = JSON.parse(rawText);
|
|
1378
|
-
} catch {
|
|
1379
|
-
return { events: [], interventions: [] };
|
|
1380
|
-
}
|
|
1381
|
-
return sanitizeOpportunities(raw, manifest);
|
|
1382
|
-
}
|
|
1383
|
-
function sanitizeOpportunities(raw, manifest) {
|
|
1384
|
-
const out = { events: [], interventions: [] };
|
|
1385
|
-
if (typeof raw !== "object" || raw === null) return out;
|
|
1386
|
-
const doc = raw;
|
|
1387
|
-
const knownEvents = new Set(manifest.events.map((e) => normalizeCode(e.eventType)));
|
|
1388
|
-
const knownInterventions = /* @__PURE__ */ new Set([
|
|
1389
|
-
...manifest.events.flatMap((e) => e.interventions ?? []).map((i) => normalizeCode(i.code)),
|
|
1390
|
-
...(manifest.interventions ?? []).map((i) => normalizeCode(i.code))
|
|
1391
|
-
]);
|
|
1392
|
-
const seenEvents = /* @__PURE__ */ new Set();
|
|
1393
|
-
for (const entry of asArray(doc.events)) {
|
|
1394
|
-
const ev = coerceEvent(entry);
|
|
1395
|
-
if (!ev) continue;
|
|
1396
|
-
if (knownEvents.has(ev.code) || seenEvents.has(ev.code)) continue;
|
|
1397
|
-
seenEvents.add(ev.code);
|
|
1398
|
-
out.events.push(ev);
|
|
1399
|
-
}
|
|
1400
|
-
const seenInterventions = /* @__PURE__ */ new Set();
|
|
1401
|
-
for (const entry of asArray(doc.interventions)) {
|
|
1402
|
-
const iv = coerceIntervention(entry);
|
|
1403
|
-
if (!iv) continue;
|
|
1404
|
-
if (knownInterventions.has(iv.code) || seenInterventions.has(iv.code)) continue;
|
|
1405
|
-
seenInterventions.add(iv.code);
|
|
1406
|
-
out.interventions.push(iv);
|
|
1407
|
-
}
|
|
1408
|
-
return out;
|
|
1409
|
-
}
|
|
1410
|
-
var SUBMIT_TIMEOUT_MS = 3e4;
|
|
1411
|
-
async function submitAdditions(config, session, target9, repoFingerprint2, opportunities) {
|
|
1412
|
-
const res = await fetch(`${config.apiBaseUrl}/wizard/universe/additions`, {
|
|
1413
|
-
method: "POST",
|
|
1414
|
-
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
1415
|
-
headers: {
|
|
1416
|
-
"Content-Type": "application/json",
|
|
1417
|
-
Authorization: `Bearer ${session.token}`
|
|
1418
|
-
},
|
|
1419
|
-
body: JSON.stringify({
|
|
1420
|
-
target: target9,
|
|
1421
|
-
repoFingerprint: repoFingerprint2,
|
|
1422
|
-
events: opportunities.events,
|
|
1423
|
-
interventions: opportunities.interventions
|
|
1424
|
-
})
|
|
1425
|
-
});
|
|
1426
|
-
if (!res.ok) {
|
|
1427
|
-
const body = await res.text().catch(() => "");
|
|
1428
|
-
throw new Error(
|
|
1429
|
-
`Submitting universe additions failed (${res.status})${body ? `: ${body.slice(0, 300)}` : ""}`
|
|
1430
|
-
);
|
|
1431
|
-
}
|
|
1432
|
-
return await res.json();
|
|
1433
|
-
}
|
|
1434
|
-
async function submitSuggestions(config, session, target9, repoFingerprint2, opportunities) {
|
|
1435
|
-
const res = await fetch(`${config.apiBaseUrl}/wizard/universe/suggestions`, {
|
|
1436
|
-
method: "POST",
|
|
1437
|
-
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
1438
|
-
headers: {
|
|
1439
|
-
"Content-Type": "application/json",
|
|
1440
|
-
Authorization: `Bearer ${session.token}`
|
|
1441
|
-
},
|
|
1442
|
-
body: JSON.stringify({
|
|
1443
|
-
target: target9,
|
|
1444
|
-
repoFingerprint: repoFingerprint2,
|
|
1445
|
-
events: opportunities.events,
|
|
1446
|
-
interventions: opportunities.interventions
|
|
1447
|
-
})
|
|
1448
|
-
});
|
|
1449
|
-
if (res.status === 404) return null;
|
|
1450
|
-
if (!res.ok) {
|
|
1451
|
-
const body = await res.text().catch(() => "");
|
|
1452
|
-
throw new Error(
|
|
1453
|
-
`Submitting universe suggestions failed (${res.status})${body ? `: ${body.slice(0, 300)}` : ""}`
|
|
1454
|
-
);
|
|
1455
|
-
}
|
|
1456
|
-
return await res.json();
|
|
1457
|
-
}
|
|
1458
|
-
async function fetchSuggestions(config, session, statuses) {
|
|
1459
|
-
if (config.offline) return [];
|
|
1460
|
-
try {
|
|
1461
|
-
const statusQuery = statuses.map(encodeURIComponent).join(",");
|
|
1462
|
-
const url = `${config.apiBaseUrl}/wizard/universe/suggestions${statusQuery ? `?status=${statusQuery}` : ""}`;
|
|
1463
|
-
const res = await fetch(url, {
|
|
1464
|
-
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
1465
|
-
headers: { Authorization: `Bearer ${session.token}` }
|
|
1466
|
-
});
|
|
1467
|
-
if (!res.ok) return [];
|
|
1468
|
-
const body = await res.json();
|
|
1469
|
-
return Array.isArray(body) && body.every(isSuggestionRecord) ? body : [];
|
|
1470
|
-
} catch {
|
|
1471
|
-
return [];
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
function isSuggestionRecord(value) {
|
|
1475
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1476
|
-
const record = value;
|
|
1477
|
-
return typeof record.id === "string" && (record.kind === "event" || record.kind === "intervention") && typeof record.code === "string" && (record.status === "proposed" || record.status === "approved" || record.status === "rejected" || record.status === "integrated") && typeof record.payload === "object" && record.payload !== null;
|
|
1478
|
-
}
|
|
1479
|
-
async function markSuggestionIntegrated(config, session, id, target9, repoFingerprint2) {
|
|
1480
|
-
if (config.offline) return false;
|
|
1481
|
-
try {
|
|
1482
|
-
const res = await fetch(
|
|
1483
|
-
`${config.apiBaseUrl}/wizard/universe/suggestions/${encodeURIComponent(id)}/integrated`,
|
|
1484
|
-
{
|
|
1485
|
-
method: "POST",
|
|
1486
|
-
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
1487
|
-
headers: {
|
|
1488
|
-
"Content-Type": "application/json",
|
|
1489
|
-
Authorization: `Bearer ${session.token}`
|
|
1490
|
-
},
|
|
1491
|
-
body: JSON.stringify({ target: target9, repoFingerprint: repoFingerprint2 })
|
|
1492
|
-
}
|
|
1493
|
-
);
|
|
1494
|
-
return res.status === 200;
|
|
1495
|
-
} catch {
|
|
1496
|
-
return false;
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
function asArray(value) {
|
|
1500
|
-
return Array.isArray(value) ? value : [];
|
|
1501
|
-
}
|
|
1502
|
-
function asString(value) {
|
|
1503
|
-
if (typeof value !== "string") return void 0;
|
|
1504
|
-
const cleaned = value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
1505
|
-
return cleaned ? cleaned : void 0;
|
|
1506
|
-
}
|
|
1507
|
-
function asConfidence(value) {
|
|
1508
|
-
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
1509
|
-
return Math.min(1, Math.max(0, value));
|
|
1510
|
-
}
|
|
1511
|
-
function asWeight(value) {
|
|
1512
|
-
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
1513
|
-
if (value < 0 || value > 1) return void 0;
|
|
1514
|
-
return value;
|
|
1515
|
-
}
|
|
1516
|
-
function coerceEvent(entry) {
|
|
1517
|
-
if (typeof entry !== "object" || entry === null) return null;
|
|
1518
|
-
const e = entry;
|
|
1519
|
-
const code = normalizeCode(asString(e.code) ?? "");
|
|
1520
|
-
if (!code) return null;
|
|
1521
|
-
const properties = [];
|
|
1522
|
-
for (const prop of asArray(e.properties)) {
|
|
1523
|
-
if (typeof prop === "string") {
|
|
1524
|
-
if (prop.trim()) properties.push({ name: prop.trim() });
|
|
1525
|
-
continue;
|
|
1526
|
-
}
|
|
1527
|
-
if (typeof prop !== "object" || prop === null) continue;
|
|
1528
|
-
const pr = prop;
|
|
1529
|
-
const name = asString(pr.name);
|
|
1530
|
-
if (!name) continue;
|
|
1531
|
-
const desc = asString(pr.description);
|
|
1532
|
-
properties.push({
|
|
1533
|
-
name,
|
|
1534
|
-
...desc ? { description: desc } : {},
|
|
1535
|
-
...pr.required === true ? { required: true } : {}
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
const links = [];
|
|
1539
|
-
for (const link of asArray(e.links)) {
|
|
1540
|
-
if (typeof link !== "object" || link === null) continue;
|
|
1541
|
-
const l = link;
|
|
1542
|
-
const interventionCode = normalizeCode(asString(l.interventionCode) ?? "");
|
|
1543
|
-
if (!interventionCode) continue;
|
|
1544
|
-
const weight = asWeight(l.weight);
|
|
1545
|
-
links.push({ interventionCode, ...weight !== void 0 ? { weight } : {} });
|
|
1546
|
-
}
|
|
1547
|
-
const side = normalizeSide(asString(e.side));
|
|
1548
|
-
return {
|
|
1549
|
-
code,
|
|
1550
|
-
label: asString(e.label),
|
|
1551
|
-
description: asString(e.description),
|
|
1552
|
-
...side ? { side } : {},
|
|
1553
|
-
rationale: asString(e.rationale),
|
|
1554
|
-
expectedEffect: asString(e.expectedEffect),
|
|
1555
|
-
confidence: asConfidence(e.confidence),
|
|
1556
|
-
...properties.length ? { properties } : {},
|
|
1557
|
-
...links.length ? { links } : {}
|
|
1558
|
-
};
|
|
1559
|
-
}
|
|
1560
|
-
function coerceIntervention(entry) {
|
|
1561
|
-
if (typeof entry !== "object" || entry === null) return null;
|
|
1562
|
-
const i = entry;
|
|
1563
|
-
const code = normalizeCode(asString(i.code) ?? "");
|
|
1564
|
-
if (!code) return null;
|
|
1565
|
-
const links = [];
|
|
1566
|
-
for (const link of asArray(i.links)) {
|
|
1567
|
-
if (typeof link !== "object" || link === null) continue;
|
|
1568
|
-
const l = link;
|
|
1569
|
-
const eventCode = normalizeCode(asString(l.eventCode) ?? "");
|
|
1570
|
-
if (!eventCode) continue;
|
|
1571
|
-
const weight = asWeight(l.weight);
|
|
1572
|
-
links.push({ eventCode, ...weight !== void 0 ? { weight } : {} });
|
|
1573
|
-
}
|
|
1574
|
-
return {
|
|
1575
|
-
code,
|
|
1576
|
-
label: asString(i.label),
|
|
1577
|
-
description: asString(i.description),
|
|
1578
|
-
rationale: asString(i.rationale),
|
|
1579
|
-
expectedEffect: asString(i.expectedEffect),
|
|
1580
|
-
confidence: asConfidence(i.confidence),
|
|
1581
|
-
...links.length ? { links } : {}
|
|
1582
|
-
};
|
|
1583
|
-
}
|
|
1584
|
-
function normalizeSide(value) {
|
|
1585
|
-
switch (value?.toLowerCase()) {
|
|
1586
|
-
case "frontend":
|
|
1587
|
-
return "frontend";
|
|
1588
|
-
case "backend":
|
|
1589
|
-
return "backend";
|
|
1590
|
-
case "either":
|
|
1591
|
-
return "either";
|
|
1592
|
-
default:
|
|
1593
|
-
return void 0;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
1305
|
// src/core/playbooks/shared-prompt.ts
|
|
1598
1306
|
var BASE_WIZARD_PROMPT = `
|
|
1599
1307
|
You are the Whisperr Wizard \u2014 an expert integration agent running INSIDE a
|
|
@@ -1770,55 +1478,51 @@ function renderEventsBrief(m) {
|
|
|
1770
1478
|
}
|
|
1771
1479
|
return lines.join("\n");
|
|
1772
1480
|
}
|
|
1773
|
-
function renderOpportunitiesBrief(m
|
|
1481
|
+
function renderOpportunitiesBrief(m) {
|
|
1774
1482
|
const eventCodes = m.events.map((e) => e.eventType);
|
|
1775
1483
|
const interventionCodes = [
|
|
1776
|
-
|
|
1484
|
+
.../* @__PURE__ */ new Set([
|
|
1485
|
+
...m.events.flatMap((e) => e.interventions ?? []).map((i) => i.code),
|
|
1486
|
+
...(m.interventions ?? []).map((i) => i.code)
|
|
1487
|
+
])
|
|
1777
1488
|
];
|
|
1778
1489
|
return [
|
|
1779
|
-
"
|
|
1780
|
-
"
|
|
1781
|
-
"
|
|
1782
|
-
"
|
|
1783
|
-
"
|
|
1784
|
-
"
|
|
1785
|
-
"
|
|
1786
|
-
"NOT cover? Record those as PROPOSALS. Do NOT add track() calls for them.",
|
|
1787
|
-
`Write a single JSON file at the repo root named ${opportunitiesFile}:`,
|
|
1490
|
+
"Rules for `opportunities` \u2014 what this app's plan is MISSING:",
|
|
1491
|
+
"You are reading this app's real lifecycle anyway. Sweep it deliberately:",
|
|
1492
|
+
"walk each surface the plan should care about \u2014 signup and auth,",
|
|
1493
|
+
"billing/payments (renewals, failures, upgrades, downgrades, cancellation),",
|
|
1494
|
+
"the core engagement loop, support/feedback/refund flows, sharing/invites,",
|
|
1495
|
+
"and any expiry or dormancy mechanics you see \u2014 and for each ask: does a",
|
|
1496
|
+
"churn-relevant moment happen here that the plan does NOT cover?",
|
|
1788
1497
|
"",
|
|
1789
|
-
"
|
|
1790
|
-
'
|
|
1791
|
-
' "code": "snake_case_event", "label": "...", "description": "...",',
|
|
1498
|
+
"Entry shapes:",
|
|
1499
|
+
' events: [{"code": "snake_case_event", "label": "...", "description": "...",',
|
|
1792
1500
|
' "side": "frontend|backend|either", "rationale": "what in the code shows this",',
|
|
1793
1501
|
' "expectedEffect": "one sentence: what improves if this is adopted",',
|
|
1794
1502
|
' "confidence": 0.0-1.0,',
|
|
1795
1503
|
' "properties": [{"name": "...", "description": "...", "required": false}],',
|
|
1796
|
-
' "links": [{"interventionCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
1797
|
-
"
|
|
1798
|
-
'
|
|
1799
|
-
' "
|
|
1800
|
-
' "rationale": "...", "confidence": 0.0-1.0,',
|
|
1801
|
-
' "expectedEffect": "one sentence: what improves if this is adopted",',
|
|
1802
|
-
' "links": [{"eventCode": "existing_or_proposed", "weight": 0.0-1.0}]',
|
|
1803
|
-
" }]",
|
|
1804
|
-
"}",
|
|
1504
|
+
' "links": [{"interventionCode": "existing_or_proposed", "weight": 0.0-1.0}]}]',
|
|
1505
|
+
' interventions: [{"code": "snake_case_strategy", "label": "...", "description": "...",',
|
|
1506
|
+
' "rationale": "...", "confidence": 0.0-1.0, "expectedEffect": "...",',
|
|
1507
|
+
' "links": [{"eventCode": "existing_or_proposed", "weight": 0.0-1.0}]}]',
|
|
1805
1508
|
"",
|
|
1806
|
-
"Rules:",
|
|
1807
1509
|
`- The plan already covers these events: ${eventCodes.join(", ") || "(none)"}.`,
|
|
1808
1510
|
` And these interventions: ${interventionCodes.join(", ") || "(none)"}.`,
|
|
1809
1511
|
" Propose ONLY what is genuinely missing \u2014 never re-propose or rename these.",
|
|
1810
|
-
"-
|
|
1811
|
-
"
|
|
1812
|
-
"
|
|
1813
|
-
"
|
|
1512
|
+
"- These go straight into the customer's universe, and the events you propose",
|
|
1513
|
+
" get instrumented in this same run. That raises the bar on evidence, not the",
|
|
1514
|
+
" volume: every proposal needs concrete code backing in its rationale, with",
|
|
1515
|
+
" the file/flow named. VERIFY before you write it \u2014 re-open the file you are",
|
|
1516
|
+
" citing and confirm the flow exists as you describe. Drop anything you",
|
|
1517
|
+
" cannot point at real code; a false proposal costs more trust than a missed one.",
|
|
1518
|
+
"- Say where you saw it in `rationale` precisely enough to wire from later \u2014",
|
|
1519
|
+
" it is the only placement note the wiring pass will have for these.",
|
|
1814
1520
|
"- Be thorough, not shy: a feature-rich product typically yields 3-6 solid",
|
|
1815
|
-
" event proposals and 1-3 interventions.
|
|
1816
|
-
"
|
|
1817
|
-
"
|
|
1818
|
-
"- Every proposal should include expectedEffect: one sentence explaining what adoption should improve.",
|
|
1521
|
+
" event proposals and 1-3 interventions. Empty is only right when the plan",
|
|
1522
|
+
" genuinely covers the product's lifecycle \u2014 never because you stopped early.",
|
|
1523
|
+
"- Every proposal should include expectedEffect: one sentence on what adoption improves.",
|
|
1819
1524
|
"- Link each proposed event to the intervention(s) it should feed (existing",
|
|
1820
|
-
" codes or ones you propose
|
|
1821
|
-
"- This file is metadata for the wizard, not app code \u2014 write it and move on."
|
|
1525
|
+
" codes or ones you propose alongside it)."
|
|
1822
1526
|
].join("\n");
|
|
1823
1527
|
}
|
|
1824
1528
|
function coverageNote(coverage) {
|
|
@@ -2140,36 +1844,56 @@ function tokenizeShellSegment(segment) {
|
|
|
2140
1844
|
|
|
2141
1845
|
// src/core/agent.ts
|
|
2142
1846
|
async function runIntegrationAgent(opts) {
|
|
2143
|
-
const {
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
1847
|
+
const {
|
|
1848
|
+
repoPath,
|
|
1849
|
+
config,
|
|
1850
|
+
session,
|
|
1851
|
+
playbook,
|
|
1852
|
+
manifest,
|
|
1853
|
+
progress,
|
|
1854
|
+
onPlanReady,
|
|
1855
|
+
onOpportunitiesReady
|
|
1856
|
+
} = opts;
|
|
1857
|
+
applyModelAuthEnv(config, session);
|
|
1858
|
+
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
1859
|
+
const started = Date.now();
|
|
1860
|
+
let costUsd = 0;
|
|
2148
1861
|
const summaries = [];
|
|
2149
1862
|
let repoMap = "";
|
|
2150
1863
|
let eventOutcomes = [];
|
|
2151
1864
|
const phaseTimings = [];
|
|
2152
1865
|
const BUDGET_FLOOR = 0.25;
|
|
1866
|
+
let planning = { repoMap: "", plan: null, opportunities: emptyOpportunities() };
|
|
2153
1867
|
if (config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2154
1868
|
try {
|
|
2155
|
-
const
|
|
2156
|
-
prompt:
|
|
1869
|
+
const pass = await runTimedPass("Planning your integration", {
|
|
1870
|
+
prompt: renderPlanningPrompt(repoPath, playbook, manifest),
|
|
2157
1871
|
systemPrompt: systemPrompt9,
|
|
2158
1872
|
repoPath,
|
|
2159
1873
|
model: config.plannerModel,
|
|
2160
1874
|
effort: "high",
|
|
2161
|
-
maxTurns:
|
|
2162
|
-
budgetUsd: Math.min(config.budgetUsd - costUsd, config.budgetUsd * 0.
|
|
1875
|
+
maxTurns: 60,
|
|
1876
|
+
budgetUsd: Math.min(config.budgetUsd - costUsd, config.budgetUsd * 0.3),
|
|
2163
1877
|
allowedTools: READ_ONLY_TOOLS,
|
|
2164
1878
|
progress
|
|
2165
1879
|
}, phaseTimings);
|
|
2166
|
-
costUsd +=
|
|
2167
|
-
|
|
1880
|
+
costUsd += pass.costUsd;
|
|
1881
|
+
planning = parsePlanningResult(pass.summary);
|
|
2168
1882
|
} catch (err) {
|
|
2169
|
-
debugNote(`
|
|
2170
|
-
repoMap = "";
|
|
1883
|
+
debugNote(`planning pass failed: ${err.message}`);
|
|
2171
1884
|
}
|
|
2172
1885
|
}
|
|
1886
|
+
repoMap = planning.repoMap;
|
|
1887
|
+
let scopedPlan = null;
|
|
1888
|
+
if (planning.plan) {
|
|
1889
|
+
scopedPlan = planForManifest(planning.plan, manifest);
|
|
1890
|
+
const reviewedPlan = await onPlanReady?.(scopedPlan);
|
|
1891
|
+
if (reviewedPlan !== void 0 && reviewedPlan !== null) {
|
|
1892
|
+
scopedPlan = planForManifest(reviewedPlan, manifest);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
let appliedOpportunityEvents = [];
|
|
1896
|
+
const opportunitiesProposed = planning.opportunities.events.length + planning.opportunities.interventions.length;
|
|
2173
1897
|
const corePrompt = [
|
|
2174
1898
|
`Integrate the Whisperr ${playbook.target.displayName} SDK \u2014 CORE SETUP ONLY.`,
|
|
2175
1899
|
`Project root: ${repoPath}`,
|
|
@@ -2212,6 +1936,13 @@ async function runIntegrationAgent(opts) {
|
|
|
2212
1936
|
costUsd += core.costUsd;
|
|
2213
1937
|
if (core.summary) summaries.push(`Core setup:
|
|
2214
1938
|
${core.summary}`);
|
|
1939
|
+
if (onOpportunitiesReady && opportunitiesProposed > 0 && core.ok) {
|
|
1940
|
+
try {
|
|
1941
|
+
appliedOpportunityEvents = await onOpportunitiesReady(planning.opportunities);
|
|
1942
|
+
} catch (err) {
|
|
1943
|
+
debugNote(`universe additions failed: ${err.message}`);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
2215
1946
|
let eventsComplete = true;
|
|
2216
1947
|
if (manifest.events.length > 0 && config.budgetUsd - costUsd <= BUDGET_FLOOR) {
|
|
2217
1948
|
eventsComplete = false;
|
|
@@ -2224,20 +1955,7 @@ ${core.summary}`);
|
|
|
2224
1955
|
"Events: skipped \u2014 the spend limit was reached during core setup. Re-run with a higher WHISPERR_WIZARD_BUDGET_USD to instrument events."
|
|
2225
1956
|
);
|
|
2226
1957
|
} else if (manifest.events.length > 0) {
|
|
2227
|
-
|
|
2228
|
-
prompt: renderEventPlanPrompt(repoPath, playbook, manifest, repoMap),
|
|
2229
|
-
systemPrompt: systemPrompt9,
|
|
2230
|
-
repoPath,
|
|
2231
|
-
model: config.plannerModel,
|
|
2232
|
-
effort: "high",
|
|
2233
|
-
maxTurns: 40,
|
|
2234
|
-
budgetUsd: config.budgetUsd - costUsd,
|
|
2235
|
-
allowedTools: READ_ONLY_TOOLS,
|
|
2236
|
-
progress
|
|
2237
|
-
}, phaseTimings);
|
|
2238
|
-
costUsd += planPass.costUsd;
|
|
2239
|
-
const plan = parseEventPlan(planPass.summary);
|
|
2240
|
-
if (!plan) {
|
|
1958
|
+
if (!scopedPlan) {
|
|
2241
1959
|
debugNote("event plan parse failed; falling back to direct event wiring");
|
|
2242
1960
|
progress?.onActivity?.("Event plan was not parseable; falling back to direct wiring");
|
|
2243
1961
|
const direct = await runDirectEventsPass({
|
|
@@ -2257,21 +1975,23 @@ ${core.summary}`);
|
|
|
2257
1975
|
if (direct.pass.summary) summaries.push(`Events:
|
|
2258
1976
|
${direct.pass.summary}`);
|
|
2259
1977
|
} else {
|
|
2260
|
-
let scopedPlan = planForManifest(plan, manifest);
|
|
2261
|
-
const reviewedPlan = await onPlanReady?.(scopedPlan);
|
|
2262
|
-
if (reviewedPlan !== void 0 && reviewedPlan !== null) {
|
|
2263
|
-
scopedPlan = planForManifest(reviewedPlan, manifest);
|
|
2264
|
-
}
|
|
2265
1978
|
const placeEntries = scopedPlan.filter((entry) => entry.decision === "place");
|
|
2266
1979
|
const unsureEntries = scopedPlan.filter((entry) => entry.decision === "unsure");
|
|
2267
1980
|
let wire;
|
|
2268
|
-
|
|
1981
|
+
const hasWorkToWire = placeEntries.length > 0 || appliedOpportunityEvents.length > 0;
|
|
1982
|
+
if (hasWorkToWire && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2269
1983
|
wire = await runTimedPass("Instrumenting planned events", {
|
|
2270
|
-
prompt: renderEventWirePrompt(
|
|
1984
|
+
prompt: renderEventWirePrompt(
|
|
1985
|
+
repoPath,
|
|
1986
|
+
playbook,
|
|
1987
|
+
repoMap,
|
|
1988
|
+
placeEntries,
|
|
1989
|
+
appliedOpportunityEvents
|
|
1990
|
+
),
|
|
2271
1991
|
systemPrompt: systemPrompt9,
|
|
2272
1992
|
repoPath,
|
|
2273
1993
|
model: config.model,
|
|
2274
|
-
effort: "
|
|
1994
|
+
effort: "medium",
|
|
2275
1995
|
maxTurns: config.maxTurns,
|
|
2276
1996
|
budgetUsd: config.budgetUsd - costUsd,
|
|
2277
1997
|
progress
|
|
@@ -2279,7 +1999,7 @@ ${direct.pass.summary}`);
|
|
|
2279
1999
|
costUsd += wire.costUsd;
|
|
2280
2000
|
if (wire.summary) summaries.push(`Events:
|
|
2281
2001
|
${wire.summary}`);
|
|
2282
|
-
} else if (!
|
|
2002
|
+
} else if (!hasWorkToWire) {
|
|
2283
2003
|
summaries.push("Events: no events had a confident placement in this surface.");
|
|
2284
2004
|
}
|
|
2285
2005
|
let wired = await scanManifestEvents(repoPath, manifest);
|
|
@@ -2303,7 +2023,7 @@ ${followUp.summary}`);
|
|
|
2303
2023
|
wired = await scanManifestEvents(repoPath, manifest);
|
|
2304
2024
|
}
|
|
2305
2025
|
eventOutcomes = buildEventOutcomes(manifest, scopedPlan, wired);
|
|
2306
|
-
eventsComplete = !
|
|
2026
|
+
eventsComplete = !(wire?.maxedOut ?? false) && !(followUp?.maxedOut ?? false);
|
|
2307
2027
|
}
|
|
2308
2028
|
}
|
|
2309
2029
|
if (core.ok && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
@@ -2313,7 +2033,7 @@ ${followUp.summary}`);
|
|
|
2313
2033
|
`Project root: ${repoPath}`,
|
|
2314
2034
|
"",
|
|
2315
2035
|
...renderRepoMapSection(repoMap),
|
|
2316
|
-
"Run `git diff` and read the
|
|
2036
|
+
"Run `git diff` and read ONLY the code immediately around each change.",
|
|
2317
2037
|
"For every identify() and track() call, verify \u2014 and FIX in place:",
|
|
2318
2038
|
"1. Subject \u2014 keys to the END USER, never an admin/staff/operator/seller.",
|
|
2319
2039
|
" identify() sits on the SAME surface as account_created (same person).",
|
|
@@ -2327,21 +2047,19 @@ ${followUp.summary}`);
|
|
|
2327
2047
|
" / payment_source / amount / plan). Add the ones you missed.",
|
|
2328
2048
|
"4. Coverage \u2014 every gateway/callback path that should emit an event does;",
|
|
2329
2049
|
" no recurring or secondary path left as a dead zone.",
|
|
2330
|
-
"
|
|
2331
|
-
"
|
|
2332
|
-
"",
|
|
2333
|
-
renderOpportunitiesBrief(manifest, OPPORTUNITIES_FILE),
|
|
2050
|
+
"Work from the diff. Do NOT re-explore the repository, do NOT propose new",
|
|
2051
|
+
"events, and change ONLY what is genuinely wrong or incomplete. Be surgical.",
|
|
2334
2052
|
"",
|
|
2335
2053
|
"End with a one-line, plain-text note of what you corrected (or 'No",
|
|
2336
|
-
"corrections needed.')
|
|
2054
|
+
"corrections needed.')."
|
|
2337
2055
|
].join("\n");
|
|
2338
2056
|
const review = await runTimedPass("Reviewing & correcting placements", {
|
|
2339
2057
|
prompt: reviewPrompt,
|
|
2340
2058
|
systemPrompt: systemPrompt9,
|
|
2341
2059
|
repoPath,
|
|
2342
|
-
model: config.plannerModel,
|
|
2060
|
+
model: config.strictReview ? config.plannerModel : config.model,
|
|
2343
2061
|
effort: "medium",
|
|
2344
|
-
maxTurns: 40,
|
|
2062
|
+
maxTurns: config.strictReview ? 40 : 15,
|
|
2345
2063
|
budgetUsd: config.budgetUsd - costUsd,
|
|
2346
2064
|
progress
|
|
2347
2065
|
}, phaseTimings);
|
|
@@ -2364,57 +2082,11 @@ ${review.summary}`);
|
|
|
2364
2082
|
coreOk: core.ok,
|
|
2365
2083
|
eventsComplete,
|
|
2366
2084
|
eventOutcomes,
|
|
2367
|
-
repoMap: repoMap || void 0
|
|
2085
|
+
repoMap: repoMap || void 0,
|
|
2086
|
+
opportunitiesProposed,
|
|
2087
|
+
opportunitiesApplied: appliedOpportunityEvents.length
|
|
2368
2088
|
};
|
|
2369
2089
|
}
|
|
2370
|
-
async function runAdditionsInstrumentationPass(opts) {
|
|
2371
|
-
const { repoPath, config, session, playbook, acceptedEvents, budgetUsd, repoMap, progress } = opts;
|
|
2372
|
-
if (!acceptedEvents.length || budgetUsd <= 0) {
|
|
2373
|
-
return { summary: "", costUsd: 0, ran: false };
|
|
2374
|
-
}
|
|
2375
|
-
applyModelAuthEnv(config, session);
|
|
2376
|
-
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
2377
|
-
const eventLines = [];
|
|
2378
|
-
for (const e of acceptedEvents) {
|
|
2379
|
-
eventLines.push("");
|
|
2380
|
-
eventLines.push(`\u25A0 ${e.code}${e.label ? ` (${e.label})` : ""}`);
|
|
2381
|
-
if (e.description) eventLines.push(` ${e.description}`);
|
|
2382
|
-
if (e.rationale) eventLines.push(` where: ${e.rationale}`);
|
|
2383
|
-
if (e.properties?.length) {
|
|
2384
|
-
eventLines.push(" properties to capture if available:");
|
|
2385
|
-
for (const prop of e.properties) {
|
|
2386
|
-
eventLines.push(` \xB7 ${prop.name}${prop.required ? " (required)" : ""}${prop.description ? ` \u2014 ${prop.description}` : ""}`);
|
|
2387
|
-
}
|
|
2388
|
-
}
|
|
2389
|
-
}
|
|
2390
|
-
progress?.onPhase?.("Instrumenting the newly added events");
|
|
2391
|
-
const prompt = [
|
|
2392
|
-
"You proposed these events as universe opportunities while reviewing this",
|
|
2393
|
-
"repo, and the user just APPROVED adding them to the plan. Instrument them",
|
|
2394
|
-
"now with track(), exactly like the plan's other events: place each at the",
|
|
2395
|
-
"real call site (your own rationale tells you where you saw it), attach the",
|
|
2396
|
-
"properties available in scope, and skip anything that turns out not to have",
|
|
2397
|
-
"a clear trigger after all. The SDK is already installed and initialized.",
|
|
2398
|
-
`Project root: ${repoPath}`,
|
|
2399
|
-
"",
|
|
2400
|
-
...renderRepoMapSection(repoMap),
|
|
2401
|
-
"----- APPROVED NEW EVENTS -----",
|
|
2402
|
-
...eventLines,
|
|
2403
|
-
"",
|
|
2404
|
-
"----- END -----"
|
|
2405
|
-
].join("\n");
|
|
2406
|
-
const pass = await runPass({
|
|
2407
|
-
prompt,
|
|
2408
|
-
systemPrompt: systemPrompt9,
|
|
2409
|
-
repoPath,
|
|
2410
|
-
model: config.model,
|
|
2411
|
-
effort: config.effort,
|
|
2412
|
-
maxTurns: 25,
|
|
2413
|
-
budgetUsd,
|
|
2414
|
-
progress
|
|
2415
|
-
});
|
|
2416
|
-
return { summary: pass.summary, costUsd: pass.costUsd, ran: true };
|
|
2417
|
-
}
|
|
2418
2090
|
async function runRepairPass(opts) {
|
|
2419
2091
|
const {
|
|
2420
2092
|
repoPath,
|
|
@@ -2458,11 +2130,64 @@ async function runRepairPass(opts) {
|
|
|
2458
2130
|
}
|
|
2459
2131
|
var READ_ONLY_TOOLS = ["Read", "Glob", "Grep"];
|
|
2460
2132
|
var FULL_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
2133
|
+
var PLANNING_JSON_FENCE = /```json\s*([\s\S]*?)```/;
|
|
2134
|
+
function scrubTerminalText(value) {
|
|
2135
|
+
return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
2136
|
+
}
|
|
2137
|
+
function emptyOpportunities() {
|
|
2138
|
+
return { events: [], interventions: [] };
|
|
2139
|
+
}
|
|
2140
|
+
function parsePlanningResult(text) {
|
|
2141
|
+
const fenced = PLANNING_JSON_FENCE.exec(text);
|
|
2142
|
+
const payload = parseJsonObject(fenced?.[1]) ?? parseJsonObject(sliceLastJsonObject(text));
|
|
2143
|
+
const mapText = fenced ? text.slice(0, fenced.index) : text;
|
|
2144
|
+
const repoMap = sliceRepoMap(stripMapMarkers(mapText));
|
|
2145
|
+
if (!payload) {
|
|
2146
|
+
return { repoMap, plan: parseEventPlan(text), opportunities: emptyOpportunities() };
|
|
2147
|
+
}
|
|
2148
|
+
return {
|
|
2149
|
+
repoMap,
|
|
2150
|
+
plan: Array.isArray(payload.plan) ? coerceEventPlan(payload.plan) : null,
|
|
2151
|
+
opportunities: coerceOpportunities(payload.opportunities)
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
function parseJsonObject(raw) {
|
|
2155
|
+
if (!raw?.trim()) return null;
|
|
2156
|
+
try {
|
|
2157
|
+
const parsed = JSON.parse(raw);
|
|
2158
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2159
|
+
const obj = parsed;
|
|
2160
|
+
if ("plan" in obj || "opportunities" in obj) return obj;
|
|
2161
|
+
}
|
|
2162
|
+
} catch {
|
|
2163
|
+
}
|
|
2164
|
+
return null;
|
|
2165
|
+
}
|
|
2166
|
+
function sliceLastJsonObject(text) {
|
|
2167
|
+
const start = text.lastIndexOf("{\n");
|
|
2168
|
+
const from = start >= 0 ? start : text.indexOf("{");
|
|
2169
|
+
if (from < 0) return null;
|
|
2170
|
+
return sliceBalanced(text, from, "{", "}");
|
|
2171
|
+
}
|
|
2172
|
+
function stripMapMarkers(text) {
|
|
2173
|
+
return text.replace(/^-+\s*REPO MAP\s*-+$/gim, "").replace(/^-+\s*END REPO MAP\s*-+$/gim, "").trim();
|
|
2174
|
+
}
|
|
2175
|
+
function coerceOpportunities(raw) {
|
|
2176
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return emptyOpportunities();
|
|
2177
|
+
const doc = raw;
|
|
2178
|
+
return {
|
|
2179
|
+
events: Array.isArray(doc.events) ? doc.events : [],
|
|
2180
|
+
interventions: Array.isArray(doc.interventions) ? doc.interventions : []
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2461
2183
|
function parseEventPlan(text) {
|
|
2462
2184
|
const parsed = parseFirstJsonArray(text);
|
|
2463
2185
|
if (!Array.isArray(parsed)) return null;
|
|
2186
|
+
return coerceEventPlan(parsed);
|
|
2187
|
+
}
|
|
2188
|
+
function coerceEventPlan(items) {
|
|
2464
2189
|
const entries = [];
|
|
2465
|
-
for (const item of
|
|
2190
|
+
for (const item of items) {
|
|
2466
2191
|
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2467
2192
|
const obj = item;
|
|
2468
2193
|
const event = typeof obj.event === "string" ? obj.event.trim() : "";
|
|
@@ -2473,7 +2198,10 @@ function parseEventPlan(text) {
|
|
|
2473
2198
|
const entry = {
|
|
2474
2199
|
event,
|
|
2475
2200
|
decision,
|
|
2476
|
-
|
|
2201
|
+
// Model-authored text that ends up in the customer's terminal and in
|
|
2202
|
+
// the run report — scrub it like every other string we take from the
|
|
2203
|
+
// model, so it cannot restyle or spoof the surrounding UI.
|
|
2204
|
+
reason: typeof obj.reason === "string" ? scrubTerminalText(obj.reason) : ""
|
|
2477
2205
|
};
|
|
2478
2206
|
if (typeof obj.file === "string" && obj.file.trim()) {
|
|
2479
2207
|
entry.file = obj.file.trim();
|
|
@@ -2491,7 +2219,7 @@ function parseEventPlan(text) {
|
|
|
2491
2219
|
function parseFirstJsonArray(text) {
|
|
2492
2220
|
const start = text.indexOf("[");
|
|
2493
2221
|
if (start < 0) return null;
|
|
2494
|
-
const json =
|
|
2222
|
+
const json = sliceBalanced(text, start, "[", "]");
|
|
2495
2223
|
if (!json) return null;
|
|
2496
2224
|
try {
|
|
2497
2225
|
return JSON.parse(json);
|
|
@@ -2499,7 +2227,7 @@ function parseFirstJsonArray(text) {
|
|
|
2499
2227
|
return null;
|
|
2500
2228
|
}
|
|
2501
2229
|
}
|
|
2502
|
-
function
|
|
2230
|
+
function sliceBalanced(text, start, open3, close) {
|
|
2503
2231
|
let depth = 0;
|
|
2504
2232
|
let inString = false;
|
|
2505
2233
|
let escaped = false;
|
|
@@ -2517,31 +2245,15 @@ function sliceJsonArray(text, start) {
|
|
|
2517
2245
|
}
|
|
2518
2246
|
if (ch === '"') {
|
|
2519
2247
|
inString = true;
|
|
2520
|
-
} else if (ch ===
|
|
2248
|
+
} else if (ch === open3) {
|
|
2521
2249
|
depth++;
|
|
2522
|
-
} else if (ch ===
|
|
2250
|
+
} else if (ch === close) {
|
|
2523
2251
|
depth--;
|
|
2524
2252
|
if (depth === 0) return text.slice(start, i + 1);
|
|
2525
2253
|
}
|
|
2526
2254
|
}
|
|
2527
2255
|
return null;
|
|
2528
2256
|
}
|
|
2529
|
-
function renderRepoMapPrompt(repoPath) {
|
|
2530
|
-
return [
|
|
2531
|
-
"Map this repository for a Whisperr SDK integration. This is READ-ONLY.",
|
|
2532
|
-
`Project root: ${repoPath}`,
|
|
2533
|
-
"",
|
|
2534
|
-
"Produce a concise structured repo map as final text with these exact sections:",
|
|
2535
|
-
"- framework + entry points",
|
|
2536
|
-
"- auth flows enumerated: END-USER vs admin/staff, with precise file paths",
|
|
2537
|
-
"- billing/payment/webhook surfaces",
|
|
2538
|
-
"- analytics/tracking wrapper if any",
|
|
2539
|
-
"- state management",
|
|
2540
|
-
"- directory guide: which dirs hold pages/controllers/services",
|
|
2541
|
-
"",
|
|
2542
|
-
"This map guides event placement. List file paths precisely. No prose beyond the map."
|
|
2543
|
-
].join("\n");
|
|
2544
|
-
}
|
|
2545
2257
|
function sliceRepoMap(map) {
|
|
2546
2258
|
return map.trim().slice(0, 8e3);
|
|
2547
2259
|
}
|
|
@@ -2550,30 +2262,46 @@ function renderRepoMapSection(repoMap) {
|
|
|
2550
2262
|
if (!map) return [];
|
|
2551
2263
|
return ["----- REPO MAP -----", map, "----- END REPO MAP -----", ""];
|
|
2552
2264
|
}
|
|
2553
|
-
function
|
|
2265
|
+
function renderPlanningPrompt(repoPath, playbook, manifest) {
|
|
2554
2266
|
return [
|
|
2555
|
-
`Plan Whisperr ${playbook.target.displayName}
|
|
2267
|
+
`Plan the Whisperr ${playbook.target.displayName} integration for this repo.`,
|
|
2268
|
+
"This pass is READ-ONLY \u2014 do not edit anything. You are deciding what the",
|
|
2269
|
+
"editing passes will do, so be precise about files and anchors.",
|
|
2556
2270
|
`Project root: ${repoPath}`,
|
|
2557
2271
|
"",
|
|
2558
|
-
|
|
2559
|
-
"Use the repo map and the events brief to decide where each event belongs.",
|
|
2560
|
-
"Return ONLY a JSON code block containing an array with this shape:",
|
|
2561
|
-
'[{"event":"event_name","decision":"place|skip|unsure","file":"path","anchor":"function/route/handler","properties":["prop"],"reason":"short reason"}]',
|
|
2272
|
+
"Produce your answer in TWO parts, in this order.",
|
|
2562
2273
|
"",
|
|
2563
|
-
"
|
|
2274
|
+
"PART 1 \u2014 the repo map, as plain text under these exact sections:",
|
|
2275
|
+
"- framework + entry points",
|
|
2276
|
+
"- auth flows enumerated: END-USER vs admin/staff, with precise file paths",
|
|
2277
|
+
"- billing/payment/webhook surfaces",
|
|
2278
|
+
"- analytics/tracking wrapper if any",
|
|
2279
|
+
"- state management",
|
|
2280
|
+
"- directory guide: which dirs hold pages/controllers/services",
|
|
2281
|
+
"List file paths precisely. No prose beyond the map.",
|
|
2282
|
+
"",
|
|
2283
|
+
"PART 2 \u2014 a single ```json fenced block, and nothing after it:",
|
|
2284
|
+
"{",
|
|
2285
|
+
' "plan": [{"event":"event_name","decision":"place|skip|unsure","file":"path",',
|
|
2286
|
+
' "anchor":"function/route/handler","properties":["prop"],"reason":"short reason"}],',
|
|
2287
|
+
' "opportunities": {"events": [], "interventions": []}',
|
|
2288
|
+
"}",
|
|
2289
|
+
"",
|
|
2290
|
+
"Rules for `plan` \u2014 one entry per event in the brief below:",
|
|
2564
2291
|
"- decision=place only when there is a concrete end-user call site in this repo.",
|
|
2565
2292
|
"- decision=skip when the event clearly lives on another surface or does not exist here.",
|
|
2566
2293
|
"- decision=unsure when there is a plausible surface but the exact anchor is not proven.",
|
|
2567
2294
|
"- For place decisions, include file and anchor precisely.",
|
|
2568
2295
|
"- properties should list only properties likely available at that anchor.",
|
|
2569
|
-
"
|
|
2296
|
+
"",
|
|
2297
|
+
renderOpportunitiesBrief(manifest),
|
|
2570
2298
|
"",
|
|
2571
2299
|
"----- EVENTS -----",
|
|
2572
2300
|
renderEventsBrief(manifest),
|
|
2573
2301
|
"----- END EVENTS -----"
|
|
2574
2302
|
].join("\n");
|
|
2575
2303
|
}
|
|
2576
|
-
function renderEventWirePrompt(repoPath, playbook, repoMap, entries) {
|
|
2304
|
+
function renderEventWirePrompt(repoPath, playbook, repoMap, entries, addedEvents = []) {
|
|
2577
2305
|
return [
|
|
2578
2306
|
`The Whisperr ${playbook.target.displayName} SDK is installed and initialized.`,
|
|
2579
2307
|
"Wire only the planned event placements below with track().",
|
|
@@ -2586,9 +2314,39 @@ function renderEventWirePrompt(repoPath, playbook, repoMap, entries) {
|
|
|
2586
2314
|
"",
|
|
2587
2315
|
"----- EVENT WIRING PLAN -----",
|
|
2588
2316
|
...renderPlanEntryLines(entries),
|
|
2589
|
-
"----- END EVENT WIRING PLAN -----"
|
|
2317
|
+
"----- END EVENT WIRING PLAN -----",
|
|
2318
|
+
// Events the planner proposed and the universe accepted this run. They have
|
|
2319
|
+
// no plan entry (they weren't in the manifest when the plan was written), so
|
|
2320
|
+
// the rationale carries the placement — the planner already saw the site.
|
|
2321
|
+
...addedEvents.length ? [
|
|
2322
|
+
"",
|
|
2323
|
+
"----- NEWLY ADDED EVENTS -----",
|
|
2324
|
+
"You proposed these while planning and they are now part of this app's",
|
|
2325
|
+
"universe. Wire them the same way \u2014 your own rationale says where you",
|
|
2326
|
+
"saw each one. Skip any that turn out not to have a clear trigger.",
|
|
2327
|
+
...renderAddedEventLines(addedEvents),
|
|
2328
|
+
"----- END NEWLY ADDED EVENTS -----"
|
|
2329
|
+
] : []
|
|
2590
2330
|
].join("\n");
|
|
2591
2331
|
}
|
|
2332
|
+
function renderAddedEventLines(events) {
|
|
2333
|
+
const lines = [];
|
|
2334
|
+
for (const event of events) {
|
|
2335
|
+
lines.push("");
|
|
2336
|
+
lines.push(`\u25A0 ${event.code}${event.label ? ` (${event.label})` : ""}`);
|
|
2337
|
+
if (event.description) lines.push(` ${event.description}`);
|
|
2338
|
+
if (event.rationale) lines.push(` where: ${event.rationale}`);
|
|
2339
|
+
if (event.properties?.length) {
|
|
2340
|
+
lines.push(" properties to capture if available:");
|
|
2341
|
+
for (const prop of event.properties) {
|
|
2342
|
+
lines.push(
|
|
2343
|
+
` \xB7 ${prop.name}${prop.required ? " (required)" : ""}${prop.description ? ` \u2014 ${prop.description}` : ""}`
|
|
2344
|
+
);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
return lines;
|
|
2349
|
+
}
|
|
2592
2350
|
function renderEventReconcilePrompt(repoPath, playbook, repoMap, entries) {
|
|
2593
2351
|
return [
|
|
2594
2352
|
`One targeted follow-up for Whisperr ${playbook.target.displayName} events.`,
|
|
@@ -2933,6 +2691,224 @@ function short(s, max = 60) {
|
|
|
2933
2691
|
return s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
2934
2692
|
}
|
|
2935
2693
|
|
|
2694
|
+
// src/core/opportunities.ts
|
|
2695
|
+
function normalizeCode(value) {
|
|
2696
|
+
return value.trim().toLowerCase().replace(/[ \-./]/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
2697
|
+
}
|
|
2698
|
+
function sanitizeOpportunities(raw, manifest) {
|
|
2699
|
+
const out = { events: [], interventions: [] };
|
|
2700
|
+
if (typeof raw !== "object" || raw === null) return out;
|
|
2701
|
+
const doc = raw;
|
|
2702
|
+
const knownEvents = new Set(manifest.events.map((e) => normalizeCode(e.eventType)));
|
|
2703
|
+
const knownInterventions = /* @__PURE__ */ new Set([
|
|
2704
|
+
...manifest.events.flatMap((e) => e.interventions ?? []).map((i) => normalizeCode(i.code)),
|
|
2705
|
+
...(manifest.interventions ?? []).map((i) => normalizeCode(i.code))
|
|
2706
|
+
]);
|
|
2707
|
+
const seenEvents = /* @__PURE__ */ new Set();
|
|
2708
|
+
for (const entry of asArray(doc.events)) {
|
|
2709
|
+
const ev = coerceEvent(entry);
|
|
2710
|
+
if (!ev) continue;
|
|
2711
|
+
if (knownEvents.has(ev.code) || seenEvents.has(ev.code)) continue;
|
|
2712
|
+
seenEvents.add(ev.code);
|
|
2713
|
+
out.events.push(ev);
|
|
2714
|
+
}
|
|
2715
|
+
const seenInterventions = /* @__PURE__ */ new Set();
|
|
2716
|
+
for (const entry of asArray(doc.interventions)) {
|
|
2717
|
+
const iv = coerceIntervention(entry);
|
|
2718
|
+
if (!iv) continue;
|
|
2719
|
+
if (knownInterventions.has(iv.code) || seenInterventions.has(iv.code)) continue;
|
|
2720
|
+
seenInterventions.add(iv.code);
|
|
2721
|
+
out.interventions.push(iv);
|
|
2722
|
+
}
|
|
2723
|
+
return out;
|
|
2724
|
+
}
|
|
2725
|
+
var SUBMIT_TIMEOUT_MS = 3e4;
|
|
2726
|
+
async function submitAdditions(config, session, target9, repoFingerprint2, opportunities) {
|
|
2727
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/universe/additions`, {
|
|
2728
|
+
method: "POST",
|
|
2729
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
2730
|
+
headers: {
|
|
2731
|
+
"Content-Type": "application/json",
|
|
2732
|
+
Authorization: `Bearer ${session.token}`
|
|
2733
|
+
},
|
|
2734
|
+
body: JSON.stringify({
|
|
2735
|
+
target: target9,
|
|
2736
|
+
repoFingerprint: repoFingerprint2,
|
|
2737
|
+
events: opportunities.events,
|
|
2738
|
+
interventions: opportunities.interventions
|
|
2739
|
+
})
|
|
2740
|
+
});
|
|
2741
|
+
if (!res.ok) {
|
|
2742
|
+
const body = await res.text().catch(() => "");
|
|
2743
|
+
throw new Error(
|
|
2744
|
+
`Submitting universe additions failed (${res.status})${body ? `: ${body.slice(0, 300)}` : ""}`
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
return await res.json();
|
|
2748
|
+
}
|
|
2749
|
+
async function submitSuggestions(config, session, target9, repoFingerprint2, opportunities) {
|
|
2750
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/universe/suggestions`, {
|
|
2751
|
+
method: "POST",
|
|
2752
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
2753
|
+
headers: {
|
|
2754
|
+
"Content-Type": "application/json",
|
|
2755
|
+
Authorization: `Bearer ${session.token}`
|
|
2756
|
+
},
|
|
2757
|
+
body: JSON.stringify({
|
|
2758
|
+
target: target9,
|
|
2759
|
+
repoFingerprint: repoFingerprint2,
|
|
2760
|
+
events: opportunities.events,
|
|
2761
|
+
interventions: opportunities.interventions
|
|
2762
|
+
})
|
|
2763
|
+
});
|
|
2764
|
+
if (res.status === 404) return null;
|
|
2765
|
+
if (!res.ok) {
|
|
2766
|
+
const body = await res.text().catch(() => "");
|
|
2767
|
+
throw new Error(
|
|
2768
|
+
`Submitting universe suggestions failed (${res.status})${body ? `: ${body.slice(0, 300)}` : ""}`
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
return await res.json();
|
|
2772
|
+
}
|
|
2773
|
+
async function fetchSuggestions(config, session, statuses) {
|
|
2774
|
+
if (config.offline) return [];
|
|
2775
|
+
try {
|
|
2776
|
+
const statusQuery = statuses.map(encodeURIComponent).join(",");
|
|
2777
|
+
const url = `${config.apiBaseUrl}/wizard/universe/suggestions${statusQuery ? `?status=${statusQuery}` : ""}`;
|
|
2778
|
+
const res = await fetch(url, {
|
|
2779
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
2780
|
+
headers: { Authorization: `Bearer ${session.token}` }
|
|
2781
|
+
});
|
|
2782
|
+
if (!res.ok) return [];
|
|
2783
|
+
const body = await res.json();
|
|
2784
|
+
return Array.isArray(body) && body.every(isSuggestionRecord) ? body : [];
|
|
2785
|
+
} catch {
|
|
2786
|
+
return [];
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
function isSuggestionRecord(value) {
|
|
2790
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2791
|
+
const record = value;
|
|
2792
|
+
return typeof record.id === "string" && (record.kind === "event" || record.kind === "intervention") && typeof record.code === "string" && (record.status === "proposed" || record.status === "approved" || record.status === "rejected" || record.status === "integrated") && typeof record.payload === "object" && record.payload !== null;
|
|
2793
|
+
}
|
|
2794
|
+
async function markSuggestionIntegrated(config, session, id, target9, repoFingerprint2) {
|
|
2795
|
+
if (config.offline) return false;
|
|
2796
|
+
try {
|
|
2797
|
+
const res = await fetch(
|
|
2798
|
+
`${config.apiBaseUrl}/wizard/universe/suggestions/${encodeURIComponent(id)}/integrated`,
|
|
2799
|
+
{
|
|
2800
|
+
method: "POST",
|
|
2801
|
+
signal: AbortSignal.timeout(SUBMIT_TIMEOUT_MS),
|
|
2802
|
+
headers: {
|
|
2803
|
+
"Content-Type": "application/json",
|
|
2804
|
+
Authorization: `Bearer ${session.token}`
|
|
2805
|
+
},
|
|
2806
|
+
body: JSON.stringify({ target: target9, repoFingerprint: repoFingerprint2 })
|
|
2807
|
+
}
|
|
2808
|
+
);
|
|
2809
|
+
return res.status === 200;
|
|
2810
|
+
} catch {
|
|
2811
|
+
return false;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
function asArray(value) {
|
|
2815
|
+
return Array.isArray(value) ? value : [];
|
|
2816
|
+
}
|
|
2817
|
+
function asString(value) {
|
|
2818
|
+
if (typeof value !== "string") return void 0;
|
|
2819
|
+
const cleaned = value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
2820
|
+
return cleaned ? cleaned : void 0;
|
|
2821
|
+
}
|
|
2822
|
+
function asConfidence(value) {
|
|
2823
|
+
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
2824
|
+
return Math.min(1, Math.max(0, value));
|
|
2825
|
+
}
|
|
2826
|
+
function asWeight(value) {
|
|
2827
|
+
if (typeof value !== "number" || Number.isNaN(value)) return void 0;
|
|
2828
|
+
if (value < 0 || value > 1) return void 0;
|
|
2829
|
+
return value;
|
|
2830
|
+
}
|
|
2831
|
+
function coerceEvent(entry) {
|
|
2832
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
2833
|
+
const e = entry;
|
|
2834
|
+
const code = normalizeCode(asString(e.code) ?? "");
|
|
2835
|
+
if (!code) return null;
|
|
2836
|
+
const properties = [];
|
|
2837
|
+
for (const prop of asArray(e.properties)) {
|
|
2838
|
+
if (typeof prop === "string") {
|
|
2839
|
+
if (prop.trim()) properties.push({ name: prop.trim() });
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
if (typeof prop !== "object" || prop === null) continue;
|
|
2843
|
+
const pr = prop;
|
|
2844
|
+
const name = asString(pr.name);
|
|
2845
|
+
if (!name) continue;
|
|
2846
|
+
const desc = asString(pr.description);
|
|
2847
|
+
properties.push({
|
|
2848
|
+
name,
|
|
2849
|
+
...desc ? { description: desc } : {},
|
|
2850
|
+
...pr.required === true ? { required: true } : {}
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
const links = [];
|
|
2854
|
+
for (const link of asArray(e.links)) {
|
|
2855
|
+
if (typeof link !== "object" || link === null) continue;
|
|
2856
|
+
const l = link;
|
|
2857
|
+
const interventionCode = normalizeCode(asString(l.interventionCode) ?? "");
|
|
2858
|
+
if (!interventionCode) continue;
|
|
2859
|
+
const weight = asWeight(l.weight);
|
|
2860
|
+
links.push({ interventionCode, ...weight !== void 0 ? { weight } : {} });
|
|
2861
|
+
}
|
|
2862
|
+
const side = normalizeSide(asString(e.side));
|
|
2863
|
+
return {
|
|
2864
|
+
code,
|
|
2865
|
+
label: asString(e.label),
|
|
2866
|
+
description: asString(e.description),
|
|
2867
|
+
...side ? { side } : {},
|
|
2868
|
+
rationale: asString(e.rationale),
|
|
2869
|
+
expectedEffect: asString(e.expectedEffect),
|
|
2870
|
+
confidence: asConfidence(e.confidence),
|
|
2871
|
+
...properties.length ? { properties } : {},
|
|
2872
|
+
...links.length ? { links } : {}
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
function coerceIntervention(entry) {
|
|
2876
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
2877
|
+
const i = entry;
|
|
2878
|
+
const code = normalizeCode(asString(i.code) ?? "");
|
|
2879
|
+
if (!code) return null;
|
|
2880
|
+
const links = [];
|
|
2881
|
+
for (const link of asArray(i.links)) {
|
|
2882
|
+
if (typeof link !== "object" || link === null) continue;
|
|
2883
|
+
const l = link;
|
|
2884
|
+
const eventCode = normalizeCode(asString(l.eventCode) ?? "");
|
|
2885
|
+
if (!eventCode) continue;
|
|
2886
|
+
const weight = asWeight(l.weight);
|
|
2887
|
+
links.push({ eventCode, ...weight !== void 0 ? { weight } : {} });
|
|
2888
|
+
}
|
|
2889
|
+
return {
|
|
2890
|
+
code,
|
|
2891
|
+
label: asString(i.label),
|
|
2892
|
+
description: asString(i.description),
|
|
2893
|
+
rationale: asString(i.rationale),
|
|
2894
|
+
expectedEffect: asString(i.expectedEffect),
|
|
2895
|
+
confidence: asConfidence(i.confidence),
|
|
2896
|
+
...links.length ? { links } : {}
|
|
2897
|
+
};
|
|
2898
|
+
}
|
|
2899
|
+
function normalizeSide(value) {
|
|
2900
|
+
switch (value?.toLowerCase()) {
|
|
2901
|
+
case "frontend":
|
|
2902
|
+
return "frontend";
|
|
2903
|
+
case "backend":
|
|
2904
|
+
return "backend";
|
|
2905
|
+
case "either":
|
|
2906
|
+
return "either";
|
|
2907
|
+
default:
|
|
2908
|
+
return void 0;
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2936
2912
|
// src/core/verify.ts
|
|
2937
2913
|
async function pollFirstEvent(config, session, opts = {}) {
|
|
2938
2914
|
if (config.offline) return { received: false };
|
|
@@ -3053,6 +3029,25 @@ function scrubTerminalControls(value) {
|
|
|
3053
3029
|
return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
3054
3030
|
}
|
|
3055
3031
|
|
|
3032
|
+
// src/core/version.ts
|
|
3033
|
+
import { readFileSync } from "fs";
|
|
3034
|
+
import { dirname as dirname2, join as join3, parse } from "path";
|
|
3035
|
+
import { fileURLToPath } from "url";
|
|
3036
|
+
var PACKAGE_NAME = "@whisperr/wizard";
|
|
3037
|
+
function packageVersion() {
|
|
3038
|
+
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
3039
|
+
const { root } = parse(dir);
|
|
3040
|
+
while (true) {
|
|
3041
|
+
try {
|
|
3042
|
+
const pkg = JSON.parse(readFileSync(join3(dir, "package.json"), "utf8"));
|
|
3043
|
+
if (pkg.name === PACKAGE_NAME && pkg.version) return pkg.version;
|
|
3044
|
+
} catch {
|
|
3045
|
+
}
|
|
3046
|
+
if (dir === root) return "0.0.0";
|
|
3047
|
+
dir = dirname2(dir);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
|
|
3056
3051
|
// src/core/gapreport.ts
|
|
3057
3052
|
function buildGapReport(input) {
|
|
3058
3053
|
const outcomes = new Map(
|
|
@@ -3280,39 +3275,56 @@ Flutter is live today; ${theme.bright(
|
|
|
3280
3275
|
p.log.step(theme.bright(phaseLabel));
|
|
3281
3276
|
}
|
|
3282
3277
|
const tick = useIntegrationSpinner ? setInterval(updateIntegrationMessage, 1e3) : void 0;
|
|
3278
|
+
const additions = { proposed: 0, applied: [] };
|
|
3279
|
+
const interactive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
3283
3280
|
const outcome = await runIntegrationAgent({
|
|
3284
3281
|
repoPath,
|
|
3285
3282
|
config,
|
|
3286
3283
|
session,
|
|
3287
3284
|
playbook: chosen.playbook,
|
|
3288
3285
|
manifest,
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
const placeEntries = plan.filter((entry) => entry.decision === "place");
|
|
3296
|
-
let selectedEvents = placeEntries.map((entry) => entry.event);
|
|
3297
|
-
if (placeEntries.length) {
|
|
3298
|
-
const selection = await p.multiselect({
|
|
3299
|
-
message: "Wire these events?",
|
|
3300
|
-
options: placeEntries.map((entry) => ({
|
|
3301
|
-
value: entry.event,
|
|
3302
|
-
label: entry.event,
|
|
3303
|
-
hint: `${entry.file ?? "unknown file"}@${entry.anchor ?? "unknown anchor"}`
|
|
3304
|
-
})),
|
|
3305
|
-
initialValues: selectedEvents,
|
|
3306
|
-
required: false
|
|
3307
|
-
});
|
|
3308
|
-
selectedEvents = p.isCancel(selection) ? [] : selection;
|
|
3309
|
-
}
|
|
3310
|
-
if (useIntegrationSpinner) {
|
|
3311
|
-
spin.start(theme.bright("Continuing integration"));
|
|
3312
|
-
}
|
|
3313
|
-
return filterEventPlan(plan, selectedEvents);
|
|
3286
|
+
// The plan is always shown — it's the last look at what's about to change
|
|
3287
|
+
// in their code. It only becomes a question under --review-plan; the
|
|
3288
|
+
// default run reads it out and keeps going.
|
|
3289
|
+
async onPlanReady(plan) {
|
|
3290
|
+
if (useIntegrationSpinner) {
|
|
3291
|
+
spin.stop(theme.success("Placement plan ready"));
|
|
3314
3292
|
}
|
|
3315
|
-
|
|
3293
|
+
p.note(renderPlacementPlan(plan), "Placement plan");
|
|
3294
|
+
const placeEntries = plan.filter((entry) => entry.decision === "place");
|
|
3295
|
+
let selectedEvents = placeEntries.map((entry) => entry.event);
|
|
3296
|
+
if (config.reviewPlan && interactive && placeEntries.length) {
|
|
3297
|
+
const selection = await p.multiselect({
|
|
3298
|
+
message: "Wire these events?",
|
|
3299
|
+
options: placeEntries.map((entry) => ({
|
|
3300
|
+
value: entry.event,
|
|
3301
|
+
label: entry.event,
|
|
3302
|
+
hint: `${entry.file ?? "unknown file"}@${entry.anchor ?? "unknown anchor"}`
|
|
3303
|
+
})),
|
|
3304
|
+
initialValues: selectedEvents,
|
|
3305
|
+
required: false
|
|
3306
|
+
});
|
|
3307
|
+
selectedEvents = p.isCancel(selection) ? [] : selection;
|
|
3308
|
+
}
|
|
3309
|
+
if (useIntegrationSpinner) {
|
|
3310
|
+
spin.start(theme.bright("Continuing integration"));
|
|
3311
|
+
}
|
|
3312
|
+
return filterEventPlan(plan, selectedEvents);
|
|
3313
|
+
},
|
|
3314
|
+
async onOpportunitiesReady(raw) {
|
|
3315
|
+
if (useIntegrationSpinner) spin.stop(theme.success("Planning done"));
|
|
3316
|
+
const applied = await applyOpportunities({
|
|
3317
|
+
config,
|
|
3318
|
+
session,
|
|
3319
|
+
playbook: chosen.playbook,
|
|
3320
|
+
manifest,
|
|
3321
|
+
fingerprint,
|
|
3322
|
+
raw,
|
|
3323
|
+
additions
|
|
3324
|
+
});
|
|
3325
|
+
if (useIntegrationSpinner) spin.start(theme.bright("Continuing integration"));
|
|
3326
|
+
return applied;
|
|
3327
|
+
},
|
|
3316
3328
|
progress: {
|
|
3317
3329
|
onPhase(label) {
|
|
3318
3330
|
phaseLabel = label;
|
|
@@ -3336,13 +3348,78 @@ Flutter is live today; ${theme.bright(
|
|
|
3336
3348
|
return null;
|
|
3337
3349
|
});
|
|
3338
3350
|
if (tick) clearInterval(tick);
|
|
3351
|
+
let verified = null;
|
|
3352
|
+
let reported = false;
|
|
3353
|
+
const collectReportEvents = async () => {
|
|
3354
|
+
if (!outcome) return [];
|
|
3355
|
+
const eventOutcomeByType = new Map(
|
|
3356
|
+
outcome.eventOutcomes.map((event) => [event.event, event])
|
|
3357
|
+
);
|
|
3358
|
+
const eventTypes = [
|
|
3359
|
+
...manifest.events.map((e) => e.eventType),
|
|
3360
|
+
...additions.applied.map((e) => e.code)
|
|
3361
|
+
];
|
|
3362
|
+
let wired = /* @__PURE__ */ new Map();
|
|
3363
|
+
try {
|
|
3364
|
+
wired = await scanWiredEvents(
|
|
3365
|
+
repoPath,
|
|
3366
|
+
await changedFiles(repoPath, checkpoint),
|
|
3367
|
+
eventTypes
|
|
3368
|
+
);
|
|
3369
|
+
} catch {
|
|
3370
|
+
}
|
|
3371
|
+
return eventTypes.map((eventType) => ({
|
|
3372
|
+
event_type: eventType,
|
|
3373
|
+
status: wired.has(eventType) ? "wired" : "skipped",
|
|
3374
|
+
file: wired.get(eventType),
|
|
3375
|
+
// The agent's own words on why it skipped — the field that answers "why
|
|
3376
|
+
// didn't it wire X" without anyone opening the repo.
|
|
3377
|
+
reason: wired.has(eventType) ? void 0 : eventOutcomeByType.get(eventType)?.reason
|
|
3378
|
+
}));
|
|
3379
|
+
};
|
|
3380
|
+
const reportRun = async (exitClass) => {
|
|
3381
|
+
if (reported) return [];
|
|
3382
|
+
reported = true;
|
|
3383
|
+
const reportEvents2 = await collectReportEvents();
|
|
3384
|
+
const result = await postRunReport(config, session, {
|
|
3385
|
+
target: chosen.playbook.target.id,
|
|
3386
|
+
repo_fingerprint: fingerprint,
|
|
3387
|
+
identify_wired: outcome?.coreOk ?? false,
|
|
3388
|
+
verified,
|
|
3389
|
+
cost_usd: outcome?.costUsd ?? 0,
|
|
3390
|
+
duration_ms: outcome?.durationMs ?? Date.now() - startedAt,
|
|
3391
|
+
summary: scrubSummary(outcome?.summary ?? "").slice(0, 4e3),
|
|
3392
|
+
events: reportEvents2,
|
|
3393
|
+
wizard_version: packageVersion(),
|
|
3394
|
+
planner_model: config.plannerModel,
|
|
3395
|
+
editor_model: config.model,
|
|
3396
|
+
effort: config.effort,
|
|
3397
|
+
exit_class: exitClass,
|
|
3398
|
+
phase_timings: outcome?.phaseTimings ?? [],
|
|
3399
|
+
opportunities_proposed: additions.proposed,
|
|
3400
|
+
opportunities_applied: additions.applied.length
|
|
3401
|
+
});
|
|
3402
|
+
if (!result.ok) {
|
|
3403
|
+
p.log.warn(
|
|
3404
|
+
theme.warn("Couldn't record this run's coverage on the server") + theme.muted(
|
|
3405
|
+
` (${formatReportFailure(result)}) \u2014 future runs may re-propose events already wired here.`
|
|
3406
|
+
)
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
return reportEvents2;
|
|
3410
|
+
};
|
|
3339
3411
|
if (!outcome) {
|
|
3340
3412
|
if (useIntegrationSpinner) {
|
|
3341
3413
|
spin.stop(theme.alert("Integration stopped"));
|
|
3342
3414
|
} else {
|
|
3343
3415
|
p.log.error(theme.alert("Integration stopped"));
|
|
3344
3416
|
}
|
|
3345
|
-
await maybeRevert(
|
|
3417
|
+
const reverted = await maybeRevert(
|
|
3418
|
+
repoPath,
|
|
3419
|
+
checkpoint,
|
|
3420
|
+
"The run stopped before finishing."
|
|
3421
|
+
);
|
|
3422
|
+
await reportRun(reverted ? "reverted" : "agent_error");
|
|
3346
3423
|
return 1;
|
|
3347
3424
|
}
|
|
3348
3425
|
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");
|
|
@@ -3353,7 +3430,6 @@ Flutter is live today; ${theme.bright(
|
|
|
3353
3430
|
} else {
|
|
3354
3431
|
p.log.success(stopLabel);
|
|
3355
3432
|
}
|
|
3356
|
-
const opportunities = await collectOpportunities(repoPath, manifest, checkpoint);
|
|
3357
3433
|
let files = await changedFiles(repoPath, checkpoint);
|
|
3358
3434
|
if (files.length) {
|
|
3359
3435
|
p.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
@@ -3366,11 +3442,12 @@ Flutter is live today; ${theme.bright(
|
|
|
3366
3442
|
"The core setup didn't complete, so these changes may be incomplete."
|
|
3367
3443
|
);
|
|
3368
3444
|
if (reverted) {
|
|
3445
|
+
await reportRun("reverted");
|
|
3369
3446
|
p.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
3370
3447
|
return 1;
|
|
3371
3448
|
}
|
|
3449
|
+
await reportRun("core_failed");
|
|
3372
3450
|
}
|
|
3373
|
-
let verified = null;
|
|
3374
3451
|
if (chosen.playbook.verifyCommand && files.length) {
|
|
3375
3452
|
const cmd = chosen.playbook.verifyCommand;
|
|
3376
3453
|
const vspin = p.spinner();
|
|
@@ -3450,94 +3527,21 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3450
3527
|
"The integration didn't pass its build/lint check, so the edits may be broken."
|
|
3451
3528
|
);
|
|
3452
3529
|
if (reverted) {
|
|
3530
|
+
await reportRun("reverted");
|
|
3453
3531
|
p.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
3454
3532
|
return 1;
|
|
3455
3533
|
}
|
|
3534
|
+
await reportRun("verify_failed");
|
|
3456
3535
|
}
|
|
3457
3536
|
}
|
|
3458
3537
|
}
|
|
3459
|
-
const
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
fingerprint,
|
|
3467
|
-
checkpoint,
|
|
3468
|
-
remainingBudgetUsd: config.budgetUsd - outcome.costUsd,
|
|
3469
|
-
repoMap: outcome.repoMap
|
|
3470
|
-
});
|
|
3471
|
-
if (instrumentationSnapshot && chosen.playbook.verifyCommand) {
|
|
3472
|
-
const cmd = chosen.playbook.verifyCommand;
|
|
3473
|
-
const preInstrumentationVerified = verified;
|
|
3474
|
-
const vspin = p.spinner();
|
|
3475
|
-
vspin.start(`Re-verifying after instrumenting the new events \u2014 ${theme.muted(cmd)}`);
|
|
3476
|
-
const verdict = await runVerifyCommand(repoPath, cmd);
|
|
3477
|
-
verified = verdictToVerified(verdict);
|
|
3478
|
-
if (verdict.toolMissing) {
|
|
3479
|
-
vspin.stop(
|
|
3480
|
-
theme.warn("Couldn't re-verify automatically") + theme.muted(` \u2014 \`${cmd}\` isn't available here. Run it yourself before committing.`)
|
|
3481
|
-
);
|
|
3482
|
-
} else if (verdict.timedOut) {
|
|
3483
|
-
vspin.stop(
|
|
3484
|
-
theme.warn(`Re-verification timed out \u2014 \`${cmd}\``) + theme.muted(" \u2014 run it yourself before committing.")
|
|
3485
|
-
);
|
|
3486
|
-
} else if (verdict.ok) {
|
|
3487
|
-
vspin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
3488
|
-
} else {
|
|
3489
|
-
vspin.stop(theme.alert(`Verification failed after instrumenting the new events \u2014 ${cmd}`));
|
|
3490
|
-
if (verdict.output) {
|
|
3491
|
-
p.note(verdict.output, theme.warn("Verifier output"));
|
|
3492
|
-
}
|
|
3493
|
-
const doRevert = await p.confirm({
|
|
3494
|
-
message: "Instrumenting the new events didn't pass the build/lint check. Revert just those edits? (The events stay in your universe \u2014 wire them on a future run.)",
|
|
3495
|
-
initialValue: true
|
|
3496
|
-
});
|
|
3497
|
-
if (!p.isCancel(doRevert) && doRevert) {
|
|
3498
|
-
if (await restoreToSnapshot(repoPath, checkpoint, instrumentationSnapshot)) {
|
|
3499
|
-
verified = preInstrumentationVerified;
|
|
3500
|
-
p.log.success(theme.success("Reverted the instrumentation edits."));
|
|
3501
|
-
} else {
|
|
3502
|
-
p.log.warn(
|
|
3503
|
-
theme.warn("Couldn't revert automatically \u2014 undo manually: ") + (revertHint(checkpoint) ?? "git reset --hard")
|
|
3504
|
-
);
|
|
3505
|
-
}
|
|
3506
|
-
} else {
|
|
3507
|
-
p.log.info(
|
|
3508
|
-
theme.muted("Left in your working tree \u2014 fix the verifier errors before committing.")
|
|
3509
|
-
);
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
}
|
|
3513
|
-
const filesForScan = acceptedEvents.length ? await changedFiles(repoPath, checkpoint) : files;
|
|
3514
|
-
const eventTypesToScan = [
|
|
3515
|
-
...manifest.events.map((e) => e.eventType),
|
|
3516
|
-
...acceptedEvents.map((e) => e.code)
|
|
3517
|
-
];
|
|
3518
|
-
const wiredMap = await scanWiredEvents(repoPath, filesForScan, eventTypesToScan);
|
|
3519
|
-
const reportEvents = eventTypesToScan.map((eventType) => ({
|
|
3520
|
-
event_type: eventType,
|
|
3521
|
-
status: wiredMap.has(eventType) ? "wired" : "skipped",
|
|
3522
|
-
file: wiredMap.get(eventType)
|
|
3523
|
-
}));
|
|
3524
|
-
const reportResult = await postRunReport(config, session, {
|
|
3525
|
-
target: chosen.playbook.target.id,
|
|
3526
|
-
repo_fingerprint: fingerprint,
|
|
3527
|
-
identify_wired: outcome.coreOk,
|
|
3528
|
-
verified,
|
|
3529
|
-
cost_usd: outcome.costUsd,
|
|
3530
|
-
duration_ms: outcome.durationMs,
|
|
3531
|
-
summary: scrubSummary(outcome.summary).slice(0, 4e3),
|
|
3532
|
-
events: reportEvents
|
|
3533
|
-
});
|
|
3534
|
-
if (!reportResult.ok) {
|
|
3535
|
-
p.log.warn(
|
|
3536
|
-
theme.warn("Couldn't record this run's coverage on the server") + theme.muted(
|
|
3537
|
-
` (${formatReportFailure(reportResult)}) \u2014 future runs may re-propose events already wired here.`
|
|
3538
|
-
)
|
|
3539
|
-
);
|
|
3540
|
-
}
|
|
3538
|
+
const reportedEvents = await reportRun(
|
|
3539
|
+
outcome.eventsComplete ? "completed" : "events_incomplete"
|
|
3540
|
+
);
|
|
3541
|
+
const reportEvents = reportedEvents.length ? reportedEvents : await collectReportEvents();
|
|
3542
|
+
const wiredMap = new Map(
|
|
3543
|
+
reportEvents.filter((event) => event.status === "wired").map((event) => [event.event_type, event.file ?? ""])
|
|
3544
|
+
);
|
|
3541
3545
|
try {
|
|
3542
3546
|
const approvedSuggestions = await approvedSuggestionsPromise;
|
|
3543
3547
|
if (approvedSuggestions?.length) {
|
|
@@ -3577,15 +3581,12 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3577
3581
|
if (eventLines) {
|
|
3578
3582
|
p.note(eventLines, "Events");
|
|
3579
3583
|
}
|
|
3580
|
-
const eventOutcomeByType = new Map(
|
|
3581
|
-
outcome.eventOutcomes.map((event) => [event.event, event])
|
|
3582
|
-
);
|
|
3583
3584
|
const gapReport = buildGapReport({
|
|
3584
3585
|
manifest,
|
|
3585
3586
|
events: reportEvents.map((event) => ({
|
|
3586
3587
|
eventType: event.event_type,
|
|
3587
3588
|
status: event.status,
|
|
3588
|
-
reason:
|
|
3589
|
+
reason: event.reason
|
|
3589
3590
|
}))
|
|
3590
3591
|
});
|
|
3591
3592
|
if (gapReport.hasContent) {
|
|
@@ -3594,7 +3595,7 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3594
3595
|
theme.signal("What you could be doing")
|
|
3595
3596
|
);
|
|
3596
3597
|
}
|
|
3597
|
-
const eventDenominator = manifest.universeSummary?.wireableHere ??
|
|
3598
|
+
const eventDenominator = manifest.universeSummary?.wireableHere ?? reportEvents.length;
|
|
3598
3599
|
const eventStatsLabel = manifest.universeSummary ? "events wired here" : "events wired";
|
|
3599
3600
|
const timingDetails = outcome.phaseTimings.map(({ phase, ms }) => `${shortPhaseLabel(phase)} ${formatDuration(ms)}`).join(" \xB7 ");
|
|
3600
3601
|
p.log.info(
|
|
@@ -3634,25 +3635,21 @@ ${repair.summary}`].filter(Boolean).join("\n\n");
|
|
|
3634
3635
|
p.outro(theme.signal("\u2301 ") + theme.bright("Whisperr is wired in."));
|
|
3635
3636
|
return 0;
|
|
3636
3637
|
}
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
const
|
|
3638
|
+
async function applyOpportunities(opts) {
|
|
3639
|
+
const { config, session, playbook, manifest, fingerprint, raw, additions } = opts;
|
|
3640
|
+
const clean = sanitizeOpportunities(raw, manifest);
|
|
3640
3641
|
const openSuggestions = await fetchSuggestions(config, session, ["proposed", "approved"]);
|
|
3641
|
-
const
|
|
3642
|
-
openSuggestions.map(
|
|
3643
|
-
(suggestion) => `${suggestion.kind}:${normalizeCode(suggestion.code)}`
|
|
3644
|
-
)
|
|
3642
|
+
const openKeys = new Set(
|
|
3643
|
+
openSuggestions.map((s) => `${s.kind}:${normalizeCode(s.code)}`)
|
|
3645
3644
|
);
|
|
3646
3645
|
const opportunities = {
|
|
3647
|
-
events:
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
interventions: opts.opportunities.interventions.filter(
|
|
3651
|
-
(intervention) => !openSuggestionKeys.has(`intervention:${normalizeCode(intervention.code)}`)
|
|
3646
|
+
events: clean.events.filter((e) => !openKeys.has(`event:${normalizeCode(e.code)}`)),
|
|
3647
|
+
interventions: clean.interventions.filter(
|
|
3648
|
+
(i) => !openKeys.has(`intervention:${normalizeCode(i.code)}`)
|
|
3652
3649
|
)
|
|
3653
3650
|
};
|
|
3654
|
-
|
|
3655
|
-
if (
|
|
3651
|
+
additions.proposed = opportunities.events.length + opportunities.interventions.length;
|
|
3652
|
+
if (additions.proposed === 0) return [];
|
|
3656
3653
|
const describe = (kind, code, why) => `${theme.muted(`${kind}: `)}${theme.bright(code)}${why ? theme.muted(` \u2014 ${why}`) : ""}`;
|
|
3657
3654
|
p.note(
|
|
3658
3655
|
[
|
|
@@ -3665,110 +3662,29 @@ async function offerOpportunities(opts) {
|
|
|
3665
3662
|
);
|
|
3666
3663
|
if (config.offline) {
|
|
3667
3664
|
p.log.info(theme.muted("Offline mode \u2014 proposals shown only, nothing submitted."));
|
|
3668
|
-
return
|
|
3669
|
-
}
|
|
3670
|
-
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
3671
|
-
p.log.info(
|
|
3672
|
-
theme.muted("Non-interactive run \u2014 proposals were not submitted. Re-run interactively to add them.")
|
|
3673
|
-
);
|
|
3674
|
-
return NO_ADDITIONS;
|
|
3675
|
-
}
|
|
3676
|
-
const selection = await p.multiselect({
|
|
3677
|
-
message: "Send these to your dashboard for approval?",
|
|
3678
|
-
options: [
|
|
3679
|
-
...opportunities.events.map((e) => ({
|
|
3680
|
-
value: `e:${e.code}`,
|
|
3681
|
-
label: `event \xB7 ${e.code}`,
|
|
3682
|
-
hint: e.description ?? e.rationale
|
|
3683
|
-
})),
|
|
3684
|
-
...opportunities.interventions.map((i) => ({
|
|
3685
|
-
value: `i:${i.code}`,
|
|
3686
|
-
label: `intervention \xB7 ${i.code}`,
|
|
3687
|
-
hint: i.description ?? i.rationale
|
|
3688
|
-
}))
|
|
3689
|
-
],
|
|
3690
|
-
// Opt-in per item: nothing is pre-selected, so submitting a suggestion is
|
|
3691
|
-
// an explicit choice rather than an opt-out the user has to notice and undo.
|
|
3692
|
-
initialValues: [],
|
|
3693
|
-
required: false
|
|
3694
|
-
});
|
|
3695
|
-
if (p.isCancel(selection) || selection.length === 0) {
|
|
3696
|
-
p.log.info(theme.muted("Skipped \u2014 your universe is unchanged."));
|
|
3697
|
-
return NO_ADDITIONS;
|
|
3665
|
+
return [];
|
|
3698
3666
|
}
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
);
|
|
3703
|
-
const knownEventCodes = new Set(manifest.events.map((e) => e.eventType));
|
|
3704
|
-
const pickedEvents = opportunities.events.filter((e) => chosenSet.has(`e:${e.code}`));
|
|
3705
|
-
const pickedInterventions = opportunities.interventions.filter(
|
|
3706
|
-
(i) => chosenSet.has(`i:${i.code}`)
|
|
3707
|
-
);
|
|
3708
|
-
const pickedInterventionCodes = new Set(pickedInterventions.map((i) => i.code));
|
|
3709
|
-
const pickedEventCodes = new Set(pickedEvents.map((e) => e.code));
|
|
3710
|
-
const submitted = {
|
|
3711
|
-
events: pickedEvents.map((e) => ({
|
|
3712
|
-
...e,
|
|
3713
|
-
links: e.links?.filter(
|
|
3714
|
-
(l) => knownInterventionCodes.has(l.interventionCode) || pickedInterventionCodes.has(l.interventionCode)
|
|
3715
|
-
)
|
|
3716
|
-
})),
|
|
3717
|
-
interventions: pickedInterventions.map((i) => ({
|
|
3718
|
-
...i,
|
|
3719
|
-
links: i.links?.filter(
|
|
3720
|
-
(l) => knownEventCodes.has(l.eventCode) || pickedEventCodes.has(l.eventCode)
|
|
3721
|
-
)
|
|
3722
|
-
}))
|
|
3723
|
-
};
|
|
3724
|
-
let stageResult;
|
|
3725
|
-
try {
|
|
3726
|
-
stageResult = await withSpinner(
|
|
3727
|
-
"Sending suggestions for approval",
|
|
3728
|
-
() => submitSuggestions(config, session, opts.playbook.target.id, opts.fingerprint, submitted)
|
|
3729
|
-
);
|
|
3730
|
-
} catch (err) {
|
|
3731
|
-
p.log.warn(
|
|
3732
|
-
theme.warn("Couldn't send the proposals") + theme.muted(` \u2014 ${err.message}. Nothing was submitted.`)
|
|
3733
|
-
);
|
|
3734
|
-
return NO_ADDITIONS;
|
|
3735
|
-
}
|
|
3736
|
-
if (stageResult) {
|
|
3737
|
-
const lines2 = stageResult.outcomes.map((outcome) => {
|
|
3738
|
-
const mark = outcome.status === "staged" ? theme.success("\u2713") : outcome.status === "duplicate" ? theme.muted("=") : theme.warn("\u2717");
|
|
3739
|
-
const reason = outcome.reason ?? (outcome.duplicateOf && outcome.duplicateOf !== outcome.code ? `already queued as ${outcome.duplicateOf}` : void 0);
|
|
3740
|
-
const detail = reason ? theme.muted(` (${reason})`) : "";
|
|
3741
|
-
return `${mark} ${outcome.kind} ${theme.bright(outcome.code)}${detail}`;
|
|
3742
|
-
});
|
|
3743
|
-
lines2.push(
|
|
3744
|
-
theme.muted(
|
|
3745
|
-
`${stageResult.staged} staged \xB7 ${stageResult.duplicates} already queued \xB7 ${stageResult.invalid} rejected`
|
|
3746
|
-
)
|
|
3747
|
-
);
|
|
3748
|
-
p.note(lines2.join("\n"), theme.signal("Suggestions submitted"));
|
|
3749
|
-
const where = stageResult.approvalsUrl ? `approve at ${theme.bright(stageResult.approvalsUrl)}` : "approve them in your Whisperr dashboard";
|
|
3750
|
-
p.log.info(
|
|
3751
|
-
theme.muted(
|
|
3752
|
-
`${stageResult.staged} proposal${stageResult.staged === 1 ? "" : "s"} sent \u2014 ${where}. The next wizard run wires whatever you approve.`
|
|
3753
|
-
)
|
|
3754
|
-
);
|
|
3755
|
-
return NO_ADDITIONS;
|
|
3667
|
+
if (config.proposeOnly) {
|
|
3668
|
+
await stageOpportunities(config, session, playbook, fingerprint, opportunities);
|
|
3669
|
+
return [];
|
|
3756
3670
|
}
|
|
3757
3671
|
let result;
|
|
3758
3672
|
try {
|
|
3759
3673
|
result = await withSpinner(
|
|
3760
3674
|
"Adding to your universe",
|
|
3761
|
-
() => submitAdditions(config, session,
|
|
3675
|
+
() => submitAdditions(config, session, playbook.target.id, fingerprint, opportunities)
|
|
3762
3676
|
);
|
|
3763
3677
|
} catch (err) {
|
|
3764
3678
|
p.log.warn(
|
|
3765
3679
|
theme.warn("Couldn't add the proposals") + theme.muted(` \u2014 ${err.message}. Your universe is unchanged.`)
|
|
3766
3680
|
);
|
|
3767
|
-
return
|
|
3681
|
+
return [];
|
|
3768
3682
|
}
|
|
3769
3683
|
const lines = result.outcomes.map((o) => {
|
|
3770
3684
|
const mark = o.status === "applied" ? theme.success("\u2713") : o.status === "duplicate" ? theme.muted("=") : theme.warn("\u2717");
|
|
3771
|
-
const detail = o.status === "duplicate" ? theme.muted(
|
|
3685
|
+
const detail = o.status === "duplicate" ? theme.muted(
|
|
3686
|
+
` (already in your universe${o.duplicateOf && o.duplicateOf !== o.code ? ` as ${o.duplicateOf}` : ""})`
|
|
3687
|
+
) : o.status === "invalid" && o.reason ? theme.muted(` (${o.reason})`) : "";
|
|
3772
3688
|
return `${mark} ${o.kind} ${theme.bright(o.code)}${detail}`;
|
|
3773
3689
|
});
|
|
3774
3690
|
lines.push(
|
|
@@ -3776,7 +3692,7 @@ async function offerOpportunities(opts) {
|
|
|
3776
3692
|
`${result.applied} added \xB7 ${result.duplicates} already present \xB7 ${result.invalid} rejected`
|
|
3777
3693
|
)
|
|
3778
3694
|
);
|
|
3779
|
-
if (
|
|
3695
|
+
if (opportunities.interventions.length) {
|
|
3780
3696
|
lines.push(theme.muted("New interventions start paused \u2014 activate them in your dashboard."));
|
|
3781
3697
|
}
|
|
3782
3698
|
if (result.policyRegen?.status === "pending") {
|
|
@@ -3799,41 +3715,49 @@ async function offerOpportunities(opts) {
|
|
|
3799
3715
|
)
|
|
3800
3716
|
);
|
|
3801
3717
|
}
|
|
3802
|
-
const
|
|
3718
|
+
const appliedCodes = new Set(
|
|
3803
3719
|
result.outcomes.filter((o) => o.kind === "event" && o.status === "applied").map((o) => o.code)
|
|
3804
3720
|
);
|
|
3805
|
-
const
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3721
|
+
const applied = opportunities.events.filter((e) => appliedCodes.has(e.code));
|
|
3722
|
+
additions.applied = applied;
|
|
3723
|
+
return applied;
|
|
3724
|
+
}
|
|
3725
|
+
async function stageOpportunities(config, session, playbook, fingerprint, opportunities) {
|
|
3726
|
+
let stageResult;
|
|
3810
3727
|
try {
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
config,
|
|
3814
|
-
|
|
3815
|
-
playbook: opts.playbook,
|
|
3816
|
-
acceptedEvents,
|
|
3817
|
-
budgetUsd: opts.remainingBudgetUsd,
|
|
3818
|
-
repoMap: opts.repoMap
|
|
3819
|
-
});
|
|
3820
|
-
if (pass.ran) {
|
|
3821
|
-
spin.stop(theme.success("New events instrumented"));
|
|
3822
|
-
logAgentSummary(pass.summary);
|
|
3823
|
-
} else {
|
|
3824
|
-
spin.stop(
|
|
3825
|
-
theme.warn("Skipped instrumenting the new events") + theme.muted(
|
|
3826
|
-
" \u2014 the spend limit was reached. They're in your universe; wire them on a future run."
|
|
3827
|
-
)
|
|
3828
|
-
);
|
|
3829
|
-
}
|
|
3728
|
+
stageResult = await withSpinner(
|
|
3729
|
+
"Sending suggestions for approval",
|
|
3730
|
+
() => submitSuggestions(config, session, playbook.target.id, fingerprint, opportunities)
|
|
3731
|
+
);
|
|
3830
3732
|
} catch (err) {
|
|
3831
|
-
|
|
3832
|
-
theme.warn("Couldn't
|
|
3733
|
+
p.log.warn(
|
|
3734
|
+
theme.warn("Couldn't send the proposals") + theme.muted(` \u2014 ${err.message}. Nothing was submitted.`)
|
|
3833
3735
|
);
|
|
3736
|
+
return;
|
|
3834
3737
|
}
|
|
3835
|
-
|
|
3836
|
-
|
|
3738
|
+
if (!stageResult) {
|
|
3739
|
+
p.log.warn(
|
|
3740
|
+
theme.warn("This Whisperr backend has no approval queue") + theme.muted(" \u2014 nothing was submitted. Re-run without --propose-only to add them directly.")
|
|
3741
|
+
);
|
|
3742
|
+
return;
|
|
3743
|
+
}
|
|
3744
|
+
const lines = stageResult.outcomes.map((outcome) => {
|
|
3745
|
+
const mark = outcome.status === "staged" ? theme.success("\u2713") : outcome.status === "duplicate" ? theme.muted("=") : theme.warn("\u2717");
|
|
3746
|
+
const reason = outcome.reason ?? (outcome.duplicateOf && outcome.duplicateOf !== outcome.code ? `already queued as ${outcome.duplicateOf}` : void 0);
|
|
3747
|
+
return `${mark} ${outcome.kind} ${theme.bright(outcome.code)}${reason ? theme.muted(` (${reason})`) : ""}`;
|
|
3748
|
+
});
|
|
3749
|
+
lines.push(
|
|
3750
|
+
theme.muted(
|
|
3751
|
+
`${stageResult.staged} staged \xB7 ${stageResult.duplicates} already queued \xB7 ${stageResult.invalid} rejected`
|
|
3752
|
+
)
|
|
3753
|
+
);
|
|
3754
|
+
p.note(lines.join("\n"), theme.signal("Suggestions submitted"));
|
|
3755
|
+
const where = stageResult.approvalsUrl ? `approve at ${theme.bright(stageResult.approvalsUrl)}` : "approve them in your Whisperr dashboard";
|
|
3756
|
+
p.log.info(
|
|
3757
|
+
theme.muted(
|
|
3758
|
+
`${stageResult.staged} proposal${stageResult.staged === 1 ? "" : "s"} sent \u2014 ${where}. The next wizard run wires whatever you approve.`
|
|
3759
|
+
)
|
|
3760
|
+
);
|
|
3837
3761
|
}
|
|
3838
3762
|
async function maybeRevert(repoPath, checkpoint, reason) {
|
|
3839
3763
|
if (!checkpoint.isRepo) return false;
|
|
@@ -3993,12 +3917,10 @@ function logAgentSummary(summary) {
|
|
|
3993
3917
|
}
|
|
3994
3918
|
function shortPhaseLabel(phase) {
|
|
3995
3919
|
switch (phase) {
|
|
3996
|
-
case "
|
|
3997
|
-
return "
|
|
3920
|
+
case "Planning your integration":
|
|
3921
|
+
return "plan";
|
|
3998
3922
|
case "Installing the SDK & wiring identify()":
|
|
3999
3923
|
return "core";
|
|
4000
|
-
case "Planning event placements":
|
|
4001
|
-
return "plan";
|
|
4002
3924
|
case "Instrumenting planned events":
|
|
4003
3925
|
case "Instrumenting your events":
|
|
4004
3926
|
return "wire";
|
|
@@ -4031,12 +3953,7 @@ function formatReportFailure(result) {
|
|
|
4031
3953
|
}
|
|
4032
3954
|
|
|
4033
3955
|
// src/index.ts
|
|
4034
|
-
var modulePath =
|
|
4035
|
-
function packageVersion() {
|
|
4036
|
-
const packagePath = join4(dirname2(modulePath), "..", "package.json");
|
|
4037
|
-
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
4038
|
-
return pkg.version ?? "0.0.0";
|
|
4039
|
-
}
|
|
3956
|
+
var modulePath = fileURLToPath2(import.meta.url);
|
|
4040
3957
|
function parseArgs(argv) {
|
|
4041
3958
|
const args = [...argv];
|
|
4042
3959
|
const opts = {};
|
|
@@ -4056,6 +3973,15 @@ function parseArgs(argv) {
|
|
|
4056
3973
|
case "--force":
|
|
4057
3974
|
opts.force = true;
|
|
4058
3975
|
break;
|
|
3976
|
+
case "--review-plan":
|
|
3977
|
+
opts.reviewPlan = true;
|
|
3978
|
+
break;
|
|
3979
|
+
case "--propose-only":
|
|
3980
|
+
opts.proposeOnly = true;
|
|
3981
|
+
break;
|
|
3982
|
+
case "--strict-review":
|
|
3983
|
+
opts.strictReview = true;
|
|
3984
|
+
break;
|
|
4059
3985
|
case "--api":
|
|
4060
3986
|
opts.apiBaseUrl = args[++i];
|
|
4061
3987
|
break;
|
|
@@ -4082,6 +4008,10 @@ function printHelp() {
|
|
|
4082
4008
|
` ${theme.bright("Options")}`,
|
|
4083
4009
|
" --offline Use a demo manifest, no account/browser needed",
|
|
4084
4010
|
" --force Proceed without a clean git tree (no safe undo)",
|
|
4011
|
+
" --review-plan Confirm the placement plan before anything is wired",
|
|
4012
|
+
" --propose-only Send new events/interventions for dashboard approval",
|
|
4013
|
+
" instead of adding them to your universe now",
|
|
4014
|
+
" --strict-review Review the finished diff with the deeper planner model",
|
|
4085
4015
|
" --api <url> Override the Whisperr API base URL",
|
|
4086
4016
|
" --model <id> Override the coding-agent model",
|
|
4087
4017
|
" -h, --help Show this help",
|