@whisperr/wizard 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -10
- package/dist/index.js +1094 -104
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,12 +6,13 @@ import { dirname as dirname2, join as join4 } from "path";
|
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
|
-
import { resolve } from "path";
|
|
9
|
+
import { resolve as resolve2 } from "path";
|
|
10
10
|
import * as p from "@clack/prompts";
|
|
11
11
|
|
|
12
12
|
// src/core/config.ts
|
|
13
13
|
var DEFAULT_API_BASE = "https://api.whisperr.net";
|
|
14
14
|
var DEFAULT_MODEL = "claude-sonnet-5";
|
|
15
|
+
var DEFAULT_PLANNER_MODEL = "claude-opus-4-8";
|
|
15
16
|
var DEFAULT_EFFORT = "high";
|
|
16
17
|
var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
17
18
|
function resolveEffort() {
|
|
@@ -29,6 +30,7 @@ function resolveConfig(flags = {}) {
|
|
|
29
30
|
apiBaseUrl,
|
|
30
31
|
llmBaseUrl,
|
|
31
32
|
model: flags.model ?? process.env.WHISPERR_WIZARD_MODEL ?? DEFAULT_MODEL,
|
|
33
|
+
plannerModel: process.env.WHISPERR_WIZARD_PLANNER_MODEL ?? DEFAULT_PLANNER_MODEL,
|
|
32
34
|
effort: resolveEffort(),
|
|
33
35
|
// Cost is the real limiter (budgetUsd below). maxTurns is only a high safety
|
|
34
36
|
// backstop against a stuck loop — it should NOT bind on a normal run. (It
|
|
@@ -36,7 +38,7 @@ function resolveConfig(flags = {}) {
|
|
|
36
38
|
maxTurns: Number(process.env.WHISPERR_WIZARD_MAX_TURNS) || 200,
|
|
37
39
|
// Hard ceiling on TOTAL spend across all phases. If hit, the run stops
|
|
38
40
|
// cleanly and keeps whatever already landed. Real runs finish well under it.
|
|
39
|
-
budgetUsd: Number(process.env.WHISPERR_WIZARD_BUDGET_USD) ||
|
|
41
|
+
budgetUsd: Number(process.env.WHISPERR_WIZARD_BUDGET_USD) || 25,
|
|
40
42
|
directAnthropicKey,
|
|
41
43
|
offline
|
|
42
44
|
};
|
|
@@ -1148,10 +1150,6 @@ function mockManifest(appId, config) {
|
|
|
1148
1150
|
// src/core/agent.ts
|
|
1149
1151
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1150
1152
|
|
|
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
1153
|
// src/core/git.ts
|
|
1156
1154
|
import { spawn } from "child_process";
|
|
1157
1155
|
import { createHash } from "crypto";
|
|
@@ -1277,41 +1275,58 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
|
1277
1275
|
const wired = /* @__PURE__ */ new Map();
|
|
1278
1276
|
const patterns = eventTypes.map((e) => ({
|
|
1279
1277
|
eventType: e,
|
|
1280
|
-
|
|
1278
|
+
trackRe: new RegExp(
|
|
1281
1279
|
`\\btrack\\s*\\(\\s*[\\s\\S]{0,160}?['"\`]${escapeRegExp(e)}['"\`]`
|
|
1282
|
-
)
|
|
1280
|
+
),
|
|
1281
|
+
literalRe: quotedLiteralRegExp(e)
|
|
1283
1282
|
}));
|
|
1283
|
+
const contents = [];
|
|
1284
1284
|
for (const file of files) {
|
|
1285
|
-
let content = "";
|
|
1286
1285
|
try {
|
|
1287
|
-
content
|
|
1286
|
+
contents.push({ file, content: await readFile2(join2(repoPath, file), "utf8") });
|
|
1288
1287
|
} catch {
|
|
1289
1288
|
continue;
|
|
1290
1289
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1290
|
+
}
|
|
1291
|
+
for (const { file, content } of contents) {
|
|
1292
|
+
for (const { eventType, trackRe } of patterns) {
|
|
1293
|
+
if (!wired.has(eventType) && trackRe.test(content)) {
|
|
1294
|
+
wired.set(eventType, file);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
for (const { file, content } of contents) {
|
|
1299
|
+
for (const { eventType, literalRe } of patterns) {
|
|
1300
|
+
if (!wired.has(eventType) && literalRe.test(content)) {
|
|
1293
1301
|
wired.set(eventType, file);
|
|
1294
1302
|
}
|
|
1295
1303
|
}
|
|
1296
1304
|
}
|
|
1297
1305
|
return wired;
|
|
1298
1306
|
}
|
|
1307
|
+
function quotedLiteralRegExp(eventType) {
|
|
1308
|
+
const leftBoundary = /^\w/.test(eventType) ? "\\b" : "";
|
|
1309
|
+
const rightBoundary = /\w$/.test(eventType) ? "\\b" : "";
|
|
1310
|
+
return new RegExp(`(['"\`])${leftBoundary}${escapeRegExp(eventType)}${rightBoundary}\\1`);
|
|
1311
|
+
}
|
|
1299
1312
|
function escapeRegExp(s) {
|
|
1300
1313
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1301
1314
|
}
|
|
1302
1315
|
function run(cwd, args) {
|
|
1303
|
-
return new Promise((
|
|
1316
|
+
return new Promise((resolve3) => {
|
|
1304
1317
|
const child = spawn("git", args, { cwd });
|
|
1305
1318
|
let stdout = "";
|
|
1306
1319
|
let stderr = "";
|
|
1307
1320
|
child.stdout.on("data", (d) => stdout += d.toString());
|
|
1308
1321
|
child.stderr.on("data", (d) => stderr += d.toString());
|
|
1309
|
-
child.on("close", (code) =>
|
|
1310
|
-
child.on("error", () =>
|
|
1322
|
+
child.on("close", (code) => resolve3({ ok: code === 0, stdout, stderr }));
|
|
1323
|
+
child.on("error", () => resolve3({ ok: false, stdout, stderr }));
|
|
1311
1324
|
});
|
|
1312
1325
|
}
|
|
1313
1326
|
|
|
1314
1327
|
// src/core/opportunities.ts
|
|
1328
|
+
import { readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
1329
|
+
import { join as join3 } from "path";
|
|
1315
1330
|
var OPPORTUNITIES_FILE = "whisperr-opportunities.json";
|
|
1316
1331
|
function normalizeCode(value) {
|
|
1317
1332
|
return value.trim().toLowerCase().replace(/[ \-./]/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
@@ -1342,9 +1357,10 @@ function sanitizeOpportunities(raw, manifest) {
|
|
|
1342
1357
|
if (typeof raw !== "object" || raw === null) return out;
|
|
1343
1358
|
const doc = raw;
|
|
1344
1359
|
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
|
-
|
|
1360
|
+
const knownInterventions = /* @__PURE__ */ new Set([
|
|
1361
|
+
...manifest.events.flatMap((e) => e.interventions ?? []).map((i) => normalizeCode(i.code)),
|
|
1362
|
+
...(manifest.interventions ?? []).map((i) => normalizeCode(i.code))
|
|
1363
|
+
]);
|
|
1348
1364
|
const seenEvents = /* @__PURE__ */ new Set();
|
|
1349
1365
|
for (const entry of asArray(doc.events)) {
|
|
1350
1366
|
const ev = coerceEvent(entry);
|
|
@@ -1710,15 +1726,319 @@ function coverageNote(coverage) {
|
|
|
1710
1726
|
return "";
|
|
1711
1727
|
}
|
|
1712
1728
|
|
|
1729
|
+
// src/core/toolPolicy.ts
|
|
1730
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
1731
|
+
var SECRET_MATERIAL_DENIAL = "blocked: secret material - reference the variable name in .env.example instead of reading the real file";
|
|
1732
|
+
var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
|
|
1733
|
+
var BASH_ALLOWLIST_DENIAL = "blocked: command is outside the Bash allowlist - use package install/add commands, mkdir, or git status/diff/log only";
|
|
1734
|
+
var CHAINED_COMMAND_DENIAL = "blocked: chained or substituted shell command - run one command at a time";
|
|
1735
|
+
var ENV_EXAMPLES = /* @__PURE__ */ new Set([".env.example", ".env.sample", ".env.template"]);
|
|
1736
|
+
var SECRET_DIRECTORIES = /* @__PURE__ */ new Set([".aws", ".ssh", ".gnupg"]);
|
|
1737
|
+
var SECRET_EXACT_FILES = /* @__PURE__ */ new Set([".netrc", ".npmrc", ".pypirc"]);
|
|
1738
|
+
var SECRET_SUFFIXES = [
|
|
1739
|
+
".pem",
|
|
1740
|
+
".key",
|
|
1741
|
+
".p12",
|
|
1742
|
+
".pfx",
|
|
1743
|
+
".jks",
|
|
1744
|
+
".keystore",
|
|
1745
|
+
".tfvars"
|
|
1746
|
+
];
|
|
1747
|
+
var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
1748
|
+
var PYTHON_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["pip", "pip3", "poetry", "uv"]);
|
|
1749
|
+
var RUBY_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["gem", "bundle"]);
|
|
1750
|
+
function evaluateToolUse(toolName, input, context) {
|
|
1751
|
+
switch (toolName) {
|
|
1752
|
+
case "Read":
|
|
1753
|
+
return evaluateReadLike(readPathInputs(input), readPathInputs(input), context);
|
|
1754
|
+
case "Grep":
|
|
1755
|
+
return evaluateReadLike(grepGlobPathInputs(input), grepPathInputs(input), context);
|
|
1756
|
+
case "Glob":
|
|
1757
|
+
return evaluateReadLike(grepGlobPathInputs(input), grepGlobPathInputs(input), context);
|
|
1758
|
+
case "Bash":
|
|
1759
|
+
return evaluateBash(input);
|
|
1760
|
+
case "Write":
|
|
1761
|
+
case "Edit":
|
|
1762
|
+
return evaluateWrite(input, context);
|
|
1763
|
+
default:
|
|
1764
|
+
return {
|
|
1765
|
+
behavior: "deny",
|
|
1766
|
+
message: `blocked: tool ${toolName} is not allowed by the wizard tool policy`
|
|
1767
|
+
};
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
function isSecretPathLike(value) {
|
|
1771
|
+
const parts = normalizePathLike(value).split("/").filter(Boolean);
|
|
1772
|
+
for (const rawPart of parts) {
|
|
1773
|
+
const part = stripOuterQuotes(rawPart.trim().toLowerCase());
|
|
1774
|
+
if (!part || part === "." || part === "**") continue;
|
|
1775
|
+
if (ENV_EXAMPLES.has(part)) continue;
|
|
1776
|
+
if (SECRET_DIRECTORIES.has(part)) return true;
|
|
1777
|
+
if (SECRET_EXACT_FILES.has(part)) return true;
|
|
1778
|
+
if (part === ".env" || part === ".envrc" || part.startsWith(".env.") || part.startsWith(".env*")) {
|
|
1779
|
+
return true;
|
|
1780
|
+
}
|
|
1781
|
+
if (part.startsWith("id_rsa") || part.startsWith("id_ecdsa") || part.startsWith("id_ed25519")) {
|
|
1782
|
+
return true;
|
|
1783
|
+
}
|
|
1784
|
+
if (part.includes("credentials")) return true;
|
|
1785
|
+
if (part.startsWith("secrets.")) return true;
|
|
1786
|
+
if (SECRET_SUFFIXES.some((suffix) => part.endsWith(suffix))) return true;
|
|
1787
|
+
}
|
|
1788
|
+
return false;
|
|
1789
|
+
}
|
|
1790
|
+
function isPathInsideRepo(pathValue, repoPath) {
|
|
1791
|
+
if (pathValue.includes("..")) return false;
|
|
1792
|
+
const repoRoot = resolve(repoPath);
|
|
1793
|
+
const candidate = isAbsolute(pathValue) ? resolve(pathValue) : resolve(repoRoot, pathValue);
|
|
1794
|
+
const rel = relative(repoRoot, candidate);
|
|
1795
|
+
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1796
|
+
}
|
|
1797
|
+
function evaluateReadLike(secretValues, repoPathValues, context) {
|
|
1798
|
+
for (const value of secretValues) {
|
|
1799
|
+
if (isSecretPathLike(value)) {
|
|
1800
|
+
return { behavior: "deny", message: SECRET_MATERIAL_DENIAL };
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
for (const value of repoPathValues) {
|
|
1804
|
+
if (!isPathInsideRepo(value, context.repoPath)) {
|
|
1805
|
+
return {
|
|
1806
|
+
behavior: "deny",
|
|
1807
|
+
message: "blocked: reads must stay inside the target repository"
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
return { behavior: "allow" };
|
|
1812
|
+
}
|
|
1813
|
+
function evaluateWrite(input, context) {
|
|
1814
|
+
const pathValue = firstString(input, ["file_path", "path"]);
|
|
1815
|
+
if (!pathValue) {
|
|
1816
|
+
return { behavior: "deny", message: "blocked: write target path is missing" };
|
|
1817
|
+
}
|
|
1818
|
+
if (!isPathInsideRepo(pathValue, context.repoPath)) {
|
|
1819
|
+
return { behavior: "deny", message: WRITE_OUTSIDE_REPO_DENIAL };
|
|
1820
|
+
}
|
|
1821
|
+
return { behavior: "allow" };
|
|
1822
|
+
}
|
|
1823
|
+
function evaluateBash(input) {
|
|
1824
|
+
const command = firstString(input, ["command"]);
|
|
1825
|
+
if (!command) {
|
|
1826
|
+
return { behavior: "deny", message: "blocked: Bash command is missing" };
|
|
1827
|
+
}
|
|
1828
|
+
const split = splitShellSegments(command);
|
|
1829
|
+
if (split.hasCommandSubstitution) {
|
|
1830
|
+
return { behavior: "deny", message: CHAINED_COMMAND_DENIAL };
|
|
1831
|
+
}
|
|
1832
|
+
const segmentDecisions = split.segments.map(evaluateBashSegment);
|
|
1833
|
+
const denied = segmentDecisions.find((decision) => decision.behavior === "deny");
|
|
1834
|
+
if (denied) {
|
|
1835
|
+
return split.hadChain ? { behavior: "deny", message: CHAINED_COMMAND_DENIAL } : denied;
|
|
1836
|
+
}
|
|
1837
|
+
return { behavior: "allow" };
|
|
1838
|
+
}
|
|
1839
|
+
function evaluateBashSegment(segment) {
|
|
1840
|
+
const tokens = tokenizeShellSegment(segment);
|
|
1841
|
+
if (!tokens.length) return { behavior: "deny", message: BASH_ALLOWLIST_DENIAL };
|
|
1842
|
+
const command = tokens[0]?.toLowerCase() ?? "";
|
|
1843
|
+
const second = tokens[1]?.toLowerCase() ?? "";
|
|
1844
|
+
const third = tokens[2]?.toLowerCase() ?? "";
|
|
1845
|
+
if (JS_PACKAGE_MANAGERS.has(command)) {
|
|
1846
|
+
if (second === "install" || second === "add" || second === "ci") {
|
|
1847
|
+
return { behavior: "allow" };
|
|
1848
|
+
}
|
|
1849
|
+
if (second === "pkg" && (third === "get" || third === "set")) {
|
|
1850
|
+
return { behavior: "allow" };
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (PYTHON_PACKAGE_MANAGERS.has(command)) {
|
|
1854
|
+
if (second === "install" || second === "add") {
|
|
1855
|
+
return { behavior: "allow" };
|
|
1856
|
+
}
|
|
1857
|
+
if (command === "uv" && second === "pip" && third === "install") {
|
|
1858
|
+
return { behavior: "allow" };
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (command === "composer" && (second === "install" || second === "require")) {
|
|
1862
|
+
return { behavior: "allow" };
|
|
1863
|
+
}
|
|
1864
|
+
if (command === "pod" && second === "install") {
|
|
1865
|
+
return { behavior: "allow" };
|
|
1866
|
+
}
|
|
1867
|
+
if ((command === "flutter" || command === "dart") && second === "pub") {
|
|
1868
|
+
return { behavior: "allow" };
|
|
1869
|
+
}
|
|
1870
|
+
if (RUBY_PACKAGE_MANAGERS.has(command) && (second === "install" || second === "add")) {
|
|
1871
|
+
return { behavior: "allow" };
|
|
1872
|
+
}
|
|
1873
|
+
if (command === "go" && (second === "get" || second === "mod")) {
|
|
1874
|
+
return { behavior: "allow" };
|
|
1875
|
+
}
|
|
1876
|
+
if (command === "mkdir") {
|
|
1877
|
+
return { behavior: "allow" };
|
|
1878
|
+
}
|
|
1879
|
+
if (command === "git" && (second === "status" || second === "diff" || second === "log")) {
|
|
1880
|
+
if (tokens.slice(2).some((token) => isSecretPathLike(token))) {
|
|
1881
|
+
return { behavior: "deny", message: SECRET_MATERIAL_DENIAL };
|
|
1882
|
+
}
|
|
1883
|
+
return { behavior: "allow" };
|
|
1884
|
+
}
|
|
1885
|
+
return { behavior: "deny", message: BASH_ALLOWLIST_DENIAL };
|
|
1886
|
+
}
|
|
1887
|
+
function readPathInputs(input) {
|
|
1888
|
+
return stringInputs(input, ["file_path", "path"]);
|
|
1889
|
+
}
|
|
1890
|
+
function grepGlobPathInputs(input) {
|
|
1891
|
+
return stringInputs(input, ["file_path", "path", "pattern", "glob"]);
|
|
1892
|
+
}
|
|
1893
|
+
function grepPathInputs(input) {
|
|
1894
|
+
return stringInputs(input, ["file_path", "path"]);
|
|
1895
|
+
}
|
|
1896
|
+
function stringInputs(input, keys) {
|
|
1897
|
+
const values = [];
|
|
1898
|
+
for (const key of keys) {
|
|
1899
|
+
const value = input[key];
|
|
1900
|
+
if (typeof value === "string" && value.trim()) values.push(value.trim());
|
|
1901
|
+
}
|
|
1902
|
+
return values;
|
|
1903
|
+
}
|
|
1904
|
+
function firstString(input, keys) {
|
|
1905
|
+
return stringInputs(input, keys)[0];
|
|
1906
|
+
}
|
|
1907
|
+
function normalizePathLike(value) {
|
|
1908
|
+
return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
|
|
1909
|
+
}
|
|
1910
|
+
function stripOuterQuotes(value) {
|
|
1911
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1912
|
+
return value.slice(1, -1);
|
|
1913
|
+
}
|
|
1914
|
+
return value;
|
|
1915
|
+
}
|
|
1916
|
+
function splitShellSegments(command) {
|
|
1917
|
+
const segments = [];
|
|
1918
|
+
let current = "";
|
|
1919
|
+
let quote;
|
|
1920
|
+
let escaped = false;
|
|
1921
|
+
let hadChain = false;
|
|
1922
|
+
let hasCommandSubstitution = false;
|
|
1923
|
+
const push = () => {
|
|
1924
|
+
const segment = current.trim();
|
|
1925
|
+
if (segment) segments.push(segment);
|
|
1926
|
+
current = "";
|
|
1927
|
+
};
|
|
1928
|
+
for (let i = 0; i < command.length; i++) {
|
|
1929
|
+
const ch = command[i];
|
|
1930
|
+
const next = command[i + 1];
|
|
1931
|
+
if (ch === "`" || ch === "$" && next === "(") {
|
|
1932
|
+
hasCommandSubstitution = true;
|
|
1933
|
+
}
|
|
1934
|
+
if (escaped) {
|
|
1935
|
+
current += ch;
|
|
1936
|
+
escaped = false;
|
|
1937
|
+
continue;
|
|
1938
|
+
}
|
|
1939
|
+
if (ch === "\\" && quote !== "'") {
|
|
1940
|
+
current += ch;
|
|
1941
|
+
escaped = true;
|
|
1942
|
+
continue;
|
|
1943
|
+
}
|
|
1944
|
+
if (quote) {
|
|
1945
|
+
if (ch === quote) quote = void 0;
|
|
1946
|
+
current += ch;
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
if (ch === '"' || ch === "'") {
|
|
1950
|
+
quote = ch;
|
|
1951
|
+
current += ch;
|
|
1952
|
+
continue;
|
|
1953
|
+
}
|
|
1954
|
+
const two = `${ch}${next ?? ""}`;
|
|
1955
|
+
if (two === "&&" || two === "||") {
|
|
1956
|
+
hadChain = true;
|
|
1957
|
+
push();
|
|
1958
|
+
i++;
|
|
1959
|
+
continue;
|
|
1960
|
+
}
|
|
1961
|
+
if (ch === ";" || ch === "|" || ch === "\n") {
|
|
1962
|
+
hadChain = true;
|
|
1963
|
+
push();
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
current += ch;
|
|
1967
|
+
}
|
|
1968
|
+
push();
|
|
1969
|
+
return { segments, hadChain, hasCommandSubstitution };
|
|
1970
|
+
}
|
|
1971
|
+
function tokenizeShellSegment(segment) {
|
|
1972
|
+
const tokens = [];
|
|
1973
|
+
let current = "";
|
|
1974
|
+
let quote;
|
|
1975
|
+
let escaped = false;
|
|
1976
|
+
const push = () => {
|
|
1977
|
+
if (current) tokens.push(current);
|
|
1978
|
+
current = "";
|
|
1979
|
+
};
|
|
1980
|
+
for (let i = 0; i < segment.length; i++) {
|
|
1981
|
+
const ch = segment[i];
|
|
1982
|
+
if (escaped) {
|
|
1983
|
+
current += ch;
|
|
1984
|
+
escaped = false;
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
if (ch === "\\" && quote !== "'") {
|
|
1988
|
+
escaped = true;
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
if (quote) {
|
|
1992
|
+
if (ch === quote) quote = void 0;
|
|
1993
|
+
else current += ch;
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
if (ch === '"' || ch === "'") {
|
|
1997
|
+
quote = ch;
|
|
1998
|
+
continue;
|
|
1999
|
+
}
|
|
2000
|
+
if (/\s/.test(ch)) {
|
|
2001
|
+
push();
|
|
2002
|
+
continue;
|
|
2003
|
+
}
|
|
2004
|
+
current += ch;
|
|
2005
|
+
}
|
|
2006
|
+
push();
|
|
2007
|
+
return tokens;
|
|
2008
|
+
}
|
|
2009
|
+
|
|
1713
2010
|
// src/core/agent.ts
|
|
1714
2011
|
async function runIntegrationAgent(opts) {
|
|
1715
|
-
const { repoPath, config, session, playbook, manifest, progress } = opts;
|
|
2012
|
+
const { repoPath, config, session, playbook, manifest, progress, onPlanReady } = opts;
|
|
1716
2013
|
applyModelAuthEnv(config, session);
|
|
1717
2014
|
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
1718
2015
|
const started = Date.now();
|
|
1719
2016
|
let costUsd = 0;
|
|
1720
2017
|
const summaries = [];
|
|
1721
|
-
|
|
2018
|
+
let repoMap = "";
|
|
2019
|
+
let eventOutcomes = [];
|
|
2020
|
+
const phaseTimings = [];
|
|
2021
|
+
const BUDGET_FLOOR = 0.25;
|
|
2022
|
+
if (config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2023
|
+
try {
|
|
2024
|
+
const map = await runTimedPass("Mapping your codebase", {
|
|
2025
|
+
prompt: renderRepoMapPrompt(repoPath),
|
|
2026
|
+
systemPrompt: systemPrompt9,
|
|
2027
|
+
repoPath,
|
|
2028
|
+
model: config.plannerModel,
|
|
2029
|
+
effort: "high",
|
|
2030
|
+
maxTurns: 40,
|
|
2031
|
+
budgetUsd: Math.min(config.budgetUsd - costUsd, config.budgetUsd * 0.15),
|
|
2032
|
+
allowedTools: READ_ONLY_TOOLS,
|
|
2033
|
+
progress
|
|
2034
|
+
}, phaseTimings);
|
|
2035
|
+
costUsd += map.costUsd;
|
|
2036
|
+
repoMap = sliceRepoMap(map.summary);
|
|
2037
|
+
} catch (err) {
|
|
2038
|
+
debugNote(`repo map pass failed: ${err.message}`);
|
|
2039
|
+
repoMap = "";
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
1722
2042
|
const corePrompt = [
|
|
1723
2043
|
`Integrate the Whisperr ${playbook.target.displayName} SDK \u2014 CORE SETUP ONLY.`,
|
|
1724
2044
|
`Project root: ${repoPath}`,
|
|
@@ -1743,11 +2063,12 @@ async function runIntegrationAgent(opts) {
|
|
|
1743
2063
|
"Do NOT instrument product events yet \u2014 that is a separate step. Do not run",
|
|
1744
2064
|
"the analyzer or build; the human will review the diff.",
|
|
1745
2065
|
"",
|
|
2066
|
+
...renderRepoMapSection(repoMap),
|
|
1746
2067
|
"----- IDENTIFY -----",
|
|
1747
2068
|
renderIdentifyBrief(manifest),
|
|
1748
2069
|
"----- END -----"
|
|
1749
2070
|
].join("\n");
|
|
1750
|
-
const core = await
|
|
2071
|
+
const core = await runTimedPass("Installing the SDK & wiring identify()", {
|
|
1751
2072
|
prompt: corePrompt,
|
|
1752
2073
|
systemPrompt: systemPrompt9,
|
|
1753
2074
|
repoPath,
|
|
@@ -1756,58 +2077,111 @@ async function runIntegrationAgent(opts) {
|
|
|
1756
2077
|
maxTurns: 50,
|
|
1757
2078
|
budgetUsd: config.budgetUsd - costUsd,
|
|
1758
2079
|
progress
|
|
1759
|
-
});
|
|
2080
|
+
}, phaseTimings);
|
|
1760
2081
|
costUsd += core.costUsd;
|
|
1761
2082
|
if (core.summary) summaries.push(`Core setup:
|
|
1762
2083
|
${core.summary}`);
|
|
1763
|
-
const BUDGET_FLOOR = 0.25;
|
|
1764
2084
|
let eventsComplete = true;
|
|
1765
2085
|
if (manifest.events.length > 0 && config.budgetUsd - costUsd <= BUDGET_FLOOR) {
|
|
1766
2086
|
eventsComplete = false;
|
|
2087
|
+
eventOutcomes = manifest.events.map((event) => ({
|
|
2088
|
+
event: event.eventType,
|
|
2089
|
+
outcome: "skipped",
|
|
2090
|
+
reason: "Spend limit reached during core setup."
|
|
2091
|
+
}));
|
|
1767
2092
|
summaries.push(
|
|
1768
2093
|
"Events: skipped \u2014 the spend limit was reached during core setup. Re-run with a higher WHISPERR_WIZARD_BUDGET_USD to instrument events."
|
|
1769
2094
|
);
|
|
1770
2095
|
} else if (manifest.events.length > 0) {
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
`The Whisperr ${playbook.target.displayName} SDK is already installed and`,
|
|
1774
|
-
"initialized, and identify() is wired. Now instrument the product events",
|
|
1775
|
-
"below with track().",
|
|
1776
|
-
"",
|
|
1777
|
-
"Work in importance order. Place each event at the real call site where the",
|
|
1778
|
-
"END USER performs the action (not an admin/staff path \u2014 see WHO COUNTS AS",
|
|
1779
|
-
`"THE USER"), and attach the properties you can source from what's in scope`,
|
|
1780
|
-
"there (see the property-capture rule). These events describe the same",
|
|
1781
|
-
"person identify() did, so they live on the same end-user surface. Skip fast",
|
|
1782
|
-
"when an event has no clear trigger here \u2014 spend steps placing, not exploring.",
|
|
1783
|
-
"Stop once the events that clearly exist in this app are wired.",
|
|
1784
|
-
"",
|
|
1785
|
-
"----- EVENTS -----",
|
|
1786
|
-
renderEventsBrief(manifest),
|
|
1787
|
-
"----- END -----"
|
|
1788
|
-
].join("\n");
|
|
1789
|
-
const events = await runPass({
|
|
1790
|
-
prompt: eventsPrompt,
|
|
2096
|
+
const planPass = await runTimedPass("Planning event placements", {
|
|
2097
|
+
prompt: renderEventPlanPrompt(repoPath, playbook, manifest, repoMap),
|
|
1791
2098
|
systemPrompt: systemPrompt9,
|
|
1792
2099
|
repoPath,
|
|
1793
|
-
model: config.
|
|
1794
|
-
effort:
|
|
1795
|
-
maxTurns:
|
|
2100
|
+
model: config.plannerModel,
|
|
2101
|
+
effort: "high",
|
|
2102
|
+
maxTurns: 40,
|
|
1796
2103
|
budgetUsd: config.budgetUsd - costUsd,
|
|
2104
|
+
allowedTools: READ_ONLY_TOOLS,
|
|
1797
2105
|
progress
|
|
1798
|
-
});
|
|
1799
|
-
costUsd +=
|
|
1800
|
-
|
|
1801
|
-
if (
|
|
1802
|
-
|
|
2106
|
+
}, phaseTimings);
|
|
2107
|
+
costUsd += planPass.costUsd;
|
|
2108
|
+
const plan = parseEventPlan(planPass.summary);
|
|
2109
|
+
if (!plan) {
|
|
2110
|
+
debugNote("event plan parse failed; falling back to direct event wiring");
|
|
2111
|
+
progress?.onActivity?.("Event plan was not parseable; falling back to direct wiring");
|
|
2112
|
+
const direct = await runDirectEventsPass({
|
|
2113
|
+
repoPath,
|
|
2114
|
+
config,
|
|
2115
|
+
systemPrompt: systemPrompt9,
|
|
2116
|
+
playbook,
|
|
2117
|
+
manifest,
|
|
2118
|
+
repoMap,
|
|
2119
|
+
budgetUsd: config.budgetUsd - costUsd,
|
|
2120
|
+
progress,
|
|
2121
|
+
phaseTimings
|
|
2122
|
+
});
|
|
2123
|
+
costUsd += direct.pass.costUsd;
|
|
2124
|
+
eventsComplete = !direct.pass.maxedOut;
|
|
2125
|
+
eventOutcomes = direct.eventOutcomes;
|
|
2126
|
+
if (direct.pass.summary) summaries.push(`Events:
|
|
2127
|
+
${direct.pass.summary}`);
|
|
2128
|
+
} else {
|
|
2129
|
+
let scopedPlan = planForManifest(plan, manifest);
|
|
2130
|
+
const reviewedPlan = await onPlanReady?.(scopedPlan);
|
|
2131
|
+
if (reviewedPlan !== void 0 && reviewedPlan !== null) {
|
|
2132
|
+
scopedPlan = planForManifest(reviewedPlan, manifest);
|
|
2133
|
+
}
|
|
2134
|
+
const placeEntries = scopedPlan.filter((entry) => entry.decision === "place");
|
|
2135
|
+
const unsureEntries = scopedPlan.filter((entry) => entry.decision === "unsure");
|
|
2136
|
+
let wire;
|
|
2137
|
+
if (placeEntries.length && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2138
|
+
wire = await runTimedPass("Instrumenting planned events", {
|
|
2139
|
+
prompt: renderEventWirePrompt(repoPath, playbook, repoMap, placeEntries),
|
|
2140
|
+
systemPrompt: systemPrompt9,
|
|
2141
|
+
repoPath,
|
|
2142
|
+
model: config.model,
|
|
2143
|
+
effort: "high",
|
|
2144
|
+
maxTurns: config.maxTurns,
|
|
2145
|
+
budgetUsd: config.budgetUsd - costUsd,
|
|
2146
|
+
progress
|
|
2147
|
+
}, phaseTimings);
|
|
2148
|
+
costUsd += wire.costUsd;
|
|
2149
|
+
if (wire.summary) summaries.push(`Events:
|
|
2150
|
+
${wire.summary}`);
|
|
2151
|
+
} else if (!placeEntries.length) {
|
|
2152
|
+
summaries.push("Events: no events had a confident placement in this surface.");
|
|
2153
|
+
}
|
|
2154
|
+
let wired = await scanManifestEvents(repoPath, manifest);
|
|
2155
|
+
const missedPlaceEntries = placeEntries.filter((entry) => !wired.has(entry.event));
|
|
2156
|
+
const followUpEntries = uniquePlanEntries([...missedPlaceEntries, ...unsureEntries]);
|
|
2157
|
+
let followUp;
|
|
2158
|
+
if (followUpEntries.length && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2159
|
+
followUp = await runTimedPass("Reconciling missed events", {
|
|
2160
|
+
prompt: renderEventReconcilePrompt(repoPath, playbook, repoMap, followUpEntries),
|
|
2161
|
+
systemPrompt: systemPrompt9,
|
|
2162
|
+
repoPath,
|
|
2163
|
+
model: config.plannerModel,
|
|
2164
|
+
effort: "medium",
|
|
2165
|
+
maxTurns: 15,
|
|
2166
|
+
budgetUsd: config.budgetUsd - costUsd,
|
|
2167
|
+
progress
|
|
2168
|
+
}, phaseTimings);
|
|
2169
|
+
costUsd += followUp.costUsd;
|
|
2170
|
+
if (followUp.summary) summaries.push(`Event reconciliation:
|
|
2171
|
+
${followUp.summary}`);
|
|
2172
|
+
wired = await scanManifestEvents(repoPath, manifest);
|
|
2173
|
+
}
|
|
2174
|
+
eventOutcomes = buildEventOutcomes(manifest, scopedPlan, wired);
|
|
2175
|
+
eventsComplete = !planPass.maxedOut && !(wire?.maxedOut ?? false) && !(followUp?.maxedOut ?? false);
|
|
2176
|
+
}
|
|
1803
2177
|
}
|
|
1804
2178
|
if (core.ok && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
1805
|
-
progress?.onPhase?.("Reviewing & correcting placements");
|
|
1806
2179
|
const reviewPrompt = [
|
|
1807
2180
|
`You wired the Whisperr ${playbook.target.displayName} SDK into this repo.`,
|
|
1808
2181
|
"Now AUDIT YOUR OWN WORK and fix mistakes before finishing.",
|
|
1809
2182
|
`Project root: ${repoPath}`,
|
|
1810
2183
|
"",
|
|
2184
|
+
...renderRepoMapSection(repoMap),
|
|
1811
2185
|
"Run `git diff` and read the surrounding code to see exactly what you added.",
|
|
1812
2186
|
"For every identify() and track() call, verify \u2014 and FIX in place:",
|
|
1813
2187
|
"1. Subject \u2014 keys to the END USER, never an admin/staff/operator/seller.",
|
|
@@ -1830,30 +2204,40 @@ ${events.summary}`);
|
|
|
1830
2204
|
"End with a one-line, plain-text note of what you corrected (or 'No",
|
|
1831
2205
|
"corrections needed.'), plus one line on what you proposed, if anything."
|
|
1832
2206
|
].join("\n");
|
|
1833
|
-
const review = await
|
|
2207
|
+
const review = await runTimedPass("Reviewing & correcting placements", {
|
|
1834
2208
|
prompt: reviewPrompt,
|
|
1835
2209
|
systemPrompt: systemPrompt9,
|
|
1836
2210
|
repoPath,
|
|
1837
|
-
model: config.
|
|
1838
|
-
effort:
|
|
2211
|
+
model: config.plannerModel,
|
|
2212
|
+
effort: "medium",
|
|
1839
2213
|
maxTurns: 40,
|
|
1840
2214
|
budgetUsd: config.budgetUsd - costUsd,
|
|
1841
2215
|
progress
|
|
1842
|
-
});
|
|
2216
|
+
}, phaseTimings);
|
|
1843
2217
|
costUsd += review.costUsd;
|
|
1844
2218
|
if (review.summary) summaries.push(`Review:
|
|
1845
2219
|
${review.summary}`);
|
|
2220
|
+
if (manifest.events.length) {
|
|
2221
|
+
eventOutcomes = refreshWiredOutcomes(
|
|
2222
|
+
eventOutcomes,
|
|
2223
|
+
manifest,
|
|
2224
|
+
await scanManifestEvents(repoPath, manifest)
|
|
2225
|
+
);
|
|
2226
|
+
}
|
|
1846
2227
|
}
|
|
1847
2228
|
return {
|
|
1848
2229
|
summary: summaries.join("\n\n"),
|
|
1849
2230
|
costUsd,
|
|
1850
2231
|
durationMs: Date.now() - started,
|
|
2232
|
+
phaseTimings,
|
|
1851
2233
|
coreOk: core.ok,
|
|
1852
|
-
eventsComplete
|
|
2234
|
+
eventsComplete,
|
|
2235
|
+
eventOutcomes,
|
|
2236
|
+
repoMap: repoMap || void 0
|
|
1853
2237
|
};
|
|
1854
2238
|
}
|
|
1855
2239
|
async function runAdditionsInstrumentationPass(opts) {
|
|
1856
|
-
const { repoPath, config, session, playbook, acceptedEvents, budgetUsd, progress } = opts;
|
|
2240
|
+
const { repoPath, config, session, playbook, acceptedEvents, budgetUsd, repoMap, progress } = opts;
|
|
1857
2241
|
if (!acceptedEvents.length || budgetUsd <= 0) {
|
|
1858
2242
|
return { summary: "", costUsd: 0, ran: false };
|
|
1859
2243
|
}
|
|
@@ -1882,6 +2266,7 @@ async function runAdditionsInstrumentationPass(opts) {
|
|
|
1882
2266
|
"a clear trigger after all. The SDK is already installed and initialized.",
|
|
1883
2267
|
`Project root: ${repoPath}`,
|
|
1884
2268
|
"",
|
|
2269
|
+
...renderRepoMapSection(repoMap),
|
|
1885
2270
|
"----- APPROVED NEW EVENTS -----",
|
|
1886
2271
|
...eventLines,
|
|
1887
2272
|
"",
|
|
@@ -1899,16 +2284,382 @@ async function runAdditionsInstrumentationPass(opts) {
|
|
|
1899
2284
|
});
|
|
1900
2285
|
return { summary: pass.summary, costUsd: pass.costUsd, ran: true };
|
|
1901
2286
|
}
|
|
2287
|
+
async function runRepairPass(opts) {
|
|
2288
|
+
const {
|
|
2289
|
+
repoPath,
|
|
2290
|
+
config,
|
|
2291
|
+
session,
|
|
2292
|
+
playbook,
|
|
2293
|
+
verifyCommand,
|
|
2294
|
+
verifyOutput,
|
|
2295
|
+
budgetUsd,
|
|
2296
|
+
progress
|
|
2297
|
+
} = opts;
|
|
2298
|
+
if (budgetUsd <= 0) return { summary: "", costUsd: 0, ran: false };
|
|
2299
|
+
applyModelAuthEnv(config, session);
|
|
2300
|
+
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
2301
|
+
progress?.onPhase?.("Repairing verifier failures");
|
|
2302
|
+
const prompt = [
|
|
2303
|
+
"The Whisperr integration was applied, but the verifier command failed.",
|
|
2304
|
+
`Project root: ${repoPath}`,
|
|
2305
|
+
"",
|
|
2306
|
+
"Verifier command:",
|
|
2307
|
+
verifyCommand,
|
|
2308
|
+
"",
|
|
2309
|
+
"Verifier output tail:",
|
|
2310
|
+
tail(verifyOutput, 4e3) || "(no output)",
|
|
2311
|
+
"",
|
|
2312
|
+
"Fix ONLY what the verifier reports; do not refactor. Keep the existing",
|
|
2313
|
+
"Whisperr placement intent unless a verifier error directly requires a small",
|
|
2314
|
+
"syntax/import/type correction. Do not explore unrelated code."
|
|
2315
|
+
].join("\n");
|
|
2316
|
+
const pass = await runPass({
|
|
2317
|
+
prompt,
|
|
2318
|
+
systemPrompt: systemPrompt9,
|
|
2319
|
+
repoPath,
|
|
2320
|
+
model: config.model,
|
|
2321
|
+
effort: "high",
|
|
2322
|
+
maxTurns: 20,
|
|
2323
|
+
budgetUsd,
|
|
2324
|
+
progress
|
|
2325
|
+
});
|
|
2326
|
+
return { summary: pass.summary, costUsd: pass.costUsd, ran: true };
|
|
2327
|
+
}
|
|
2328
|
+
var READ_ONLY_TOOLS = ["Read", "Glob", "Grep"];
|
|
2329
|
+
var FULL_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
2330
|
+
function parseEventPlan(text) {
|
|
2331
|
+
const parsed = parseFirstJsonArray(text);
|
|
2332
|
+
if (!Array.isArray(parsed)) return null;
|
|
2333
|
+
const entries = [];
|
|
2334
|
+
for (const item of parsed) {
|
|
2335
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2336
|
+
const obj = item;
|
|
2337
|
+
const event = typeof obj.event === "string" ? obj.event.trim() : "";
|
|
2338
|
+
const decision = obj.decision;
|
|
2339
|
+
if (!event || decision !== "place" && decision !== "skip" && decision !== "unsure") {
|
|
2340
|
+
return null;
|
|
2341
|
+
}
|
|
2342
|
+
const entry = {
|
|
2343
|
+
event,
|
|
2344
|
+
decision,
|
|
2345
|
+
reason: typeof obj.reason === "string" ? obj.reason.trim() : ""
|
|
2346
|
+
};
|
|
2347
|
+
if (typeof obj.file === "string" && obj.file.trim()) {
|
|
2348
|
+
entry.file = obj.file.trim();
|
|
2349
|
+
}
|
|
2350
|
+
if (typeof obj.anchor === "string" && obj.anchor.trim()) {
|
|
2351
|
+
entry.anchor = obj.anchor.trim();
|
|
2352
|
+
}
|
|
2353
|
+
if (Array.isArray(obj.properties)) {
|
|
2354
|
+
entry.properties = obj.properties.filter((prop) => typeof prop === "string" && !!prop.trim()).map((prop) => prop.trim());
|
|
2355
|
+
}
|
|
2356
|
+
entries.push(entry);
|
|
2357
|
+
}
|
|
2358
|
+
return entries;
|
|
2359
|
+
}
|
|
2360
|
+
function parseFirstJsonArray(text) {
|
|
2361
|
+
const start = text.indexOf("[");
|
|
2362
|
+
if (start < 0) return null;
|
|
2363
|
+
const json = sliceJsonArray(text, start);
|
|
2364
|
+
if (!json) return null;
|
|
2365
|
+
try {
|
|
2366
|
+
return JSON.parse(json);
|
|
2367
|
+
} catch {
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
function sliceJsonArray(text, start) {
|
|
2372
|
+
let depth = 0;
|
|
2373
|
+
let inString = false;
|
|
2374
|
+
let escaped = false;
|
|
2375
|
+
for (let i = start; i < text.length; i++) {
|
|
2376
|
+
const ch = text[i];
|
|
2377
|
+
if (inString) {
|
|
2378
|
+
if (escaped) {
|
|
2379
|
+
escaped = false;
|
|
2380
|
+
} else if (ch === "\\") {
|
|
2381
|
+
escaped = true;
|
|
2382
|
+
} else if (ch === '"') {
|
|
2383
|
+
inString = false;
|
|
2384
|
+
}
|
|
2385
|
+
continue;
|
|
2386
|
+
}
|
|
2387
|
+
if (ch === '"') {
|
|
2388
|
+
inString = true;
|
|
2389
|
+
} else if (ch === "[") {
|
|
2390
|
+
depth++;
|
|
2391
|
+
} else if (ch === "]") {
|
|
2392
|
+
depth--;
|
|
2393
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
return null;
|
|
2397
|
+
}
|
|
2398
|
+
function renderRepoMapPrompt(repoPath) {
|
|
2399
|
+
return [
|
|
2400
|
+
"Map this repository for a Whisperr SDK integration. This is READ-ONLY.",
|
|
2401
|
+
`Project root: ${repoPath}`,
|
|
2402
|
+
"",
|
|
2403
|
+
"Produce a concise structured repo map as final text with these exact sections:",
|
|
2404
|
+
"- framework + entry points",
|
|
2405
|
+
"- auth flows enumerated: END-USER vs admin/staff, with precise file paths",
|
|
2406
|
+
"- billing/payment/webhook surfaces",
|
|
2407
|
+
"- analytics/tracking wrapper if any",
|
|
2408
|
+
"- state management",
|
|
2409
|
+
"- directory guide: which dirs hold pages/controllers/services",
|
|
2410
|
+
"",
|
|
2411
|
+
"This map guides event placement. List file paths precisely. No prose beyond the map."
|
|
2412
|
+
].join("\n");
|
|
2413
|
+
}
|
|
2414
|
+
function sliceRepoMap(map) {
|
|
2415
|
+
return map.trim().slice(0, 8e3);
|
|
2416
|
+
}
|
|
2417
|
+
function renderRepoMapSection(repoMap) {
|
|
2418
|
+
const map = repoMap?.trim();
|
|
2419
|
+
if (!map) return [];
|
|
2420
|
+
return ["----- REPO MAP -----", map, "----- END REPO MAP -----", ""];
|
|
2421
|
+
}
|
|
2422
|
+
function renderEventPlanPrompt(repoPath, playbook, manifest, repoMap) {
|
|
2423
|
+
return [
|
|
2424
|
+
`Plan Whisperr ${playbook.target.displayName} event placements. This is READ-ONLY.`,
|
|
2425
|
+
`Project root: ${repoPath}`,
|
|
2426
|
+
"",
|
|
2427
|
+
...renderRepoMapSection(repoMap),
|
|
2428
|
+
"Use the repo map and the events brief to decide where each event belongs.",
|
|
2429
|
+
"Return ONLY a JSON code block containing an array with this shape:",
|
|
2430
|
+
'[{"event":"event_name","decision":"place|skip|unsure","file":"path","anchor":"function/route/handler","properties":["prop"],"reason":"short reason"}]',
|
|
2431
|
+
"",
|
|
2432
|
+
"Rules:",
|
|
2433
|
+
"- decision=place only when there is a concrete end-user call site in this repo.",
|
|
2434
|
+
"- decision=skip when the event clearly lives on another surface or does not exist here.",
|
|
2435
|
+
"- decision=unsure when there is a plausible surface but the exact anchor is not proven.",
|
|
2436
|
+
"- For place decisions, include file and anchor precisely.",
|
|
2437
|
+
"- properties should list only properties likely available at that anchor.",
|
|
2438
|
+
"- No prose beyond the JSON code block.",
|
|
2439
|
+
"",
|
|
2440
|
+
"----- EVENTS -----",
|
|
2441
|
+
renderEventsBrief(manifest),
|
|
2442
|
+
"----- END EVENTS -----"
|
|
2443
|
+
].join("\n");
|
|
2444
|
+
}
|
|
2445
|
+
function renderEventWirePrompt(repoPath, playbook, repoMap, entries) {
|
|
2446
|
+
return [
|
|
2447
|
+
`The Whisperr ${playbook.target.displayName} SDK is installed and initialized.`,
|
|
2448
|
+
"Wire only the planned event placements below with track().",
|
|
2449
|
+
`Project root: ${repoPath}`,
|
|
2450
|
+
"",
|
|
2451
|
+
...renderRepoMapSection(repoMap),
|
|
2452
|
+
"For each: open the file, find the anchor, add track() with the listed",
|
|
2453
|
+
"properties. If an anchor is genuinely absent, mark it unsure in your",
|
|
2454
|
+
"summary and move on. Do not re-plan skipped or unsure events in this pass.",
|
|
2455
|
+
"",
|
|
2456
|
+
"----- EVENT WIRING PLAN -----",
|
|
2457
|
+
...renderPlanEntryLines(entries),
|
|
2458
|
+
"----- END EVENT WIRING PLAN -----"
|
|
2459
|
+
].join("\n");
|
|
2460
|
+
}
|
|
2461
|
+
function renderEventReconcilePrompt(repoPath, playbook, repoMap, entries) {
|
|
2462
|
+
return [
|
|
2463
|
+
`One targeted follow-up for Whisperr ${playbook.target.displayName} events.`,
|
|
2464
|
+
`Project root: ${repoPath}`,
|
|
2465
|
+
"",
|
|
2466
|
+
...renderRepoMapSection(repoMap),
|
|
2467
|
+
"The events below were either planned for placement but not detected in the",
|
|
2468
|
+
"changed files, or were marked unsure. For each one, make one focused attempt",
|
|
2469
|
+
"at the listed file/anchor. If the anchor is absent or still not clear, skip",
|
|
2470
|
+
"it and say why in the summary. Do not search broadly or refactor.",
|
|
2471
|
+
"",
|
|
2472
|
+
"----- TARGET EVENTS -----",
|
|
2473
|
+
...renderPlanEntryLines(entries),
|
|
2474
|
+
"----- END TARGET EVENTS -----"
|
|
2475
|
+
].join("\n");
|
|
2476
|
+
}
|
|
2477
|
+
function renderPlanEntryLines(entries) {
|
|
2478
|
+
const lines = [];
|
|
2479
|
+
for (const entry of entries) {
|
|
2480
|
+
lines.push("");
|
|
2481
|
+
lines.push(`- event: ${entry.event}`);
|
|
2482
|
+
lines.push(` decision: ${entry.decision}`);
|
|
2483
|
+
if (entry.file) lines.push(` file: ${entry.file}`);
|
|
2484
|
+
if (entry.anchor) lines.push(` anchor: ${entry.anchor}`);
|
|
2485
|
+
if (entry.properties?.length) {
|
|
2486
|
+
lines.push(` properties: ${entry.properties.join(", ")}`);
|
|
2487
|
+
}
|
|
2488
|
+
if (entry.reason) lines.push(` reason: ${entry.reason}`);
|
|
2489
|
+
}
|
|
2490
|
+
return lines;
|
|
2491
|
+
}
|
|
2492
|
+
async function runDirectEventsPass(opts) {
|
|
2493
|
+
const {
|
|
2494
|
+
repoPath,
|
|
2495
|
+
config,
|
|
2496
|
+
systemPrompt: systemPrompt9,
|
|
2497
|
+
playbook,
|
|
2498
|
+
manifest,
|
|
2499
|
+
repoMap,
|
|
2500
|
+
budgetUsd,
|
|
2501
|
+
progress,
|
|
2502
|
+
phaseTimings
|
|
2503
|
+
} = opts;
|
|
2504
|
+
const eventsPrompt = [
|
|
2505
|
+
`The Whisperr ${playbook.target.displayName} SDK is already installed and`,
|
|
2506
|
+
"initialized, and identify() is wired. Now instrument the product events",
|
|
2507
|
+
"below with track().",
|
|
2508
|
+
"",
|
|
2509
|
+
...renderRepoMapSection(repoMap),
|
|
2510
|
+
"Work in importance order. Place each event at the real call site where the",
|
|
2511
|
+
"END USER performs the action (not an admin/staff path \u2014 see WHO COUNTS AS",
|
|
2512
|
+
`"THE USER"), and attach the properties you can source from what's in scope`,
|
|
2513
|
+
"there (see the property-capture rule). These events describe the same",
|
|
2514
|
+
"person identify() did, so they live on the same end-user surface. Skip fast",
|
|
2515
|
+
"when an event has no clear trigger here \u2014 spend steps placing, not exploring.",
|
|
2516
|
+
"Stop once the events that clearly exist in this app are wired.",
|
|
2517
|
+
"",
|
|
2518
|
+
"----- EVENTS -----",
|
|
2519
|
+
renderEventsBrief(manifest),
|
|
2520
|
+
"----- END -----"
|
|
2521
|
+
].join("\n");
|
|
2522
|
+
const pass = await runTimedPass("Instrumenting your events", {
|
|
2523
|
+
prompt: eventsPrompt,
|
|
2524
|
+
systemPrompt: systemPrompt9,
|
|
2525
|
+
repoPath,
|
|
2526
|
+
model: config.model,
|
|
2527
|
+
effort: config.effort,
|
|
2528
|
+
maxTurns: config.maxTurns,
|
|
2529
|
+
budgetUsd,
|
|
2530
|
+
progress
|
|
2531
|
+
}, phaseTimings);
|
|
2532
|
+
const wired = await scanManifestEvents(repoPath, manifest);
|
|
2533
|
+
return {
|
|
2534
|
+
pass,
|
|
2535
|
+
eventOutcomes: manifest.events.map(
|
|
2536
|
+
(event) => wired.has(event.eventType) ? { event: event.eventType, outcome: "wired" } : {
|
|
2537
|
+
event: event.eventType,
|
|
2538
|
+
outcome: "skipped",
|
|
2539
|
+
reason: "No track() call was detected after the direct wiring pass."
|
|
2540
|
+
}
|
|
2541
|
+
)
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
function planForManifest(plan, manifest) {
|
|
2545
|
+
const known = new Set(manifest.events.map((event) => event.eventType));
|
|
2546
|
+
return uniquePlanEntries(plan.filter((entry) => known.has(entry.event)));
|
|
2547
|
+
}
|
|
2548
|
+
function uniquePlanEntries(entries) {
|
|
2549
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2550
|
+
const unique = [];
|
|
2551
|
+
for (const entry of entries) {
|
|
2552
|
+
if (seen.has(entry.event)) continue;
|
|
2553
|
+
seen.add(entry.event);
|
|
2554
|
+
unique.push(entry);
|
|
2555
|
+
}
|
|
2556
|
+
return unique;
|
|
2557
|
+
}
|
|
2558
|
+
async function scanManifestEvents(repoPath, manifest) {
|
|
2559
|
+
return scanEvents(repoPath, manifest.events.map((event) => event.eventType));
|
|
2560
|
+
}
|
|
2561
|
+
async function scanEvents(repoPath, eventTypes) {
|
|
2562
|
+
let files = [];
|
|
2563
|
+
try {
|
|
2564
|
+
files = await changedFiles(repoPath, { isRepo: true });
|
|
2565
|
+
} catch {
|
|
2566
|
+
return /* @__PURE__ */ new Map();
|
|
2567
|
+
}
|
|
2568
|
+
if (!files.length) return /* @__PURE__ */ new Map();
|
|
2569
|
+
return scanWiredEvents(repoPath, files, eventTypes);
|
|
2570
|
+
}
|
|
2571
|
+
function buildEventOutcomes(manifest, plan, wired) {
|
|
2572
|
+
const byEvent = new Map(plan.map((entry) => [entry.event, entry]));
|
|
2573
|
+
return manifest.events.map((event) => {
|
|
2574
|
+
if (wired.has(event.eventType)) {
|
|
2575
|
+
return { event: event.eventType, outcome: "wired" };
|
|
2576
|
+
}
|
|
2577
|
+
const entry = byEvent.get(event.eventType);
|
|
2578
|
+
if (entry?.decision === "skip") {
|
|
2579
|
+
return {
|
|
2580
|
+
event: event.eventType,
|
|
2581
|
+
outcome: "skipped",
|
|
2582
|
+
reason: entry.reason || "Planner skipped this event for this surface."
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
if (entry?.decision === "unsure") {
|
|
2586
|
+
return {
|
|
2587
|
+
event: event.eventType,
|
|
2588
|
+
outcome: "skipped",
|
|
2589
|
+
reason: entry.reason || "Planner could not confirm a concrete anchor."
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
if (entry?.decision === "place") {
|
|
2593
|
+
return {
|
|
2594
|
+
event: event.eventType,
|
|
2595
|
+
outcome: "skipped",
|
|
2596
|
+
reason: "No track() call was detected after wiring and one targeted follow-up."
|
|
2597
|
+
};
|
|
2598
|
+
}
|
|
2599
|
+
return {
|
|
2600
|
+
event: event.eventType,
|
|
2601
|
+
outcome: "skipped",
|
|
2602
|
+
reason: "No placement decision was returned."
|
|
2603
|
+
};
|
|
2604
|
+
});
|
|
2605
|
+
}
|
|
2606
|
+
function refreshWiredOutcomes(current, manifest, wired) {
|
|
2607
|
+
const byEvent = new Map(current.map((outcome) => [outcome.event, outcome]));
|
|
2608
|
+
return manifest.events.map((event) => {
|
|
2609
|
+
if (wired.has(event.eventType)) {
|
|
2610
|
+
return { event: event.eventType, outcome: "wired" };
|
|
2611
|
+
}
|
|
2612
|
+
return byEvent.get(event.eventType) ?? {
|
|
2613
|
+
event: event.eventType,
|
|
2614
|
+
outcome: "skipped",
|
|
2615
|
+
reason: "No track() call was detected."
|
|
2616
|
+
};
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
function tail(s, max) {
|
|
2620
|
+
const trimmed = s.trim();
|
|
2621
|
+
return trimmed.length > max ? trimmed.slice(-max) : trimmed;
|
|
2622
|
+
}
|
|
2623
|
+
function debugNote(message) {
|
|
2624
|
+
if (process.env.WHISPERR_WIZARD_DEBUG) {
|
|
2625
|
+
console.error(`[whisperr-wizard] ${message}`);
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
async function runTimedPass(phase, opts, phaseTimings) {
|
|
2629
|
+
opts.progress?.onPhase?.(phase);
|
|
2630
|
+
const started = Date.now();
|
|
2631
|
+
try {
|
|
2632
|
+
return await runPass(opts);
|
|
2633
|
+
} finally {
|
|
2634
|
+
phaseTimings.push({ phase, ms: Date.now() - started });
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
1902
2637
|
function isLimitStop(err) {
|
|
1903
2638
|
const msg = err?.message ?? "";
|
|
1904
2639
|
return /max(imum)?[\s_-]*(turn|budget)|budget.*exceeded|number of turns/i.test(msg);
|
|
1905
2640
|
}
|
|
1906
2641
|
async function runPass(opts) {
|
|
1907
|
-
const {
|
|
2642
|
+
const {
|
|
2643
|
+
prompt,
|
|
2644
|
+
systemPrompt: systemPrompt9,
|
|
2645
|
+
repoPath,
|
|
2646
|
+
model,
|
|
2647
|
+
effort,
|
|
2648
|
+
maxTurns,
|
|
2649
|
+
budgetUsd,
|
|
2650
|
+
allowedTools = FULL_TOOLS,
|
|
2651
|
+
progress
|
|
2652
|
+
} = opts;
|
|
1908
2653
|
let summary = "";
|
|
1909
2654
|
let costUsd = 0;
|
|
1910
2655
|
let ok = false;
|
|
1911
2656
|
let maxedOut = false;
|
|
2657
|
+
let lastActivityLine = "";
|
|
2658
|
+
const emitActivity = (line) => {
|
|
2659
|
+
if (!line || line === lastActivityLine) return;
|
|
2660
|
+
lastActivityLine = line;
|
|
2661
|
+
progress?.onActivity?.(line);
|
|
2662
|
+
};
|
|
1912
2663
|
const response = query({
|
|
1913
2664
|
prompt,
|
|
1914
2665
|
options: {
|
|
@@ -1920,8 +2671,8 @@ async function runPass(opts) {
|
|
|
1920
2671
|
// defaults so the behavior is pinned regardless of SDK version.
|
|
1921
2672
|
thinking: { type: "adaptive" },
|
|
1922
2673
|
effort,
|
|
1923
|
-
|
|
1924
|
-
|
|
2674
|
+
tools: [...allowedTools],
|
|
2675
|
+
canUseTool: createToolPermissionCallback(repoPath),
|
|
1925
2676
|
maxTurns,
|
|
1926
2677
|
// Native SDK hard cost cap. Stops this pass with an `error_max_budget_usd`
|
|
1927
2678
|
// result once spend exceeds the remaining budget — this, not maxTurns, is
|
|
@@ -1935,9 +2686,9 @@ async function runPass(opts) {
|
|
|
1935
2686
|
if (message.type === "assistant") {
|
|
1936
2687
|
for (const block of message.message?.content ?? []) {
|
|
1937
2688
|
if (isTextBlock(block) && block.text?.trim()) {
|
|
1938
|
-
|
|
2689
|
+
emitActivity(firstLine(block.text));
|
|
1939
2690
|
} else if (isToolUseBlock(block)) {
|
|
1940
|
-
|
|
2691
|
+
emitActivity(describeToolUse(block));
|
|
1941
2692
|
}
|
|
1942
2693
|
}
|
|
1943
2694
|
} else if (message.type === "result") {
|
|
@@ -1957,6 +2708,16 @@ async function runPass(opts) {
|
|
|
1957
2708
|
}
|
|
1958
2709
|
return { summary, costUsd, ok, maxedOut };
|
|
1959
2710
|
}
|
|
2711
|
+
function createToolPermissionCallback(repoPath) {
|
|
2712
|
+
return async (toolName, input, options) => {
|
|
2713
|
+
const decision = evaluateToolUse(toolName, input, { repoPath });
|
|
2714
|
+
return decision.behavior === "allow" ? { behavior: "allow", toolUseID: options.toolUseID } : {
|
|
2715
|
+
behavior: "deny",
|
|
2716
|
+
message: decision.message,
|
|
2717
|
+
toolUseID: options.toolUseID
|
|
2718
|
+
};
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
1960
2721
|
function applyModelAuthEnv(config, session) {
|
|
1961
2722
|
if (config.directAnthropicKey) {
|
|
1962
2723
|
process.env.ANTHROPIC_API_KEY = config.directAnthropicKey;
|
|
@@ -2046,7 +2807,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2046
2807
|
return Promise.resolve({ ran: false, ok: true, output: "" });
|
|
2047
2808
|
}
|
|
2048
2809
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2049
|
-
return new Promise((
|
|
2810
|
+
return new Promise((resolve3) => {
|
|
2050
2811
|
const child = spawn2(command, { cwd: repoPath, shell: true, env: process.env });
|
|
2051
2812
|
let out = "";
|
|
2052
2813
|
const append = (d) => {
|
|
@@ -2057,36 +2818,36 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2057
2818
|
child.stderr?.on("data", append);
|
|
2058
2819
|
const timer = setTimeout(() => {
|
|
2059
2820
|
child.kill("SIGKILL");
|
|
2060
|
-
|
|
2821
|
+
resolve3({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
|
|
2061
2822
|
}, timeoutMs);
|
|
2062
2823
|
child.on("close", (code) => {
|
|
2063
2824
|
clearTimeout(timer);
|
|
2064
|
-
|
|
2825
|
+
resolve3({
|
|
2065
2826
|
ran: true,
|
|
2066
2827
|
ok: code === 0,
|
|
2067
2828
|
command,
|
|
2068
2829
|
exitCode: code,
|
|
2069
|
-
output:
|
|
2830
|
+
output: tail2(out),
|
|
2070
2831
|
// 127 = "command not found": the verify tool isn't on PATH here.
|
|
2071
2832
|
toolMissing: code === 127
|
|
2072
2833
|
});
|
|
2073
2834
|
});
|
|
2074
2835
|
child.on("error", () => {
|
|
2075
2836
|
clearTimeout(timer);
|
|
2076
|
-
|
|
2837
|
+
resolve3({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
|
|
2077
2838
|
});
|
|
2078
2839
|
});
|
|
2079
2840
|
}
|
|
2080
|
-
function
|
|
2841
|
+
function tail2(s) {
|
|
2081
2842
|
const t = s.trim();
|
|
2082
2843
|
return t.length > MAX_OUTPUT ? `\u2026${t.slice(-MAX_OUTPUT)}` : t;
|
|
2083
2844
|
}
|
|
2084
2845
|
|
|
2085
2846
|
// src/core/report.ts
|
|
2086
2847
|
async function postRunReport(config, session, report) {
|
|
2087
|
-
if (config.offline) return;
|
|
2848
|
+
if (config.offline) return { ok: true };
|
|
2088
2849
|
try {
|
|
2089
|
-
await fetch(`${config.apiBaseUrl}/wizard/report`, {
|
|
2850
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/report`, {
|
|
2090
2851
|
method: "POST",
|
|
2091
2852
|
headers: {
|
|
2092
2853
|
"Content-Type": "application/json",
|
|
@@ -2094,9 +2855,30 @@ async function postRunReport(config, session, report) {
|
|
|
2094
2855
|
},
|
|
2095
2856
|
body: JSON.stringify(report)
|
|
2096
2857
|
});
|
|
2097
|
-
|
|
2858
|
+
if (res.ok) return { ok: true };
|
|
2859
|
+
return {
|
|
2860
|
+
ok: false,
|
|
2861
|
+
status: res.status,
|
|
2862
|
+
detail: await responseDetail(res)
|
|
2863
|
+
};
|
|
2864
|
+
} catch (err) {
|
|
2865
|
+
return { ok: false, detail: detailSlice(errorMessage(err)) };
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
async function responseDetail(res) {
|
|
2869
|
+
try {
|
|
2870
|
+
return detailSlice(await res.text());
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
return detailSlice(errorMessage(err));
|
|
2098
2873
|
}
|
|
2099
2874
|
}
|
|
2875
|
+
function errorMessage(err) {
|
|
2876
|
+
return err instanceof Error ? err.message : String(err);
|
|
2877
|
+
}
|
|
2878
|
+
function detailSlice(detail) {
|
|
2879
|
+
const sliced = detail.slice(0, 200).trim();
|
|
2880
|
+
return sliced || void 0;
|
|
2881
|
+
}
|
|
2100
2882
|
|
|
2101
2883
|
// src/ui/banner.ts
|
|
2102
2884
|
function banner() {
|
|
@@ -2115,7 +2897,7 @@ function banner() {
|
|
|
2115
2897
|
|
|
2116
2898
|
// src/cli.ts
|
|
2117
2899
|
async function run2(options) {
|
|
2118
|
-
const repoPath =
|
|
2900
|
+
const repoPath = resolve2(options.path ?? process.cwd());
|
|
2119
2901
|
const config = resolveConfig(options);
|
|
2120
2902
|
console.log(banner());
|
|
2121
2903
|
p.intro(theme.signal("Let's wire Whisperr into your app."));
|
|
@@ -2193,45 +2975,99 @@ Flutter is live today; ${theme.bright(
|
|
|
2193
2975
|
);
|
|
2194
2976
|
}
|
|
2195
2977
|
const spin = p.spinner();
|
|
2978
|
+
const useIntegrationSpinner = Boolean(process.stdout.isTTY);
|
|
2196
2979
|
let phaseLabel = "Integrating";
|
|
2197
2980
|
let lastLine = "";
|
|
2981
|
+
let lastActivityLine = "";
|
|
2198
2982
|
const startedAt = Date.now();
|
|
2199
|
-
|
|
2200
|
-
const
|
|
2983
|
+
const nextIntegrationMessage = createMessageDeduper();
|
|
2984
|
+
const updateIntegrationMessage = () => {
|
|
2201
2985
|
const secs = Math.round((Date.now() - startedAt) / 1e3);
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
);
|
|
2205
|
-
}
|
|
2986
|
+
const message = theme.bright(phaseLabel) + theme.muted(` \xB7 ${secs}s`) + (lastLine ? theme.muted(` \u2014 ${lastLine}`) : "");
|
|
2987
|
+
const next = nextIntegrationMessage(message);
|
|
2988
|
+
if (next) spin.message(next);
|
|
2989
|
+
};
|
|
2990
|
+
if (useIntegrationSpinner) {
|
|
2991
|
+
spin.start(theme.bright(phaseLabel));
|
|
2992
|
+
} else {
|
|
2993
|
+
p.log.step(theme.bright(phaseLabel));
|
|
2994
|
+
}
|
|
2995
|
+
const tick = useIntegrationSpinner ? setInterval(updateIntegrationMessage, 1e3) : void 0;
|
|
2206
2996
|
const outcome = await runIntegrationAgent({
|
|
2207
2997
|
repoPath,
|
|
2208
2998
|
config,
|
|
2209
2999
|
session,
|
|
2210
3000
|
playbook: chosen.playbook,
|
|
2211
3001
|
manifest,
|
|
3002
|
+
...process.stdout.isTTY && process.stdin.isTTY ? {
|
|
3003
|
+
async onPlanReady(plan) {
|
|
3004
|
+
if (useIntegrationSpinner) {
|
|
3005
|
+
spin.stop(theme.success("Placement plan ready"));
|
|
3006
|
+
}
|
|
3007
|
+
p.note(renderPlacementPlan(plan), "Placement plan");
|
|
3008
|
+
const placeEntries = plan.filter((entry) => entry.decision === "place");
|
|
3009
|
+
let selectedEvents = placeEntries.map((entry) => entry.event);
|
|
3010
|
+
if (placeEntries.length) {
|
|
3011
|
+
const selection = await p.multiselect({
|
|
3012
|
+
message: "Wire these events?",
|
|
3013
|
+
options: placeEntries.map((entry) => ({
|
|
3014
|
+
value: entry.event,
|
|
3015
|
+
label: entry.event,
|
|
3016
|
+
hint: `${entry.file ?? "unknown file"}@${entry.anchor ?? "unknown anchor"}`
|
|
3017
|
+
})),
|
|
3018
|
+
initialValues: selectedEvents,
|
|
3019
|
+
required: false
|
|
3020
|
+
});
|
|
3021
|
+
selectedEvents = p.isCancel(selection) ? [] : selection;
|
|
3022
|
+
}
|
|
3023
|
+
if (useIntegrationSpinner) {
|
|
3024
|
+
spin.start(theme.bright("Continuing integration"));
|
|
3025
|
+
}
|
|
3026
|
+
return filterEventPlan(plan, selectedEvents);
|
|
3027
|
+
}
|
|
3028
|
+
} : {},
|
|
2212
3029
|
progress: {
|
|
2213
3030
|
onPhase(label) {
|
|
2214
3031
|
phaseLabel = label;
|
|
2215
3032
|
lastLine = "";
|
|
3033
|
+
lastActivityLine = "";
|
|
3034
|
+
if (useIntegrationSpinner) {
|
|
3035
|
+
updateIntegrationMessage();
|
|
3036
|
+
} else {
|
|
3037
|
+
p.log.step(theme.bright(label));
|
|
3038
|
+
}
|
|
2216
3039
|
},
|
|
2217
3040
|
onActivity(line) {
|
|
3041
|
+
if (line === lastActivityLine) return;
|
|
3042
|
+
lastActivityLine = line;
|
|
2218
3043
|
lastLine = line;
|
|
3044
|
+
if (useIntegrationSpinner) updateIntegrationMessage();
|
|
2219
3045
|
}
|
|
2220
3046
|
}
|
|
2221
3047
|
}).catch((err) => {
|
|
2222
3048
|
p.log.error(err.message);
|
|
2223
3049
|
return null;
|
|
2224
3050
|
});
|
|
2225
|
-
clearInterval(tick);
|
|
3051
|
+
if (tick) clearInterval(tick);
|
|
2226
3052
|
if (!outcome) {
|
|
2227
|
-
|
|
3053
|
+
if (useIntegrationSpinner) {
|
|
3054
|
+
spin.stop(theme.alert("Integration stopped"));
|
|
3055
|
+
} else {
|
|
3056
|
+
p.log.error(theme.alert("Integration stopped"));
|
|
3057
|
+
}
|
|
2228
3058
|
await maybeRevert(repoPath, checkpoint, "The run stopped before finishing.");
|
|
2229
3059
|
return 1;
|
|
2230
3060
|
}
|
|
2231
3061
|
const stopLabel = !outcome.coreOk ? theme.warn("Stopped early \u2014 partial setup is in your working tree") : outcome.eventsComplete ? theme.success("Integration complete") : theme.success("Core integration done") + theme.muted(" \u2014 some events left as follow-ups");
|
|
2232
|
-
|
|
3062
|
+
if (useIntegrationSpinner) {
|
|
3063
|
+
spin.stop(stopLabel);
|
|
3064
|
+
} else if (!outcome.coreOk) {
|
|
3065
|
+
p.log.warn(stopLabel);
|
|
3066
|
+
} else {
|
|
3067
|
+
p.log.success(stopLabel);
|
|
3068
|
+
}
|
|
2233
3069
|
const opportunities = await collectOpportunities(repoPath, manifest, checkpoint);
|
|
2234
|
-
|
|
3070
|
+
let files = await changedFiles(repoPath, checkpoint);
|
|
2235
3071
|
if (files.length) {
|
|
2236
3072
|
p.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
2237
3073
|
}
|
|
@@ -2254,7 +3090,7 @@ Flutter is live today; ${theme.bright(
|
|
|
2254
3090
|
const cmd = chosen.playbook.verifyCommand;
|
|
2255
3091
|
const vspin = p.spinner();
|
|
2256
3092
|
vspin.start(`Verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
2257
|
-
|
|
3093
|
+
let verdict = await runVerifyCommand(repoPath, cmd);
|
|
2258
3094
|
verified = verdictToVerified(verdict);
|
|
2259
3095
|
if (verdict.toolMissing) {
|
|
2260
3096
|
vspin.stop(
|
|
@@ -2267,18 +3103,71 @@ Flutter is live today; ${theme.bright(
|
|
|
2267
3103
|
} else if (verdict.ok) {
|
|
2268
3104
|
vspin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
2269
3105
|
} else {
|
|
2270
|
-
|
|
2271
|
-
if (
|
|
2272
|
-
|
|
3106
|
+
const remainingBudgetUsd = config.budgetUsd - outcome.costUsd;
|
|
3107
|
+
if (remainingBudgetUsd >= 0.5) {
|
|
3108
|
+
vspin.stop(
|
|
3109
|
+
theme.warn(`Verification failed \u2014 ${cmd}`) + theme.muted(" \u2014 attempting one repair pass")
|
|
3110
|
+
);
|
|
3111
|
+
const repairSpin = p.spinner();
|
|
3112
|
+
repairSpin.start(`Repairing verifier failures \u2014 ${theme.muted(cmd)}`);
|
|
3113
|
+
try {
|
|
3114
|
+
const repair = await runRepairPass({
|
|
3115
|
+
repoPath,
|
|
3116
|
+
config,
|
|
3117
|
+
session,
|
|
3118
|
+
playbook: chosen.playbook,
|
|
3119
|
+
verifyCommand: cmd,
|
|
3120
|
+
verifyOutput: verdict.output.slice(-4e3),
|
|
3121
|
+
budgetUsd: remainingBudgetUsd
|
|
3122
|
+
});
|
|
3123
|
+
outcome.costUsd += repair.costUsd;
|
|
3124
|
+
if (repair.summary.trim()) {
|
|
3125
|
+
outcome.summary = [outcome.summary, `Repair:
|
|
3126
|
+
${repair.summary}`].filter(Boolean).join("\n\n");
|
|
3127
|
+
p.log.message(repair.summary.trim());
|
|
3128
|
+
}
|
|
3129
|
+
repairSpin.stop(theme.success("Repair pass finished"));
|
|
3130
|
+
} catch (err) {
|
|
3131
|
+
repairSpin.stop(
|
|
3132
|
+
theme.warn("Repair pass failed") + theme.muted(` \u2014 ${err.message}`)
|
|
3133
|
+
);
|
|
3134
|
+
}
|
|
3135
|
+
files = await changedFiles(repoPath, checkpoint);
|
|
3136
|
+
const reverifySpin = p.spinner();
|
|
3137
|
+
reverifySpin.start(`Re-verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
3138
|
+
verdict = await runVerifyCommand(repoPath, cmd);
|
|
3139
|
+
verified = verdictToVerified(verdict);
|
|
3140
|
+
if (verdict.toolMissing) {
|
|
3141
|
+
reverifySpin.stop(
|
|
3142
|
+
theme.warn("Couldn't verify automatically") + theme.muted(` \u2014 \`${cmd}\` isn't available here. Run it yourself before committing.`)
|
|
3143
|
+
);
|
|
3144
|
+
} else if (verdict.timedOut) {
|
|
3145
|
+
reverifySpin.stop(
|
|
3146
|
+
theme.warn(`Verification timed out \u2014 \`${cmd}\``) + theme.muted(" \u2014 run it yourself before committing.")
|
|
3147
|
+
);
|
|
3148
|
+
} else if (verdict.ok) {
|
|
3149
|
+
reverifySpin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
3150
|
+
} else {
|
|
3151
|
+
reverifySpin.stop(theme.alert(`Verification still failed \u2014 ${cmd}`));
|
|
3152
|
+
}
|
|
3153
|
+
} else {
|
|
3154
|
+
vspin.stop(
|
|
3155
|
+
theme.alert(`Verification failed \u2014 ${cmd}`) + theme.muted(" \u2014 skipped repair because less than $0.50 budget remains")
|
|
3156
|
+
);
|
|
2273
3157
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
3158
|
+
if (!verdict.ok && !verdict.toolMissing && !verdict.timedOut) {
|
|
3159
|
+
if (verdict.output) {
|
|
3160
|
+
p.note(verdict.output, theme.warn("Verifier output"));
|
|
3161
|
+
}
|
|
3162
|
+
const reverted = await maybeRevert(
|
|
3163
|
+
repoPath,
|
|
3164
|
+
checkpoint,
|
|
3165
|
+
"The integration didn't pass its build/lint check, so the edits may be broken."
|
|
3166
|
+
);
|
|
3167
|
+
if (reverted) {
|
|
3168
|
+
p.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
3169
|
+
return 1;
|
|
3170
|
+
}
|
|
2282
3171
|
}
|
|
2283
3172
|
}
|
|
2284
3173
|
}
|
|
@@ -2291,7 +3180,8 @@ Flutter is live today; ${theme.bright(
|
|
|
2291
3180
|
opportunities,
|
|
2292
3181
|
fingerprint,
|
|
2293
3182
|
checkpoint,
|
|
2294
|
-
remainingBudgetUsd: config.budgetUsd - outcome.costUsd
|
|
3183
|
+
remainingBudgetUsd: config.budgetUsd - outcome.costUsd,
|
|
3184
|
+
repoMap: outcome.repoMap
|
|
2295
3185
|
});
|
|
2296
3186
|
if (instrumentationSnapshot && chosen.playbook.verifyCommand) {
|
|
2297
3187
|
const cmd = chosen.playbook.verifyCommand;
|
|
@@ -2346,7 +3236,7 @@ Flutter is live today; ${theme.bright(
|
|
|
2346
3236
|
status: wiredMap.has(eventType) ? "wired" : "skipped",
|
|
2347
3237
|
file: wiredMap.get(eventType)
|
|
2348
3238
|
}));
|
|
2349
|
-
await postRunReport(config, session, {
|
|
3239
|
+
const reportResult = await postRunReport(config, session, {
|
|
2350
3240
|
target: chosen.playbook.target.id,
|
|
2351
3241
|
repo_fingerprint: fingerprint,
|
|
2352
3242
|
identify_wired: outcome.coreOk,
|
|
@@ -2356,9 +3246,23 @@ Flutter is live today; ${theme.bright(
|
|
|
2356
3246
|
summary: outcome.summary.slice(0, 4e3),
|
|
2357
3247
|
events: reportEvents
|
|
2358
3248
|
});
|
|
3249
|
+
if (!reportResult.ok) {
|
|
3250
|
+
p.log.warn(
|
|
3251
|
+
theme.warn("Couldn't record this run's coverage on the server") + theme.muted(
|
|
3252
|
+
` (${formatReportFailure(reportResult)}) \u2014 future runs may re-propose events already wired here.`
|
|
3253
|
+
)
|
|
3254
|
+
);
|
|
3255
|
+
}
|
|
3256
|
+
const eventLines = renderEventOutcomeLines(outcome.eventOutcomes, wiredMap);
|
|
3257
|
+
if (eventLines) {
|
|
3258
|
+
p.note(eventLines, "Events");
|
|
3259
|
+
}
|
|
3260
|
+
const eventDenominator = manifest.universeSummary?.wireableHere ?? eventTypesToScan.length;
|
|
3261
|
+
const eventStatsLabel = manifest.universeSummary ? "events wired here" : "events wired";
|
|
3262
|
+
const timingDetails = outcome.phaseTimings.map(({ phase, ms }) => `${shortPhaseLabel(phase)} ${formatDuration(ms)}`).join(" \xB7 ");
|
|
2359
3263
|
p.log.info(
|
|
2360
3264
|
theme.muted(
|
|
2361
|
-
`${wiredMap.size}/${
|
|
3265
|
+
`${wiredMap.size}/${eventDenominator} ${eventStatsLabel} \xB7 ${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ` + (verified === true ? "verified \xB7 " : verified === false ? "unverified \xB7 " : "") + formatDuration(outcome.durationMs) + (timingDetails ? ` (${timingDetails})` : "")
|
|
2362
3266
|
)
|
|
2363
3267
|
);
|
|
2364
3268
|
if (!config.offline) {
|
|
@@ -2432,10 +3336,9 @@ async function offerOpportunities(opts) {
|
|
|
2432
3336
|
hint: i.description ?? i.rationale
|
|
2433
3337
|
}))
|
|
2434
3338
|
],
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
],
|
|
3339
|
+
// Opt-in per item: nothing is pre-selected, so adding to the universe is an
|
|
3340
|
+
// explicit choice rather than an opt-out the user has to notice and undo.
|
|
3341
|
+
initialValues: [],
|
|
2439
3342
|
required: false
|
|
2440
3343
|
});
|
|
2441
3344
|
if (p.isCancel(selection) || selection.length === 0) {
|
|
@@ -2497,6 +3400,13 @@ async function offerOpportunities(opts) {
|
|
|
2497
3400
|
theme.muted("Runtime policy update queued \u2014 the additions go live once it completes.")
|
|
2498
3401
|
);
|
|
2499
3402
|
}
|
|
3403
|
+
if (result.policyRegen?.status === "draft") {
|
|
3404
|
+
lines.push(
|
|
3405
|
+
theme.muted(
|
|
3406
|
+
"A review policy draft was queued \u2014 activate it in your dashboard once it finishes generating to take the additions live (your current live policy was left untouched)."
|
|
3407
|
+
)
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
2500
3410
|
p.note(lines.join("\n"), theme.signal("Universe updated"));
|
|
2501
3411
|
if (result.policyRegen?.status === "failed") {
|
|
2502
3412
|
p.log.warn(
|
|
@@ -2520,7 +3430,8 @@ async function offerOpportunities(opts) {
|
|
|
2520
3430
|
session,
|
|
2521
3431
|
playbook: opts.playbook,
|
|
2522
3432
|
acceptedEvents,
|
|
2523
|
-
budgetUsd: opts.remainingBudgetUsd
|
|
3433
|
+
budgetUsd: opts.remainingBudgetUsd,
|
|
3434
|
+
repoMap: opts.repoMap
|
|
2524
3435
|
});
|
|
2525
3436
|
if (pass.ran) {
|
|
2526
3437
|
spin.stop(theme.success("New events instrumented"));
|
|
@@ -2627,15 +3538,94 @@ async function withSpinner(label, fn) {
|
|
|
2627
3538
|
throw err;
|
|
2628
3539
|
}
|
|
2629
3540
|
}
|
|
3541
|
+
function createMessageDeduper() {
|
|
3542
|
+
let last = "";
|
|
3543
|
+
return (message) => {
|
|
3544
|
+
if (message === last) return null;
|
|
3545
|
+
last = message;
|
|
3546
|
+
return message;
|
|
3547
|
+
};
|
|
3548
|
+
}
|
|
3549
|
+
function formatDuration(ms) {
|
|
3550
|
+
const safeMs = Math.max(0, ms);
|
|
3551
|
+
const totalSeconds = Math.round(safeMs / 1e3);
|
|
3552
|
+
if (safeMs < 6e4) return `${totalSeconds}s`;
|
|
3553
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
3554
|
+
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
3555
|
+
return `${minutes}m${seconds}s`;
|
|
3556
|
+
}
|
|
3557
|
+
function filterEventPlan(plan, selectedEvents) {
|
|
3558
|
+
const selected = new Set(selectedEvents);
|
|
3559
|
+
return plan.map(
|
|
3560
|
+
(entry) => entry.decision === "place" && !selected.has(entry.event) ? { ...entry, decision: "skip", reason: "Deselected in plan review." } : entry
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3563
|
+
function renderPlacementPlan(plan) {
|
|
3564
|
+
return plan.map((entry) => {
|
|
3565
|
+
if (entry.decision === "place") {
|
|
3566
|
+
return theme.success("\u2713 ") + entry.event + theme.muted(
|
|
3567
|
+
` \u2014 ${entry.file ?? "unknown file"} @ ${entry.anchor ?? "unknown anchor"}`
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3570
|
+
const reason = entry.reason || "No reason provided.";
|
|
3571
|
+
const line = `${entry.decision === "skip" ? "\u25CB" : "?"} ${entry.event} \u2014 ${reason}`;
|
|
3572
|
+
return entry.decision === "skip" ? theme.muted(line) : theme.warn(line);
|
|
3573
|
+
}).join("\n");
|
|
3574
|
+
}
|
|
3575
|
+
function renderEventOutcomeLines(outcomes, wiredMap) {
|
|
3576
|
+
const maxLines = 40;
|
|
3577
|
+
const visibleCount = outcomes.length > maxLines ? maxLines - 1 : outcomes.length;
|
|
3578
|
+
const lines = outcomes.slice(0, visibleCount).map((outcome) => {
|
|
3579
|
+
if (outcome.outcome === "wired") {
|
|
3580
|
+
const file = wiredMap.get(outcome.event);
|
|
3581
|
+
return theme.success("\u2713 ") + outcome.event + (file ? theme.muted(` \u2014 ${file}`) : "");
|
|
3582
|
+
}
|
|
3583
|
+
return theme.muted(
|
|
3584
|
+
`\u25CB ${outcome.event} \u2014 ${outcome.reason || "No track() call was detected."}`
|
|
3585
|
+
);
|
|
3586
|
+
});
|
|
3587
|
+
const remainder = outcomes.length - visibleCount;
|
|
3588
|
+
if (remainder > 0) lines.push(theme.muted(`\u2026 ${remainder} more events`));
|
|
3589
|
+
return lines.join("\n");
|
|
3590
|
+
}
|
|
3591
|
+
function shortPhaseLabel(phase) {
|
|
3592
|
+
switch (phase) {
|
|
3593
|
+
case "Mapping your codebase":
|
|
3594
|
+
return "map";
|
|
3595
|
+
case "Installing the SDK & wiring identify()":
|
|
3596
|
+
return "core";
|
|
3597
|
+
case "Planning event placements":
|
|
3598
|
+
return "plan";
|
|
3599
|
+
case "Instrumenting planned events":
|
|
3600
|
+
case "Instrumenting your events":
|
|
3601
|
+
return "wire";
|
|
3602
|
+
case "Reconciling missed events":
|
|
3603
|
+
return "reconcile";
|
|
3604
|
+
case "Reviewing & correcting placements":
|
|
3605
|
+
return "review";
|
|
3606
|
+
default:
|
|
3607
|
+
return phase;
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
2630
3610
|
function summarizeManifest(m) {
|
|
2631
3611
|
const events = [...m.events].sort((a, b) => (b.importance ?? 0) - (a.importance ?? 0)).map((e) => theme.muted("\u2022 ") + e.eventType);
|
|
2632
3612
|
const channels = m.identify.channels?.length ? theme.muted(`channels: ${m.identify.channels.join(", ")}`) : "";
|
|
3613
|
+
const firstLine2 = m.universeSummary ? theme.bright(`${m.universeSummary.total} events in your universe`) + theme.muted(
|
|
3614
|
+
` \u2014 ${m.universeSummary.wireableHere} wireable here \xB7 ${m.universeSummary.derived} computed by Whisperr from your raw events \xB7 ${m.universeSummary.otherSurface} live on other surfaces`
|
|
3615
|
+
) : theme.bright("identify()") + theme.muted(" + ") + theme.bright(`${m.events.length} events`);
|
|
2633
3616
|
return [
|
|
2634
|
-
|
|
3617
|
+
firstLine2,
|
|
2635
3618
|
...events,
|
|
2636
3619
|
channels
|
|
2637
3620
|
].filter(Boolean).join("\n");
|
|
2638
3621
|
}
|
|
3622
|
+
function formatReportFailure(result) {
|
|
3623
|
+
const detail = result.detail?.replace(/\s+/g, " ").trim();
|
|
3624
|
+
if (result.status !== void 0) {
|
|
3625
|
+
return `HTTP ${result.status}${detail ? `: ${detail}` : ""}`;
|
|
3626
|
+
}
|
|
3627
|
+
return detail || "request failed";
|
|
3628
|
+
}
|
|
2639
3629
|
|
|
2640
3630
|
// src/index.ts
|
|
2641
3631
|
var modulePath = fileURLToPath(import.meta.url);
|