@whisperr/wizard 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -10
- package/dist/index.js +1171 -110
- 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
|
};
|
|
@@ -1146,11 +1148,9 @@ function mockManifest(appId, config) {
|
|
|
1146
1148
|
}
|
|
1147
1149
|
|
|
1148
1150
|
// src/core/agent.ts
|
|
1149
|
-
import {
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
import { readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
1153
|
-
import { join as join3 } from "path";
|
|
1151
|
+
import {
|
|
1152
|
+
query
|
|
1153
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
1154
1154
|
|
|
1155
1155
|
// src/core/git.ts
|
|
1156
1156
|
import { spawn } from "child_process";
|
|
@@ -1277,41 +1277,58 @@ async function scanWiredEvents(repoPath, files, eventTypes) {
|
|
|
1277
1277
|
const wired = /* @__PURE__ */ new Map();
|
|
1278
1278
|
const patterns = eventTypes.map((e) => ({
|
|
1279
1279
|
eventType: e,
|
|
1280
|
-
|
|
1280
|
+
trackRe: new RegExp(
|
|
1281
1281
|
`\\btrack\\s*\\(\\s*[\\s\\S]{0,160}?['"\`]${escapeRegExp(e)}['"\`]`
|
|
1282
|
-
)
|
|
1282
|
+
),
|
|
1283
|
+
literalRe: quotedLiteralRegExp(e)
|
|
1283
1284
|
}));
|
|
1285
|
+
const contents = [];
|
|
1284
1286
|
for (const file of files) {
|
|
1285
|
-
let content = "";
|
|
1286
1287
|
try {
|
|
1287
|
-
content
|
|
1288
|
+
contents.push({ file, content: await readFile2(join2(repoPath, file), "utf8") });
|
|
1288
1289
|
} catch {
|
|
1289
1290
|
continue;
|
|
1290
1291
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1292
|
+
}
|
|
1293
|
+
for (const { file, content } of contents) {
|
|
1294
|
+
for (const { eventType, trackRe } of patterns) {
|
|
1295
|
+
if (!wired.has(eventType) && trackRe.test(content)) {
|
|
1296
|
+
wired.set(eventType, file);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
for (const { file, content } of contents) {
|
|
1301
|
+
for (const { eventType, literalRe } of patterns) {
|
|
1302
|
+
if (!wired.has(eventType) && literalRe.test(content)) {
|
|
1293
1303
|
wired.set(eventType, file);
|
|
1294
1304
|
}
|
|
1295
1305
|
}
|
|
1296
1306
|
}
|
|
1297
1307
|
return wired;
|
|
1298
1308
|
}
|
|
1309
|
+
function quotedLiteralRegExp(eventType) {
|
|
1310
|
+
const leftBoundary = /^\w/.test(eventType) ? "\\b" : "";
|
|
1311
|
+
const rightBoundary = /\w$/.test(eventType) ? "\\b" : "";
|
|
1312
|
+
return new RegExp(`(['"\`])${leftBoundary}${escapeRegExp(eventType)}${rightBoundary}\\1`);
|
|
1313
|
+
}
|
|
1299
1314
|
function escapeRegExp(s) {
|
|
1300
1315
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1301
1316
|
}
|
|
1302
1317
|
function run(cwd, args) {
|
|
1303
|
-
return new Promise((
|
|
1318
|
+
return new Promise((resolve3) => {
|
|
1304
1319
|
const child = spawn("git", args, { cwd });
|
|
1305
1320
|
let stdout = "";
|
|
1306
1321
|
let stderr = "";
|
|
1307
1322
|
child.stdout.on("data", (d) => stdout += d.toString());
|
|
1308
1323
|
child.stderr.on("data", (d) => stderr += d.toString());
|
|
1309
|
-
child.on("close", (code) =>
|
|
1310
|
-
child.on("error", () =>
|
|
1324
|
+
child.on("close", (code) => resolve3({ ok: code === 0, stdout, stderr }));
|
|
1325
|
+
child.on("error", () => resolve3({ ok: false, stdout, stderr }));
|
|
1311
1326
|
});
|
|
1312
1327
|
}
|
|
1313
1328
|
|
|
1314
1329
|
// src/core/opportunities.ts
|
|
1330
|
+
import { readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
1331
|
+
import { join as join3 } from "path";
|
|
1315
1332
|
var OPPORTUNITIES_FILE = "whisperr-opportunities.json";
|
|
1316
1333
|
function normalizeCode(value) {
|
|
1317
1334
|
return value.trim().toLowerCase().replace(/[ \-./]/g, "_").replace(/[^a-z0-9_]/g, "").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
|
|
@@ -1342,9 +1359,10 @@ function sanitizeOpportunities(raw, manifest) {
|
|
|
1342
1359
|
if (typeof raw !== "object" || raw === null) return out;
|
|
1343
1360
|
const doc = raw;
|
|
1344
1361
|
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
|
-
|
|
1362
|
+
const knownInterventions = /* @__PURE__ */ new Set([
|
|
1363
|
+
...manifest.events.flatMap((e) => e.interventions ?? []).map((i) => normalizeCode(i.code)),
|
|
1364
|
+
...(manifest.interventions ?? []).map((i) => normalizeCode(i.code))
|
|
1365
|
+
]);
|
|
1348
1366
|
const seenEvents = /* @__PURE__ */ new Set();
|
|
1349
1367
|
for (const entry of asArray(doc.events)) {
|
|
1350
1368
|
const ev = coerceEvent(entry);
|
|
@@ -1710,15 +1728,344 @@ function coverageNote(coverage) {
|
|
|
1710
1728
|
return "";
|
|
1711
1729
|
}
|
|
1712
1730
|
|
|
1731
|
+
// src/core/toolPolicy.ts
|
|
1732
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
1733
|
+
var SECRET_MATERIAL_DENIAL = "blocked: secret material - reference the variable name in .env.example instead of reading the real file";
|
|
1734
|
+
var WRITE_OUTSIDE_REPO_DENIAL = "blocked: writes must stay inside the target repository";
|
|
1735
|
+
var SENSITIVE_WRITE_DENIAL = "blocked: refusing to write CI/git configuration";
|
|
1736
|
+
var BASH_ALLOWLIST_DENIAL = "blocked: command is outside the Bash allowlist - use package install/add commands, mkdir, or git status/diff/log only";
|
|
1737
|
+
var CHAINED_COMMAND_DENIAL = "blocked: chained or substituted shell command - run one command at a time";
|
|
1738
|
+
var ENV_EXAMPLES = /* @__PURE__ */ new Set([".env.example", ".env.sample", ".env.template"]);
|
|
1739
|
+
var SECRET_DIRECTORIES = /* @__PURE__ */ new Set([".aws", ".ssh", ".gnupg"]);
|
|
1740
|
+
var SECRET_EXACT_FILES = /* @__PURE__ */ new Set([".netrc", ".npmrc", ".pypirc"]);
|
|
1741
|
+
var SENSITIVE_WRITE_DIRECTORIES = /* @__PURE__ */ new Set([".git", ".circleci", ".buildkite"]);
|
|
1742
|
+
var SENSITIVE_WRITE_EXACT_FILES = /* @__PURE__ */ new Set([
|
|
1743
|
+
".gitlab-ci.yml",
|
|
1744
|
+
"azure-pipelines.yml",
|
|
1745
|
+
"bitbucket-pipelines.yml",
|
|
1746
|
+
".drone.yml",
|
|
1747
|
+
"jenkinsfile"
|
|
1748
|
+
]);
|
|
1749
|
+
var SECRET_SUFFIXES = [
|
|
1750
|
+
".pem",
|
|
1751
|
+
".key",
|
|
1752
|
+
".p12",
|
|
1753
|
+
".pfx",
|
|
1754
|
+
".jks",
|
|
1755
|
+
".keystore",
|
|
1756
|
+
".tfvars"
|
|
1757
|
+
];
|
|
1758
|
+
var JS_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["npm", "pnpm", "yarn", "bun"]);
|
|
1759
|
+
var PYTHON_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["pip", "pip3", "poetry", "uv"]);
|
|
1760
|
+
var RUBY_PACKAGE_MANAGERS = /* @__PURE__ */ new Set(["gem", "bundle"]);
|
|
1761
|
+
function evaluateToolUse(toolName, input, context) {
|
|
1762
|
+
switch (toolName) {
|
|
1763
|
+
case "Read":
|
|
1764
|
+
return evaluateReadLike(readPathInputs(input), readPathInputs(input), context);
|
|
1765
|
+
case "Grep":
|
|
1766
|
+
return evaluateReadLike(grepGlobPathInputs(input), grepPathInputs(input), context);
|
|
1767
|
+
case "Glob":
|
|
1768
|
+
return evaluateReadLike(grepGlobPathInputs(input), grepGlobPathInputs(input), context);
|
|
1769
|
+
case "Bash":
|
|
1770
|
+
return evaluateBash(input);
|
|
1771
|
+
case "Write":
|
|
1772
|
+
case "Edit":
|
|
1773
|
+
return evaluateWrite(input, context);
|
|
1774
|
+
default:
|
|
1775
|
+
return {
|
|
1776
|
+
behavior: "deny",
|
|
1777
|
+
message: `blocked: tool ${toolName} is not allowed by the wizard tool policy`
|
|
1778
|
+
};
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
function isSecretPathLike(value) {
|
|
1782
|
+
const parts = normalizePathLike(value).split("/").filter(Boolean);
|
|
1783
|
+
for (const rawPart of parts) {
|
|
1784
|
+
const part = stripOuterQuotes(rawPart.trim().toLowerCase());
|
|
1785
|
+
if (!part || part === "." || part === "**") continue;
|
|
1786
|
+
if (ENV_EXAMPLES.has(part)) continue;
|
|
1787
|
+
if (SECRET_DIRECTORIES.has(part)) return true;
|
|
1788
|
+
if (SECRET_EXACT_FILES.has(part)) return true;
|
|
1789
|
+
if (part === ".env" || part === ".envrc" || part.startsWith(".env.") || part.startsWith(".env*")) {
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
if (part.startsWith("id_rsa") || part.startsWith("id_ecdsa") || part.startsWith("id_ed25519")) {
|
|
1793
|
+
return true;
|
|
1794
|
+
}
|
|
1795
|
+
if (part.includes("credentials")) return true;
|
|
1796
|
+
if (part.startsWith("secrets.")) return true;
|
|
1797
|
+
if (SECRET_SUFFIXES.some((suffix) => part.endsWith(suffix))) return true;
|
|
1798
|
+
}
|
|
1799
|
+
return false;
|
|
1800
|
+
}
|
|
1801
|
+
function isPathInsideRepo(pathValue, repoPath) {
|
|
1802
|
+
if (pathValue.includes("..")) return false;
|
|
1803
|
+
const repoRoot = resolve(repoPath);
|
|
1804
|
+
const candidate = isAbsolute(pathValue) ? resolve(pathValue) : resolve(repoRoot, pathValue);
|
|
1805
|
+
const rel = relative(repoRoot, candidate);
|
|
1806
|
+
return rel === "" || !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
1807
|
+
}
|
|
1808
|
+
function evaluateReadLike(secretValues, repoPathValues, context) {
|
|
1809
|
+
for (const value of secretValues) {
|
|
1810
|
+
if (isSecretPathLike(value)) {
|
|
1811
|
+
return { behavior: "deny", message: SECRET_MATERIAL_DENIAL };
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
for (const value of repoPathValues) {
|
|
1815
|
+
if (!isPathInsideRepo(value, context.repoPath)) {
|
|
1816
|
+
return {
|
|
1817
|
+
behavior: "deny",
|
|
1818
|
+
message: "blocked: reads must stay inside the target repository"
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
return { behavior: "allow" };
|
|
1823
|
+
}
|
|
1824
|
+
function evaluateWrite(input, context) {
|
|
1825
|
+
const pathValue = firstString(input, ["file_path", "path"]);
|
|
1826
|
+
if (!pathValue) {
|
|
1827
|
+
return { behavior: "deny", message: "blocked: write target path is missing" };
|
|
1828
|
+
}
|
|
1829
|
+
if (!isPathInsideRepo(pathValue, context.repoPath)) {
|
|
1830
|
+
return { behavior: "deny", message: WRITE_OUTSIDE_REPO_DENIAL };
|
|
1831
|
+
}
|
|
1832
|
+
if (isSensitiveWritePath(pathValue, context.repoPath)) {
|
|
1833
|
+
return { behavior: "deny", message: SENSITIVE_WRITE_DENIAL };
|
|
1834
|
+
}
|
|
1835
|
+
return { behavior: "allow" };
|
|
1836
|
+
}
|
|
1837
|
+
function evaluateBash(input) {
|
|
1838
|
+
const command = firstString(input, ["command"]);
|
|
1839
|
+
if (!command) {
|
|
1840
|
+
return { behavior: "deny", message: "blocked: Bash command is missing" };
|
|
1841
|
+
}
|
|
1842
|
+
const split = splitShellSegments(command);
|
|
1843
|
+
if (split.hasCommandSubstitution) {
|
|
1844
|
+
return { behavior: "deny", message: CHAINED_COMMAND_DENIAL };
|
|
1845
|
+
}
|
|
1846
|
+
const segmentDecisions = split.segments.map(evaluateBashSegment);
|
|
1847
|
+
const denied = segmentDecisions.find((decision) => decision.behavior === "deny");
|
|
1848
|
+
if (denied) {
|
|
1849
|
+
return split.hadChain ? { behavior: "deny", message: CHAINED_COMMAND_DENIAL } : denied;
|
|
1850
|
+
}
|
|
1851
|
+
return { behavior: "allow" };
|
|
1852
|
+
}
|
|
1853
|
+
function evaluateBashSegment(segment) {
|
|
1854
|
+
const tokens = tokenizeShellSegment(segment);
|
|
1855
|
+
if (!tokens.length) return { behavior: "deny", message: BASH_ALLOWLIST_DENIAL };
|
|
1856
|
+
const command = tokens[0]?.toLowerCase() ?? "";
|
|
1857
|
+
const second = tokens[1]?.toLowerCase() ?? "";
|
|
1858
|
+
const third = tokens[2]?.toLowerCase() ?? "";
|
|
1859
|
+
if (JS_PACKAGE_MANAGERS.has(command)) {
|
|
1860
|
+
if (second === "install" || second === "add" || second === "ci") {
|
|
1861
|
+
return { behavior: "allow" };
|
|
1862
|
+
}
|
|
1863
|
+
if (second === "pkg" && (third === "get" || third === "set")) {
|
|
1864
|
+
return { behavior: "allow" };
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
if (PYTHON_PACKAGE_MANAGERS.has(command)) {
|
|
1868
|
+
if (second === "install" || second === "add") {
|
|
1869
|
+
return { behavior: "allow" };
|
|
1870
|
+
}
|
|
1871
|
+
if (command === "uv" && second === "pip" && third === "install") {
|
|
1872
|
+
return { behavior: "allow" };
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
if (command === "composer" && (second === "install" || second === "require")) {
|
|
1876
|
+
return { behavior: "allow" };
|
|
1877
|
+
}
|
|
1878
|
+
if (command === "pod" && second === "install") {
|
|
1879
|
+
return { behavior: "allow" };
|
|
1880
|
+
}
|
|
1881
|
+
if ((command === "flutter" || command === "dart") && second === "pub") {
|
|
1882
|
+
return { behavior: "allow" };
|
|
1883
|
+
}
|
|
1884
|
+
if (RUBY_PACKAGE_MANAGERS.has(command) && (second === "install" || second === "add")) {
|
|
1885
|
+
return { behavior: "allow" };
|
|
1886
|
+
}
|
|
1887
|
+
if (command === "go" && (second === "get" || second === "mod")) {
|
|
1888
|
+
return { behavior: "allow" };
|
|
1889
|
+
}
|
|
1890
|
+
if (command === "mkdir") {
|
|
1891
|
+
return { behavior: "allow" };
|
|
1892
|
+
}
|
|
1893
|
+
if (command === "git" && (second === "status" || second === "diff" || second === "log")) {
|
|
1894
|
+
if (tokens.slice(2).some((token) => isSecretPathLike(token))) {
|
|
1895
|
+
return { behavior: "deny", message: SECRET_MATERIAL_DENIAL };
|
|
1896
|
+
}
|
|
1897
|
+
return { behavior: "allow" };
|
|
1898
|
+
}
|
|
1899
|
+
return { behavior: "deny", message: BASH_ALLOWLIST_DENIAL };
|
|
1900
|
+
}
|
|
1901
|
+
function readPathInputs(input) {
|
|
1902
|
+
return stringInputs(input, ["file_path", "path"]);
|
|
1903
|
+
}
|
|
1904
|
+
function grepGlobPathInputs(input) {
|
|
1905
|
+
return stringInputs(input, ["file_path", "path", "pattern", "glob"]);
|
|
1906
|
+
}
|
|
1907
|
+
function grepPathInputs(input) {
|
|
1908
|
+
return stringInputs(input, ["file_path", "path"]);
|
|
1909
|
+
}
|
|
1910
|
+
function stringInputs(input, keys) {
|
|
1911
|
+
const values = [];
|
|
1912
|
+
for (const key of keys) {
|
|
1913
|
+
const value = input[key];
|
|
1914
|
+
if (typeof value === "string" && value.trim()) values.push(value.trim());
|
|
1915
|
+
}
|
|
1916
|
+
return values;
|
|
1917
|
+
}
|
|
1918
|
+
function firstString(input, keys) {
|
|
1919
|
+
return stringInputs(input, keys)[0];
|
|
1920
|
+
}
|
|
1921
|
+
function normalizePathLike(value) {
|
|
1922
|
+
return stripOuterQuotes(value.trim()).replace(/\\/g, "/");
|
|
1923
|
+
}
|
|
1924
|
+
function repoRelativePath(pathValue, repoPath) {
|
|
1925
|
+
const repoRoot = resolve(repoPath);
|
|
1926
|
+
const candidate = isAbsolute(pathValue) ? resolve(pathValue) : resolve(repoRoot, pathValue);
|
|
1927
|
+
return normalizePathLike(relative(repoRoot, candidate));
|
|
1928
|
+
}
|
|
1929
|
+
function isSensitiveWritePath(pathValue, repoPath) {
|
|
1930
|
+
const parts = repoRelativePath(pathValue, repoPath).split("/").map((part) => stripOuterQuotes(part.trim().toLowerCase())).filter(Boolean);
|
|
1931
|
+
const [first, second] = parts;
|
|
1932
|
+
if (!first) return false;
|
|
1933
|
+
if (SENSITIVE_WRITE_DIRECTORIES.has(first)) return true;
|
|
1934
|
+
if (first === ".github" && second === "workflows") return true;
|
|
1935
|
+
return parts.length === 1 && SENSITIVE_WRITE_EXACT_FILES.has(first);
|
|
1936
|
+
}
|
|
1937
|
+
function stripOuterQuotes(value) {
|
|
1938
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1939
|
+
return value.slice(1, -1);
|
|
1940
|
+
}
|
|
1941
|
+
return value;
|
|
1942
|
+
}
|
|
1943
|
+
function splitShellSegments(command) {
|
|
1944
|
+
const segments = [];
|
|
1945
|
+
let current = "";
|
|
1946
|
+
let quote;
|
|
1947
|
+
let escaped = false;
|
|
1948
|
+
let hadChain = false;
|
|
1949
|
+
let hasCommandSubstitution = false;
|
|
1950
|
+
const push = () => {
|
|
1951
|
+
const segment = current.trim();
|
|
1952
|
+
if (segment) segments.push(segment);
|
|
1953
|
+
current = "";
|
|
1954
|
+
};
|
|
1955
|
+
for (let i = 0; i < command.length; i++) {
|
|
1956
|
+
const ch = command[i];
|
|
1957
|
+
const next = command[i + 1];
|
|
1958
|
+
if (ch === "`" || ch === "$" && next === "(") {
|
|
1959
|
+
hasCommandSubstitution = true;
|
|
1960
|
+
}
|
|
1961
|
+
if (escaped) {
|
|
1962
|
+
current += ch;
|
|
1963
|
+
escaped = false;
|
|
1964
|
+
continue;
|
|
1965
|
+
}
|
|
1966
|
+
if (ch === "\\" && quote !== "'") {
|
|
1967
|
+
current += ch;
|
|
1968
|
+
escaped = true;
|
|
1969
|
+
continue;
|
|
1970
|
+
}
|
|
1971
|
+
if (quote) {
|
|
1972
|
+
if (ch === quote) quote = void 0;
|
|
1973
|
+
current += ch;
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
if (ch === '"' || ch === "'") {
|
|
1977
|
+
quote = ch;
|
|
1978
|
+
current += ch;
|
|
1979
|
+
continue;
|
|
1980
|
+
}
|
|
1981
|
+
const two = `${ch}${next ?? ""}`;
|
|
1982
|
+
if (two === "&&" || two === "||") {
|
|
1983
|
+
hadChain = true;
|
|
1984
|
+
push();
|
|
1985
|
+
i++;
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
if (ch === ";" || ch === "|" || ch === "\n") {
|
|
1989
|
+
hadChain = true;
|
|
1990
|
+
push();
|
|
1991
|
+
continue;
|
|
1992
|
+
}
|
|
1993
|
+
current += ch;
|
|
1994
|
+
}
|
|
1995
|
+
push();
|
|
1996
|
+
return { segments, hadChain, hasCommandSubstitution };
|
|
1997
|
+
}
|
|
1998
|
+
function tokenizeShellSegment(segment) {
|
|
1999
|
+
const tokens = [];
|
|
2000
|
+
let current = "";
|
|
2001
|
+
let quote;
|
|
2002
|
+
let escaped = false;
|
|
2003
|
+
const push = () => {
|
|
2004
|
+
if (current) tokens.push(current);
|
|
2005
|
+
current = "";
|
|
2006
|
+
};
|
|
2007
|
+
for (let i = 0; i < segment.length; i++) {
|
|
2008
|
+
const ch = segment[i];
|
|
2009
|
+
if (escaped) {
|
|
2010
|
+
current += ch;
|
|
2011
|
+
escaped = false;
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
if (ch === "\\" && quote !== "'") {
|
|
2015
|
+
escaped = true;
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
if (quote) {
|
|
2019
|
+
if (ch === quote) quote = void 0;
|
|
2020
|
+
else current += ch;
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
if (ch === '"' || ch === "'") {
|
|
2024
|
+
quote = ch;
|
|
2025
|
+
continue;
|
|
2026
|
+
}
|
|
2027
|
+
if (/\s/.test(ch)) {
|
|
2028
|
+
push();
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
current += ch;
|
|
2032
|
+
}
|
|
2033
|
+
push();
|
|
2034
|
+
return tokens;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
1713
2037
|
// src/core/agent.ts
|
|
1714
2038
|
async function runIntegrationAgent(opts) {
|
|
1715
|
-
const { repoPath, config, session, playbook, manifest, progress } = opts;
|
|
2039
|
+
const { repoPath, config, session, playbook, manifest, progress, onPlanReady } = opts;
|
|
1716
2040
|
applyModelAuthEnv(config, session);
|
|
1717
2041
|
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
1718
2042
|
const started = Date.now();
|
|
1719
2043
|
let costUsd = 0;
|
|
1720
2044
|
const summaries = [];
|
|
1721
|
-
|
|
2045
|
+
let repoMap = "";
|
|
2046
|
+
let eventOutcomes = [];
|
|
2047
|
+
const phaseTimings = [];
|
|
2048
|
+
const BUDGET_FLOOR = 0.25;
|
|
2049
|
+
if (config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2050
|
+
try {
|
|
2051
|
+
const map = await runTimedPass("Mapping your codebase", {
|
|
2052
|
+
prompt: renderRepoMapPrompt(repoPath),
|
|
2053
|
+
systemPrompt: systemPrompt9,
|
|
2054
|
+
repoPath,
|
|
2055
|
+
model: config.plannerModel,
|
|
2056
|
+
effort: "high",
|
|
2057
|
+
maxTurns: 40,
|
|
2058
|
+
budgetUsd: Math.min(config.budgetUsd - costUsd, config.budgetUsd * 0.15),
|
|
2059
|
+
allowedTools: READ_ONLY_TOOLS,
|
|
2060
|
+
progress
|
|
2061
|
+
}, phaseTimings);
|
|
2062
|
+
costUsd += map.costUsd;
|
|
2063
|
+
repoMap = sliceRepoMap(map.summary);
|
|
2064
|
+
} catch (err) {
|
|
2065
|
+
debugNote(`repo map pass failed: ${err.message}`);
|
|
2066
|
+
repoMap = "";
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
1722
2069
|
const corePrompt = [
|
|
1723
2070
|
`Integrate the Whisperr ${playbook.target.displayName} SDK \u2014 CORE SETUP ONLY.`,
|
|
1724
2071
|
`Project root: ${repoPath}`,
|
|
@@ -1743,11 +2090,12 @@ async function runIntegrationAgent(opts) {
|
|
|
1743
2090
|
"Do NOT instrument product events yet \u2014 that is a separate step. Do not run",
|
|
1744
2091
|
"the analyzer or build; the human will review the diff.",
|
|
1745
2092
|
"",
|
|
2093
|
+
...renderRepoMapSection(repoMap),
|
|
1746
2094
|
"----- IDENTIFY -----",
|
|
1747
2095
|
renderIdentifyBrief(manifest),
|
|
1748
2096
|
"----- END -----"
|
|
1749
2097
|
].join("\n");
|
|
1750
|
-
const core = await
|
|
2098
|
+
const core = await runTimedPass("Installing the SDK & wiring identify()", {
|
|
1751
2099
|
prompt: corePrompt,
|
|
1752
2100
|
systemPrompt: systemPrompt9,
|
|
1753
2101
|
repoPath,
|
|
@@ -1756,58 +2104,111 @@ async function runIntegrationAgent(opts) {
|
|
|
1756
2104
|
maxTurns: 50,
|
|
1757
2105
|
budgetUsd: config.budgetUsd - costUsd,
|
|
1758
2106
|
progress
|
|
1759
|
-
});
|
|
2107
|
+
}, phaseTimings);
|
|
1760
2108
|
costUsd += core.costUsd;
|
|
1761
2109
|
if (core.summary) summaries.push(`Core setup:
|
|
1762
2110
|
${core.summary}`);
|
|
1763
|
-
const BUDGET_FLOOR = 0.25;
|
|
1764
2111
|
let eventsComplete = true;
|
|
1765
2112
|
if (manifest.events.length > 0 && config.budgetUsd - costUsd <= BUDGET_FLOOR) {
|
|
1766
2113
|
eventsComplete = false;
|
|
2114
|
+
eventOutcomes = manifest.events.map((event) => ({
|
|
2115
|
+
event: event.eventType,
|
|
2116
|
+
outcome: "skipped",
|
|
2117
|
+
reason: "Spend limit reached during core setup."
|
|
2118
|
+
}));
|
|
1767
2119
|
summaries.push(
|
|
1768
2120
|
"Events: skipped \u2014 the spend limit was reached during core setup. Re-run with a higher WHISPERR_WIZARD_BUDGET_USD to instrument events."
|
|
1769
2121
|
);
|
|
1770
2122
|
} 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,
|
|
2123
|
+
const planPass = await runTimedPass("Planning event placements", {
|
|
2124
|
+
prompt: renderEventPlanPrompt(repoPath, playbook, manifest, repoMap),
|
|
1791
2125
|
systemPrompt: systemPrompt9,
|
|
1792
2126
|
repoPath,
|
|
1793
|
-
model: config.
|
|
1794
|
-
effort:
|
|
1795
|
-
maxTurns:
|
|
2127
|
+
model: config.plannerModel,
|
|
2128
|
+
effort: "high",
|
|
2129
|
+
maxTurns: 40,
|
|
1796
2130
|
budgetUsd: config.budgetUsd - costUsd,
|
|
2131
|
+
allowedTools: READ_ONLY_TOOLS,
|
|
1797
2132
|
progress
|
|
1798
|
-
});
|
|
1799
|
-
costUsd +=
|
|
1800
|
-
|
|
1801
|
-
if (
|
|
1802
|
-
|
|
2133
|
+
}, phaseTimings);
|
|
2134
|
+
costUsd += planPass.costUsd;
|
|
2135
|
+
const plan = parseEventPlan(planPass.summary);
|
|
2136
|
+
if (!plan) {
|
|
2137
|
+
debugNote("event plan parse failed; falling back to direct event wiring");
|
|
2138
|
+
progress?.onActivity?.("Event plan was not parseable; falling back to direct wiring");
|
|
2139
|
+
const direct = await runDirectEventsPass({
|
|
2140
|
+
repoPath,
|
|
2141
|
+
config,
|
|
2142
|
+
systemPrompt: systemPrompt9,
|
|
2143
|
+
playbook,
|
|
2144
|
+
manifest,
|
|
2145
|
+
repoMap,
|
|
2146
|
+
budgetUsd: config.budgetUsd - costUsd,
|
|
2147
|
+
progress,
|
|
2148
|
+
phaseTimings
|
|
2149
|
+
});
|
|
2150
|
+
costUsd += direct.pass.costUsd;
|
|
2151
|
+
eventsComplete = !direct.pass.maxedOut;
|
|
2152
|
+
eventOutcomes = direct.eventOutcomes;
|
|
2153
|
+
if (direct.pass.summary) summaries.push(`Events:
|
|
2154
|
+
${direct.pass.summary}`);
|
|
2155
|
+
} else {
|
|
2156
|
+
let scopedPlan = planForManifest(plan, manifest);
|
|
2157
|
+
const reviewedPlan = await onPlanReady?.(scopedPlan);
|
|
2158
|
+
if (reviewedPlan !== void 0 && reviewedPlan !== null) {
|
|
2159
|
+
scopedPlan = planForManifest(reviewedPlan, manifest);
|
|
2160
|
+
}
|
|
2161
|
+
const placeEntries = scopedPlan.filter((entry) => entry.decision === "place");
|
|
2162
|
+
const unsureEntries = scopedPlan.filter((entry) => entry.decision === "unsure");
|
|
2163
|
+
let wire;
|
|
2164
|
+
if (placeEntries.length && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2165
|
+
wire = await runTimedPass("Instrumenting planned events", {
|
|
2166
|
+
prompt: renderEventWirePrompt(repoPath, playbook, repoMap, placeEntries),
|
|
2167
|
+
systemPrompt: systemPrompt9,
|
|
2168
|
+
repoPath,
|
|
2169
|
+
model: config.model,
|
|
2170
|
+
effort: "high",
|
|
2171
|
+
maxTurns: config.maxTurns,
|
|
2172
|
+
budgetUsd: config.budgetUsd - costUsd,
|
|
2173
|
+
progress
|
|
2174
|
+
}, phaseTimings);
|
|
2175
|
+
costUsd += wire.costUsd;
|
|
2176
|
+
if (wire.summary) summaries.push(`Events:
|
|
2177
|
+
${wire.summary}`);
|
|
2178
|
+
} else if (!placeEntries.length) {
|
|
2179
|
+
summaries.push("Events: no events had a confident placement in this surface.");
|
|
2180
|
+
}
|
|
2181
|
+
let wired = await scanManifestEvents(repoPath, manifest);
|
|
2182
|
+
const missedPlaceEntries = placeEntries.filter((entry) => !wired.has(entry.event));
|
|
2183
|
+
const followUpEntries = uniquePlanEntries([...missedPlaceEntries, ...unsureEntries]);
|
|
2184
|
+
let followUp;
|
|
2185
|
+
if (followUpEntries.length && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
2186
|
+
followUp = await runTimedPass("Reconciling missed events", {
|
|
2187
|
+
prompt: renderEventReconcilePrompt(repoPath, playbook, repoMap, followUpEntries),
|
|
2188
|
+
systemPrompt: systemPrompt9,
|
|
2189
|
+
repoPath,
|
|
2190
|
+
model: config.plannerModel,
|
|
2191
|
+
effort: "medium",
|
|
2192
|
+
maxTurns: 15,
|
|
2193
|
+
budgetUsd: config.budgetUsd - costUsd,
|
|
2194
|
+
progress
|
|
2195
|
+
}, phaseTimings);
|
|
2196
|
+
costUsd += followUp.costUsd;
|
|
2197
|
+
if (followUp.summary) summaries.push(`Event reconciliation:
|
|
2198
|
+
${followUp.summary}`);
|
|
2199
|
+
wired = await scanManifestEvents(repoPath, manifest);
|
|
2200
|
+
}
|
|
2201
|
+
eventOutcomes = buildEventOutcomes(manifest, scopedPlan, wired);
|
|
2202
|
+
eventsComplete = !planPass.maxedOut && !(wire?.maxedOut ?? false) && !(followUp?.maxedOut ?? false);
|
|
2203
|
+
}
|
|
1803
2204
|
}
|
|
1804
2205
|
if (core.ok && config.budgetUsd - costUsd > BUDGET_FLOOR) {
|
|
1805
|
-
progress?.onPhase?.("Reviewing & correcting placements");
|
|
1806
2206
|
const reviewPrompt = [
|
|
1807
2207
|
`You wired the Whisperr ${playbook.target.displayName} SDK into this repo.`,
|
|
1808
2208
|
"Now AUDIT YOUR OWN WORK and fix mistakes before finishing.",
|
|
1809
2209
|
`Project root: ${repoPath}`,
|
|
1810
2210
|
"",
|
|
2211
|
+
...renderRepoMapSection(repoMap),
|
|
1811
2212
|
"Run `git diff` and read the surrounding code to see exactly what you added.",
|
|
1812
2213
|
"For every identify() and track() call, verify \u2014 and FIX in place:",
|
|
1813
2214
|
"1. Subject \u2014 keys to the END USER, never an admin/staff/operator/seller.",
|
|
@@ -1830,30 +2231,40 @@ ${events.summary}`);
|
|
|
1830
2231
|
"End with a one-line, plain-text note of what you corrected (or 'No",
|
|
1831
2232
|
"corrections needed.'), plus one line on what you proposed, if anything."
|
|
1832
2233
|
].join("\n");
|
|
1833
|
-
const review = await
|
|
2234
|
+
const review = await runTimedPass("Reviewing & correcting placements", {
|
|
1834
2235
|
prompt: reviewPrompt,
|
|
1835
2236
|
systemPrompt: systemPrompt9,
|
|
1836
2237
|
repoPath,
|
|
1837
|
-
model: config.
|
|
1838
|
-
effort:
|
|
2238
|
+
model: config.plannerModel,
|
|
2239
|
+
effort: "medium",
|
|
1839
2240
|
maxTurns: 40,
|
|
1840
2241
|
budgetUsd: config.budgetUsd - costUsd,
|
|
1841
2242
|
progress
|
|
1842
|
-
});
|
|
2243
|
+
}, phaseTimings);
|
|
1843
2244
|
costUsd += review.costUsd;
|
|
1844
2245
|
if (review.summary) summaries.push(`Review:
|
|
1845
2246
|
${review.summary}`);
|
|
2247
|
+
if (manifest.events.length) {
|
|
2248
|
+
eventOutcomes = refreshWiredOutcomes(
|
|
2249
|
+
eventOutcomes,
|
|
2250
|
+
manifest,
|
|
2251
|
+
await scanManifestEvents(repoPath, manifest)
|
|
2252
|
+
);
|
|
2253
|
+
}
|
|
1846
2254
|
}
|
|
1847
2255
|
return {
|
|
1848
2256
|
summary: summaries.join("\n\n"),
|
|
1849
2257
|
costUsd,
|
|
1850
2258
|
durationMs: Date.now() - started,
|
|
2259
|
+
phaseTimings,
|
|
1851
2260
|
coreOk: core.ok,
|
|
1852
|
-
eventsComplete
|
|
2261
|
+
eventsComplete,
|
|
2262
|
+
eventOutcomes,
|
|
2263
|
+
repoMap: repoMap || void 0
|
|
1853
2264
|
};
|
|
1854
2265
|
}
|
|
1855
2266
|
async function runAdditionsInstrumentationPass(opts) {
|
|
1856
|
-
const { repoPath, config, session, playbook, acceptedEvents, budgetUsd, progress } = opts;
|
|
2267
|
+
const { repoPath, config, session, playbook, acceptedEvents, budgetUsd, repoMap, progress } = opts;
|
|
1857
2268
|
if (!acceptedEvents.length || budgetUsd <= 0) {
|
|
1858
2269
|
return { summary: "", costUsd: 0, ran: false };
|
|
1859
2270
|
}
|
|
@@ -1882,6 +2293,7 @@ async function runAdditionsInstrumentationPass(opts) {
|
|
|
1882
2293
|
"a clear trigger after all. The SDK is already installed and initialized.",
|
|
1883
2294
|
`Project root: ${repoPath}`,
|
|
1884
2295
|
"",
|
|
2296
|
+
...renderRepoMapSection(repoMap),
|
|
1885
2297
|
"----- APPROVED NEW EVENTS -----",
|
|
1886
2298
|
...eventLines,
|
|
1887
2299
|
"",
|
|
@@ -1899,16 +2311,382 @@ async function runAdditionsInstrumentationPass(opts) {
|
|
|
1899
2311
|
});
|
|
1900
2312
|
return { summary: pass.summary, costUsd: pass.costUsd, ran: true };
|
|
1901
2313
|
}
|
|
2314
|
+
async function runRepairPass(opts) {
|
|
2315
|
+
const {
|
|
2316
|
+
repoPath,
|
|
2317
|
+
config,
|
|
2318
|
+
session,
|
|
2319
|
+
playbook,
|
|
2320
|
+
verifyCommand,
|
|
2321
|
+
verifyOutput,
|
|
2322
|
+
budgetUsd,
|
|
2323
|
+
progress
|
|
2324
|
+
} = opts;
|
|
2325
|
+
if (budgetUsd <= 0) return { summary: "", costUsd: 0, ran: false };
|
|
2326
|
+
applyModelAuthEnv(config, session);
|
|
2327
|
+
const systemPrompt9 = [BASE_WIZARD_PROMPT, playbook.systemPrompt].join("\n\n");
|
|
2328
|
+
progress?.onPhase?.("Repairing verifier failures");
|
|
2329
|
+
const prompt = [
|
|
2330
|
+
"The Whisperr integration was applied, but the verifier command failed.",
|
|
2331
|
+
`Project root: ${repoPath}`,
|
|
2332
|
+
"",
|
|
2333
|
+
"Verifier command:",
|
|
2334
|
+
verifyCommand,
|
|
2335
|
+
"",
|
|
2336
|
+
"Verifier output tail:",
|
|
2337
|
+
tail(verifyOutput, 4e3) || "(no output)",
|
|
2338
|
+
"",
|
|
2339
|
+
"Fix ONLY what the verifier reports; do not refactor. Keep the existing",
|
|
2340
|
+
"Whisperr placement intent unless a verifier error directly requires a small",
|
|
2341
|
+
"syntax/import/type correction. Do not explore unrelated code."
|
|
2342
|
+
].join("\n");
|
|
2343
|
+
const pass = await runPass({
|
|
2344
|
+
prompt,
|
|
2345
|
+
systemPrompt: systemPrompt9,
|
|
2346
|
+
repoPath,
|
|
2347
|
+
model: config.model,
|
|
2348
|
+
effort: "high",
|
|
2349
|
+
maxTurns: 20,
|
|
2350
|
+
budgetUsd,
|
|
2351
|
+
progress
|
|
2352
|
+
});
|
|
2353
|
+
return { summary: pass.summary, costUsd: pass.costUsd, ran: true };
|
|
2354
|
+
}
|
|
2355
|
+
var READ_ONLY_TOOLS = ["Read", "Glob", "Grep"];
|
|
2356
|
+
var FULL_TOOLS = ["Read", "Edit", "Write", "Bash", "Glob", "Grep"];
|
|
2357
|
+
function parseEventPlan(text) {
|
|
2358
|
+
const parsed = parseFirstJsonArray(text);
|
|
2359
|
+
if (!Array.isArray(parsed)) return null;
|
|
2360
|
+
const entries = [];
|
|
2361
|
+
for (const item of parsed) {
|
|
2362
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
2363
|
+
const obj = item;
|
|
2364
|
+
const event = typeof obj.event === "string" ? obj.event.trim() : "";
|
|
2365
|
+
const decision = obj.decision;
|
|
2366
|
+
if (!event || decision !== "place" && decision !== "skip" && decision !== "unsure") {
|
|
2367
|
+
return null;
|
|
2368
|
+
}
|
|
2369
|
+
const entry = {
|
|
2370
|
+
event,
|
|
2371
|
+
decision,
|
|
2372
|
+
reason: typeof obj.reason === "string" ? obj.reason.trim() : ""
|
|
2373
|
+
};
|
|
2374
|
+
if (typeof obj.file === "string" && obj.file.trim()) {
|
|
2375
|
+
entry.file = obj.file.trim();
|
|
2376
|
+
}
|
|
2377
|
+
if (typeof obj.anchor === "string" && obj.anchor.trim()) {
|
|
2378
|
+
entry.anchor = obj.anchor.trim();
|
|
2379
|
+
}
|
|
2380
|
+
if (Array.isArray(obj.properties)) {
|
|
2381
|
+
entry.properties = obj.properties.filter((prop) => typeof prop === "string" && !!prop.trim()).map((prop) => prop.trim());
|
|
2382
|
+
}
|
|
2383
|
+
entries.push(entry);
|
|
2384
|
+
}
|
|
2385
|
+
return entries;
|
|
2386
|
+
}
|
|
2387
|
+
function parseFirstJsonArray(text) {
|
|
2388
|
+
const start = text.indexOf("[");
|
|
2389
|
+
if (start < 0) return null;
|
|
2390
|
+
const json = sliceJsonArray(text, start);
|
|
2391
|
+
if (!json) return null;
|
|
2392
|
+
try {
|
|
2393
|
+
return JSON.parse(json);
|
|
2394
|
+
} catch {
|
|
2395
|
+
return null;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
function sliceJsonArray(text, start) {
|
|
2399
|
+
let depth = 0;
|
|
2400
|
+
let inString = false;
|
|
2401
|
+
let escaped = false;
|
|
2402
|
+
for (let i = start; i < text.length; i++) {
|
|
2403
|
+
const ch = text[i];
|
|
2404
|
+
if (inString) {
|
|
2405
|
+
if (escaped) {
|
|
2406
|
+
escaped = false;
|
|
2407
|
+
} else if (ch === "\\") {
|
|
2408
|
+
escaped = true;
|
|
2409
|
+
} else if (ch === '"') {
|
|
2410
|
+
inString = false;
|
|
2411
|
+
}
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
if (ch === '"') {
|
|
2415
|
+
inString = true;
|
|
2416
|
+
} else if (ch === "[") {
|
|
2417
|
+
depth++;
|
|
2418
|
+
} else if (ch === "]") {
|
|
2419
|
+
depth--;
|
|
2420
|
+
if (depth === 0) return text.slice(start, i + 1);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
return null;
|
|
2424
|
+
}
|
|
2425
|
+
function renderRepoMapPrompt(repoPath) {
|
|
2426
|
+
return [
|
|
2427
|
+
"Map this repository for a Whisperr SDK integration. This is READ-ONLY.",
|
|
2428
|
+
`Project root: ${repoPath}`,
|
|
2429
|
+
"",
|
|
2430
|
+
"Produce a concise structured repo map as final text with these exact sections:",
|
|
2431
|
+
"- framework + entry points",
|
|
2432
|
+
"- auth flows enumerated: END-USER vs admin/staff, with precise file paths",
|
|
2433
|
+
"- billing/payment/webhook surfaces",
|
|
2434
|
+
"- analytics/tracking wrapper if any",
|
|
2435
|
+
"- state management",
|
|
2436
|
+
"- directory guide: which dirs hold pages/controllers/services",
|
|
2437
|
+
"",
|
|
2438
|
+
"This map guides event placement. List file paths precisely. No prose beyond the map."
|
|
2439
|
+
].join("\n");
|
|
2440
|
+
}
|
|
2441
|
+
function sliceRepoMap(map) {
|
|
2442
|
+
return map.trim().slice(0, 8e3);
|
|
2443
|
+
}
|
|
2444
|
+
function renderRepoMapSection(repoMap) {
|
|
2445
|
+
const map = repoMap?.trim();
|
|
2446
|
+
if (!map) return [];
|
|
2447
|
+
return ["----- REPO MAP -----", map, "----- END REPO MAP -----", ""];
|
|
2448
|
+
}
|
|
2449
|
+
function renderEventPlanPrompt(repoPath, playbook, manifest, repoMap) {
|
|
2450
|
+
return [
|
|
2451
|
+
`Plan Whisperr ${playbook.target.displayName} event placements. This is READ-ONLY.`,
|
|
2452
|
+
`Project root: ${repoPath}`,
|
|
2453
|
+
"",
|
|
2454
|
+
...renderRepoMapSection(repoMap),
|
|
2455
|
+
"Use the repo map and the events brief to decide where each event belongs.",
|
|
2456
|
+
"Return ONLY a JSON code block containing an array with this shape:",
|
|
2457
|
+
'[{"event":"event_name","decision":"place|skip|unsure","file":"path","anchor":"function/route/handler","properties":["prop"],"reason":"short reason"}]',
|
|
2458
|
+
"",
|
|
2459
|
+
"Rules:",
|
|
2460
|
+
"- decision=place only when there is a concrete end-user call site in this repo.",
|
|
2461
|
+
"- decision=skip when the event clearly lives on another surface or does not exist here.",
|
|
2462
|
+
"- decision=unsure when there is a plausible surface but the exact anchor is not proven.",
|
|
2463
|
+
"- For place decisions, include file and anchor precisely.",
|
|
2464
|
+
"- properties should list only properties likely available at that anchor.",
|
|
2465
|
+
"- No prose beyond the JSON code block.",
|
|
2466
|
+
"",
|
|
2467
|
+
"----- EVENTS -----",
|
|
2468
|
+
renderEventsBrief(manifest),
|
|
2469
|
+
"----- END EVENTS -----"
|
|
2470
|
+
].join("\n");
|
|
2471
|
+
}
|
|
2472
|
+
function renderEventWirePrompt(repoPath, playbook, repoMap, entries) {
|
|
2473
|
+
return [
|
|
2474
|
+
`The Whisperr ${playbook.target.displayName} SDK is installed and initialized.`,
|
|
2475
|
+
"Wire only the planned event placements below with track().",
|
|
2476
|
+
`Project root: ${repoPath}`,
|
|
2477
|
+
"",
|
|
2478
|
+
...renderRepoMapSection(repoMap),
|
|
2479
|
+
"For each: open the file, find the anchor, add track() with the listed",
|
|
2480
|
+
"properties. If an anchor is genuinely absent, mark it unsure in your",
|
|
2481
|
+
"summary and move on. Do not re-plan skipped or unsure events in this pass.",
|
|
2482
|
+
"",
|
|
2483
|
+
"----- EVENT WIRING PLAN -----",
|
|
2484
|
+
...renderPlanEntryLines(entries),
|
|
2485
|
+
"----- END EVENT WIRING PLAN -----"
|
|
2486
|
+
].join("\n");
|
|
2487
|
+
}
|
|
2488
|
+
function renderEventReconcilePrompt(repoPath, playbook, repoMap, entries) {
|
|
2489
|
+
return [
|
|
2490
|
+
`One targeted follow-up for Whisperr ${playbook.target.displayName} events.`,
|
|
2491
|
+
`Project root: ${repoPath}`,
|
|
2492
|
+
"",
|
|
2493
|
+
...renderRepoMapSection(repoMap),
|
|
2494
|
+
"The events below were either planned for placement but not detected in the",
|
|
2495
|
+
"changed files, or were marked unsure. For each one, make one focused attempt",
|
|
2496
|
+
"at the listed file/anchor. If the anchor is absent or still not clear, skip",
|
|
2497
|
+
"it and say why in the summary. Do not search broadly or refactor.",
|
|
2498
|
+
"",
|
|
2499
|
+
"----- TARGET EVENTS -----",
|
|
2500
|
+
...renderPlanEntryLines(entries),
|
|
2501
|
+
"----- END TARGET EVENTS -----"
|
|
2502
|
+
].join("\n");
|
|
2503
|
+
}
|
|
2504
|
+
function renderPlanEntryLines(entries) {
|
|
2505
|
+
const lines = [];
|
|
2506
|
+
for (const entry of entries) {
|
|
2507
|
+
lines.push("");
|
|
2508
|
+
lines.push(`- event: ${entry.event}`);
|
|
2509
|
+
lines.push(` decision: ${entry.decision}`);
|
|
2510
|
+
if (entry.file) lines.push(` file: ${entry.file}`);
|
|
2511
|
+
if (entry.anchor) lines.push(` anchor: ${entry.anchor}`);
|
|
2512
|
+
if (entry.properties?.length) {
|
|
2513
|
+
lines.push(` properties: ${entry.properties.join(", ")}`);
|
|
2514
|
+
}
|
|
2515
|
+
if (entry.reason) lines.push(` reason: ${entry.reason}`);
|
|
2516
|
+
}
|
|
2517
|
+
return lines;
|
|
2518
|
+
}
|
|
2519
|
+
async function runDirectEventsPass(opts) {
|
|
2520
|
+
const {
|
|
2521
|
+
repoPath,
|
|
2522
|
+
config,
|
|
2523
|
+
systemPrompt: systemPrompt9,
|
|
2524
|
+
playbook,
|
|
2525
|
+
manifest,
|
|
2526
|
+
repoMap,
|
|
2527
|
+
budgetUsd,
|
|
2528
|
+
progress,
|
|
2529
|
+
phaseTimings
|
|
2530
|
+
} = opts;
|
|
2531
|
+
const eventsPrompt = [
|
|
2532
|
+
`The Whisperr ${playbook.target.displayName} SDK is already installed and`,
|
|
2533
|
+
"initialized, and identify() is wired. Now instrument the product events",
|
|
2534
|
+
"below with track().",
|
|
2535
|
+
"",
|
|
2536
|
+
...renderRepoMapSection(repoMap),
|
|
2537
|
+
"Work in importance order. Place each event at the real call site where the",
|
|
2538
|
+
"END USER performs the action (not an admin/staff path \u2014 see WHO COUNTS AS",
|
|
2539
|
+
`"THE USER"), and attach the properties you can source from what's in scope`,
|
|
2540
|
+
"there (see the property-capture rule). These events describe the same",
|
|
2541
|
+
"person identify() did, so they live on the same end-user surface. Skip fast",
|
|
2542
|
+
"when an event has no clear trigger here \u2014 spend steps placing, not exploring.",
|
|
2543
|
+
"Stop once the events that clearly exist in this app are wired.",
|
|
2544
|
+
"",
|
|
2545
|
+
"----- EVENTS -----",
|
|
2546
|
+
renderEventsBrief(manifest),
|
|
2547
|
+
"----- END -----"
|
|
2548
|
+
].join("\n");
|
|
2549
|
+
const pass = await runTimedPass("Instrumenting your events", {
|
|
2550
|
+
prompt: eventsPrompt,
|
|
2551
|
+
systemPrompt: systemPrompt9,
|
|
2552
|
+
repoPath,
|
|
2553
|
+
model: config.model,
|
|
2554
|
+
effort: config.effort,
|
|
2555
|
+
maxTurns: config.maxTurns,
|
|
2556
|
+
budgetUsd,
|
|
2557
|
+
progress
|
|
2558
|
+
}, phaseTimings);
|
|
2559
|
+
const wired = await scanManifestEvents(repoPath, manifest);
|
|
2560
|
+
return {
|
|
2561
|
+
pass,
|
|
2562
|
+
eventOutcomes: manifest.events.map(
|
|
2563
|
+
(event) => wired.has(event.eventType) ? { event: event.eventType, outcome: "wired" } : {
|
|
2564
|
+
event: event.eventType,
|
|
2565
|
+
outcome: "skipped",
|
|
2566
|
+
reason: "No track() call was detected after the direct wiring pass."
|
|
2567
|
+
}
|
|
2568
|
+
)
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
function planForManifest(plan, manifest) {
|
|
2572
|
+
const known = new Set(manifest.events.map((event) => event.eventType));
|
|
2573
|
+
return uniquePlanEntries(plan.filter((entry) => known.has(entry.event)));
|
|
2574
|
+
}
|
|
2575
|
+
function uniquePlanEntries(entries) {
|
|
2576
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2577
|
+
const unique = [];
|
|
2578
|
+
for (const entry of entries) {
|
|
2579
|
+
if (seen.has(entry.event)) continue;
|
|
2580
|
+
seen.add(entry.event);
|
|
2581
|
+
unique.push(entry);
|
|
2582
|
+
}
|
|
2583
|
+
return unique;
|
|
2584
|
+
}
|
|
2585
|
+
async function scanManifestEvents(repoPath, manifest) {
|
|
2586
|
+
return scanEvents(repoPath, manifest.events.map((event) => event.eventType));
|
|
2587
|
+
}
|
|
2588
|
+
async function scanEvents(repoPath, eventTypes) {
|
|
2589
|
+
let files = [];
|
|
2590
|
+
try {
|
|
2591
|
+
files = await changedFiles(repoPath, { isRepo: true });
|
|
2592
|
+
} catch {
|
|
2593
|
+
return /* @__PURE__ */ new Map();
|
|
2594
|
+
}
|
|
2595
|
+
if (!files.length) return /* @__PURE__ */ new Map();
|
|
2596
|
+
return scanWiredEvents(repoPath, files, eventTypes);
|
|
2597
|
+
}
|
|
2598
|
+
function buildEventOutcomes(manifest, plan, wired) {
|
|
2599
|
+
const byEvent = new Map(plan.map((entry) => [entry.event, entry]));
|
|
2600
|
+
return manifest.events.map((event) => {
|
|
2601
|
+
if (wired.has(event.eventType)) {
|
|
2602
|
+
return { event: event.eventType, outcome: "wired" };
|
|
2603
|
+
}
|
|
2604
|
+
const entry = byEvent.get(event.eventType);
|
|
2605
|
+
if (entry?.decision === "skip") {
|
|
2606
|
+
return {
|
|
2607
|
+
event: event.eventType,
|
|
2608
|
+
outcome: "skipped",
|
|
2609
|
+
reason: entry.reason || "Planner skipped this event for this surface."
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
if (entry?.decision === "unsure") {
|
|
2613
|
+
return {
|
|
2614
|
+
event: event.eventType,
|
|
2615
|
+
outcome: "skipped",
|
|
2616
|
+
reason: entry.reason || "Planner could not confirm a concrete anchor."
|
|
2617
|
+
};
|
|
2618
|
+
}
|
|
2619
|
+
if (entry?.decision === "place") {
|
|
2620
|
+
return {
|
|
2621
|
+
event: event.eventType,
|
|
2622
|
+
outcome: "skipped",
|
|
2623
|
+
reason: "No track() call was detected after wiring and one targeted follow-up."
|
|
2624
|
+
};
|
|
2625
|
+
}
|
|
2626
|
+
return {
|
|
2627
|
+
event: event.eventType,
|
|
2628
|
+
outcome: "skipped",
|
|
2629
|
+
reason: "No placement decision was returned."
|
|
2630
|
+
};
|
|
2631
|
+
});
|
|
2632
|
+
}
|
|
2633
|
+
function refreshWiredOutcomes(current, manifest, wired) {
|
|
2634
|
+
const byEvent = new Map(current.map((outcome) => [outcome.event, outcome]));
|
|
2635
|
+
return manifest.events.map((event) => {
|
|
2636
|
+
if (wired.has(event.eventType)) {
|
|
2637
|
+
return { event: event.eventType, outcome: "wired" };
|
|
2638
|
+
}
|
|
2639
|
+
return byEvent.get(event.eventType) ?? {
|
|
2640
|
+
event: event.eventType,
|
|
2641
|
+
outcome: "skipped",
|
|
2642
|
+
reason: "No track() call was detected."
|
|
2643
|
+
};
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
function tail(s, max) {
|
|
2647
|
+
const trimmed = s.trim();
|
|
2648
|
+
return trimmed.length > max ? trimmed.slice(-max) : trimmed;
|
|
2649
|
+
}
|
|
2650
|
+
function debugNote(message) {
|
|
2651
|
+
if (process.env.WHISPERR_WIZARD_DEBUG) {
|
|
2652
|
+
console.error(`[whisperr-wizard] ${message}`);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
async function runTimedPass(phase, opts, phaseTimings) {
|
|
2656
|
+
opts.progress?.onPhase?.(phase);
|
|
2657
|
+
const started = Date.now();
|
|
2658
|
+
try {
|
|
2659
|
+
return await runPass(opts);
|
|
2660
|
+
} finally {
|
|
2661
|
+
phaseTimings.push({ phase, ms: Date.now() - started });
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
1902
2664
|
function isLimitStop(err) {
|
|
1903
2665
|
const msg = err?.message ?? "";
|
|
1904
2666
|
return /max(imum)?[\s_-]*(turn|budget)|budget.*exceeded|number of turns/i.test(msg);
|
|
1905
2667
|
}
|
|
1906
2668
|
async function runPass(opts) {
|
|
1907
|
-
const {
|
|
2669
|
+
const {
|
|
2670
|
+
prompt,
|
|
2671
|
+
systemPrompt: systemPrompt9,
|
|
2672
|
+
repoPath,
|
|
2673
|
+
model,
|
|
2674
|
+
effort,
|
|
2675
|
+
maxTurns,
|
|
2676
|
+
budgetUsd,
|
|
2677
|
+
allowedTools = FULL_TOOLS,
|
|
2678
|
+
progress
|
|
2679
|
+
} = opts;
|
|
1908
2680
|
let summary = "";
|
|
1909
2681
|
let costUsd = 0;
|
|
1910
2682
|
let ok = false;
|
|
1911
2683
|
let maxedOut = false;
|
|
2684
|
+
let lastActivityLine = "";
|
|
2685
|
+
const emitActivity = (line) => {
|
|
2686
|
+
if (!line || line === lastActivityLine) return;
|
|
2687
|
+
lastActivityLine = line;
|
|
2688
|
+
progress?.onActivity?.(line);
|
|
2689
|
+
};
|
|
1912
2690
|
const response = query({
|
|
1913
2691
|
prompt,
|
|
1914
2692
|
options: {
|
|
@@ -1920,8 +2698,9 @@ async function runPass(opts) {
|
|
|
1920
2698
|
// defaults so the behavior is pinned regardless of SDK version.
|
|
1921
2699
|
thinking: { type: "adaptive" },
|
|
1922
2700
|
effort,
|
|
1923
|
-
|
|
1924
|
-
|
|
2701
|
+
tools: [...allowedTools],
|
|
2702
|
+
hooks: buildToolPolicyHooks(repoPath),
|
|
2703
|
+
canUseTool: createToolPermissionCallback(repoPath),
|
|
1925
2704
|
maxTurns,
|
|
1926
2705
|
// Native SDK hard cost cap. Stops this pass with an `error_max_budget_usd`
|
|
1927
2706
|
// result once spend exceeds the remaining budget — this, not maxTurns, is
|
|
@@ -1935,9 +2714,9 @@ async function runPass(opts) {
|
|
|
1935
2714
|
if (message.type === "assistant") {
|
|
1936
2715
|
for (const block of message.message?.content ?? []) {
|
|
1937
2716
|
if (isTextBlock(block) && block.text?.trim()) {
|
|
1938
|
-
|
|
2717
|
+
emitActivity(firstLine(block.text));
|
|
1939
2718
|
} else if (isToolUseBlock(block)) {
|
|
1940
|
-
|
|
2719
|
+
emitActivity(describeToolUse(block));
|
|
1941
2720
|
}
|
|
1942
2721
|
}
|
|
1943
2722
|
} else if (message.type === "result") {
|
|
@@ -1957,6 +2736,48 @@ async function runPass(opts) {
|
|
|
1957
2736
|
}
|
|
1958
2737
|
return { summary, costUsd, ok, maxedOut };
|
|
1959
2738
|
}
|
|
2739
|
+
function buildToolPolicyHooks(repoPath) {
|
|
2740
|
+
return {
|
|
2741
|
+
PreToolUse: [
|
|
2742
|
+
{
|
|
2743
|
+
hooks: [
|
|
2744
|
+
async (input) => {
|
|
2745
|
+
if (input.hook_event_name !== "PreToolUse") return { continue: true };
|
|
2746
|
+
const decision = evaluateToolUse(input.tool_name, toolInputRecord(input.tool_input), {
|
|
2747
|
+
repoPath
|
|
2748
|
+
});
|
|
2749
|
+
if (decision.behavior === "allow") {
|
|
2750
|
+
return { continue: true };
|
|
2751
|
+
}
|
|
2752
|
+
return {
|
|
2753
|
+
hookSpecificOutput: {
|
|
2754
|
+
hookEventName: "PreToolUse",
|
|
2755
|
+
permissionDecision: "deny",
|
|
2756
|
+
permissionDecisionReason: decision.message
|
|
2757
|
+
}
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
]
|
|
2761
|
+
}
|
|
2762
|
+
]
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
function createToolPermissionCallback(repoPath) {
|
|
2766
|
+
return async (toolName, input, options) => {
|
|
2767
|
+
const decision = evaluateToolUse(toolName, input, { repoPath });
|
|
2768
|
+
return decision.behavior === "allow" ? { behavior: "allow", toolUseID: options.toolUseID } : {
|
|
2769
|
+
behavior: "deny",
|
|
2770
|
+
message: decision.message,
|
|
2771
|
+
toolUseID: options.toolUseID
|
|
2772
|
+
};
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
function toolInputRecord(input) {
|
|
2776
|
+
if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
2777
|
+
return input;
|
|
2778
|
+
}
|
|
2779
|
+
return {};
|
|
2780
|
+
}
|
|
1960
2781
|
function applyModelAuthEnv(config, session) {
|
|
1961
2782
|
if (config.directAnthropicKey) {
|
|
1962
2783
|
process.env.ANTHROPIC_API_KEY = config.directAnthropicKey;
|
|
@@ -2046,7 +2867,7 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2046
2867
|
return Promise.resolve({ ran: false, ok: true, output: "" });
|
|
2047
2868
|
}
|
|
2048
2869
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2049
|
-
return new Promise((
|
|
2870
|
+
return new Promise((resolve3) => {
|
|
2050
2871
|
const child = spawn2(command, { cwd: repoPath, shell: true, env: process.env });
|
|
2051
2872
|
let out = "";
|
|
2052
2873
|
const append = (d) => {
|
|
@@ -2057,36 +2878,42 @@ function runVerifyCommand(repoPath, command, opts = {}) {
|
|
|
2057
2878
|
child.stderr?.on("data", append);
|
|
2058
2879
|
const timer = setTimeout(() => {
|
|
2059
2880
|
child.kill("SIGKILL");
|
|
2060
|
-
|
|
2881
|
+
resolve3({ ran: true, ok: false, command, output: tail2(out), timedOut: true });
|
|
2061
2882
|
}, timeoutMs);
|
|
2062
2883
|
child.on("close", (code) => {
|
|
2063
2884
|
clearTimeout(timer);
|
|
2064
|
-
|
|
2885
|
+
resolve3({
|
|
2065
2886
|
ran: true,
|
|
2066
2887
|
ok: code === 0,
|
|
2067
2888
|
command,
|
|
2068
2889
|
exitCode: code,
|
|
2069
|
-
output:
|
|
2890
|
+
output: tail2(out),
|
|
2070
2891
|
// 127 = "command not found": the verify tool isn't on PATH here.
|
|
2071
2892
|
toolMissing: code === 127
|
|
2072
2893
|
});
|
|
2073
2894
|
});
|
|
2074
2895
|
child.on("error", () => {
|
|
2075
2896
|
clearTimeout(timer);
|
|
2076
|
-
|
|
2897
|
+
resolve3({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
|
|
2077
2898
|
});
|
|
2078
2899
|
});
|
|
2079
2900
|
}
|
|
2080
|
-
function
|
|
2901
|
+
function tail2(s) {
|
|
2081
2902
|
const t = s.trim();
|
|
2082
2903
|
return t.length > MAX_OUTPUT ? `\u2026${t.slice(-MAX_OUTPUT)}` : t;
|
|
2083
2904
|
}
|
|
2084
2905
|
|
|
2085
2906
|
// src/core/report.ts
|
|
2907
|
+
function scrubSummary(text) {
|
|
2908
|
+
return scrubTerminalControls(text).replace(/sk-ant-[A-Za-z0-9_-]+/g, "[redacted]").replace(/sk-[A-Za-z0-9]{16,}/g, "[redacted]").replace(/AKIA[0-9A-Z]{16}/g, "[redacted]").replace(/Bearer\s+[A-Za-z0-9._-]+/g, "[redacted]").replace(
|
|
2909
|
+
/\b(ANTHROPIC_(?:AUTH_TOKEN|API_KEY))\s*=\s*["']?[^"'\s]+["']?/gi,
|
|
2910
|
+
"$1=[redacted]"
|
|
2911
|
+
);
|
|
2912
|
+
}
|
|
2086
2913
|
async function postRunReport(config, session, report) {
|
|
2087
|
-
if (config.offline) return;
|
|
2914
|
+
if (config.offline) return { ok: true };
|
|
2088
2915
|
try {
|
|
2089
|
-
await fetch(`${config.apiBaseUrl}/wizard/report`, {
|
|
2916
|
+
const res = await fetch(`${config.apiBaseUrl}/wizard/report`, {
|
|
2090
2917
|
method: "POST",
|
|
2091
2918
|
headers: {
|
|
2092
2919
|
"Content-Type": "application/json",
|
|
@@ -2094,9 +2921,33 @@ async function postRunReport(config, session, report) {
|
|
|
2094
2921
|
},
|
|
2095
2922
|
body: JSON.stringify(report)
|
|
2096
2923
|
});
|
|
2097
|
-
|
|
2924
|
+
if (res.ok) return { ok: true };
|
|
2925
|
+
return {
|
|
2926
|
+
ok: false,
|
|
2927
|
+
status: res.status,
|
|
2928
|
+
detail: await responseDetail(res)
|
|
2929
|
+
};
|
|
2930
|
+
} catch (err) {
|
|
2931
|
+
return { ok: false, detail: detailSlice(errorMessage(err)) };
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
async function responseDetail(res) {
|
|
2935
|
+
try {
|
|
2936
|
+
return detailSlice(await res.text());
|
|
2937
|
+
} catch (err) {
|
|
2938
|
+
return detailSlice(errorMessage(err));
|
|
2098
2939
|
}
|
|
2099
2940
|
}
|
|
2941
|
+
function errorMessage(err) {
|
|
2942
|
+
return err instanceof Error ? err.message : String(err);
|
|
2943
|
+
}
|
|
2944
|
+
function detailSlice(detail) {
|
|
2945
|
+
const sliced = detail.slice(0, 200).trim();
|
|
2946
|
+
return sliced || void 0;
|
|
2947
|
+
}
|
|
2948
|
+
function scrubTerminalControls(value) {
|
|
2949
|
+
return value.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "").replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)?/g, "").replace(/[\x00-\x1f\x7f]/g, " ").replace(/ {2,}/g, " ").trim();
|
|
2950
|
+
}
|
|
2100
2951
|
|
|
2101
2952
|
// src/ui/banner.ts
|
|
2102
2953
|
function banner() {
|
|
@@ -2115,7 +2966,7 @@ function banner() {
|
|
|
2115
2966
|
|
|
2116
2967
|
// src/cli.ts
|
|
2117
2968
|
async function run2(options) {
|
|
2118
|
-
const repoPath =
|
|
2969
|
+
const repoPath = resolve2(options.path ?? process.cwd());
|
|
2119
2970
|
const config = resolveConfig(options);
|
|
2120
2971
|
console.log(banner());
|
|
2121
2972
|
p.intro(theme.signal("Let's wire Whisperr into your app."));
|
|
@@ -2193,51 +3044,103 @@ Flutter is live today; ${theme.bright(
|
|
|
2193
3044
|
);
|
|
2194
3045
|
}
|
|
2195
3046
|
const spin = p.spinner();
|
|
3047
|
+
const useIntegrationSpinner = Boolean(process.stdout.isTTY);
|
|
2196
3048
|
let phaseLabel = "Integrating";
|
|
2197
3049
|
let lastLine = "";
|
|
3050
|
+
let lastActivityLine = "";
|
|
2198
3051
|
const startedAt = Date.now();
|
|
2199
|
-
|
|
2200
|
-
const
|
|
3052
|
+
const nextIntegrationMessage = createMessageDeduper();
|
|
3053
|
+
const updateIntegrationMessage = () => {
|
|
2201
3054
|
const secs = Math.round((Date.now() - startedAt) / 1e3);
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
);
|
|
2205
|
-
}
|
|
3055
|
+
const message = theme.bright(phaseLabel) + theme.muted(` \xB7 ${secs}s`) + (lastLine ? theme.muted(` \u2014 ${lastLine}`) : "");
|
|
3056
|
+
const next = nextIntegrationMessage(message);
|
|
3057
|
+
if (next) spin.message(next);
|
|
3058
|
+
};
|
|
3059
|
+
if (useIntegrationSpinner) {
|
|
3060
|
+
spin.start(theme.bright(phaseLabel));
|
|
3061
|
+
} else {
|
|
3062
|
+
p.log.step(theme.bright(phaseLabel));
|
|
3063
|
+
}
|
|
3064
|
+
const tick = useIntegrationSpinner ? setInterval(updateIntegrationMessage, 1e3) : void 0;
|
|
2206
3065
|
const outcome = await runIntegrationAgent({
|
|
2207
3066
|
repoPath,
|
|
2208
3067
|
config,
|
|
2209
3068
|
session,
|
|
2210
3069
|
playbook: chosen.playbook,
|
|
2211
3070
|
manifest,
|
|
3071
|
+
...process.stdout.isTTY && process.stdin.isTTY ? {
|
|
3072
|
+
async onPlanReady(plan) {
|
|
3073
|
+
if (useIntegrationSpinner) {
|
|
3074
|
+
spin.stop(theme.success("Placement plan ready"));
|
|
3075
|
+
}
|
|
3076
|
+
p.note(renderPlacementPlan(plan), "Placement plan");
|
|
3077
|
+
const placeEntries = plan.filter((entry) => entry.decision === "place");
|
|
3078
|
+
let selectedEvents = placeEntries.map((entry) => entry.event);
|
|
3079
|
+
if (placeEntries.length) {
|
|
3080
|
+
const selection = await p.multiselect({
|
|
3081
|
+
message: "Wire these events?",
|
|
3082
|
+
options: placeEntries.map((entry) => ({
|
|
3083
|
+
value: entry.event,
|
|
3084
|
+
label: entry.event,
|
|
3085
|
+
hint: `${entry.file ?? "unknown file"}@${entry.anchor ?? "unknown anchor"}`
|
|
3086
|
+
})),
|
|
3087
|
+
initialValues: selectedEvents,
|
|
3088
|
+
required: false
|
|
3089
|
+
});
|
|
3090
|
+
selectedEvents = p.isCancel(selection) ? [] : selection;
|
|
3091
|
+
}
|
|
3092
|
+
if (useIntegrationSpinner) {
|
|
3093
|
+
spin.start(theme.bright("Continuing integration"));
|
|
3094
|
+
}
|
|
3095
|
+
return filterEventPlan(plan, selectedEvents);
|
|
3096
|
+
}
|
|
3097
|
+
} : {},
|
|
2212
3098
|
progress: {
|
|
2213
3099
|
onPhase(label) {
|
|
2214
3100
|
phaseLabel = label;
|
|
2215
3101
|
lastLine = "";
|
|
3102
|
+
lastActivityLine = "";
|
|
3103
|
+
if (useIntegrationSpinner) {
|
|
3104
|
+
updateIntegrationMessage();
|
|
3105
|
+
} else {
|
|
3106
|
+
p.log.step(theme.bright(label));
|
|
3107
|
+
}
|
|
2216
3108
|
},
|
|
2217
3109
|
onActivity(line) {
|
|
3110
|
+
if (line === lastActivityLine) return;
|
|
3111
|
+
lastActivityLine = line;
|
|
2218
3112
|
lastLine = line;
|
|
3113
|
+
if (useIntegrationSpinner) updateIntegrationMessage();
|
|
2219
3114
|
}
|
|
2220
3115
|
}
|
|
2221
3116
|
}).catch((err) => {
|
|
2222
3117
|
p.log.error(err.message);
|
|
2223
3118
|
return null;
|
|
2224
3119
|
});
|
|
2225
|
-
clearInterval(tick);
|
|
3120
|
+
if (tick) clearInterval(tick);
|
|
2226
3121
|
if (!outcome) {
|
|
2227
|
-
|
|
3122
|
+
if (useIntegrationSpinner) {
|
|
3123
|
+
spin.stop(theme.alert("Integration stopped"));
|
|
3124
|
+
} else {
|
|
3125
|
+
p.log.error(theme.alert("Integration stopped"));
|
|
3126
|
+
}
|
|
2228
3127
|
await maybeRevert(repoPath, checkpoint, "The run stopped before finishing.");
|
|
2229
3128
|
return 1;
|
|
2230
3129
|
}
|
|
2231
3130
|
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
|
-
|
|
3131
|
+
if (useIntegrationSpinner) {
|
|
3132
|
+
spin.stop(stopLabel);
|
|
3133
|
+
} else if (!outcome.coreOk) {
|
|
3134
|
+
p.log.warn(stopLabel);
|
|
3135
|
+
} else {
|
|
3136
|
+
p.log.success(stopLabel);
|
|
3137
|
+
}
|
|
2233
3138
|
const opportunities = await collectOpportunities(repoPath, manifest, checkpoint);
|
|
2234
|
-
|
|
3139
|
+
let files = await changedFiles(repoPath, checkpoint);
|
|
2235
3140
|
if (files.length) {
|
|
2236
3141
|
p.note(files.map((f) => theme.muted("\u2022 ") + f).join("\n"), "Files changed");
|
|
2237
3142
|
}
|
|
2238
|
-
|
|
2239
|
-
p.log.message(outcome.summary.trim());
|
|
2240
|
-
}
|
|
3143
|
+
logAgentSummary(outcome.summary);
|
|
2241
3144
|
if (!outcome.coreOk) {
|
|
2242
3145
|
const reverted = await maybeRevert(
|
|
2243
3146
|
repoPath,
|
|
@@ -2254,7 +3157,7 @@ Flutter is live today; ${theme.bright(
|
|
|
2254
3157
|
const cmd = chosen.playbook.verifyCommand;
|
|
2255
3158
|
const vspin = p.spinner();
|
|
2256
3159
|
vspin.start(`Verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
2257
|
-
|
|
3160
|
+
let verdict = await runVerifyCommand(repoPath, cmd);
|
|
2258
3161
|
verified = verdictToVerified(verdict);
|
|
2259
3162
|
if (verdict.toolMissing) {
|
|
2260
3163
|
vspin.stop(
|
|
@@ -2267,18 +3170,71 @@ Flutter is live today; ${theme.bright(
|
|
|
2267
3170
|
} else if (verdict.ok) {
|
|
2268
3171
|
vspin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
2269
3172
|
} else {
|
|
2270
|
-
|
|
2271
|
-
if (
|
|
2272
|
-
|
|
3173
|
+
const remainingBudgetUsd = config.budgetUsd - outcome.costUsd;
|
|
3174
|
+
if (remainingBudgetUsd >= 0.5) {
|
|
3175
|
+
vspin.stop(
|
|
3176
|
+
theme.warn(`Verification failed \u2014 ${cmd}`) + theme.muted(" \u2014 attempting one repair pass")
|
|
3177
|
+
);
|
|
3178
|
+
const repairSpin = p.spinner();
|
|
3179
|
+
repairSpin.start(`Repairing verifier failures \u2014 ${theme.muted(cmd)}`);
|
|
3180
|
+
try {
|
|
3181
|
+
const repair = await runRepairPass({
|
|
3182
|
+
repoPath,
|
|
3183
|
+
config,
|
|
3184
|
+
session,
|
|
3185
|
+
playbook: chosen.playbook,
|
|
3186
|
+
verifyCommand: cmd,
|
|
3187
|
+
verifyOutput: verdict.output.slice(-4e3),
|
|
3188
|
+
budgetUsd: remainingBudgetUsd
|
|
3189
|
+
});
|
|
3190
|
+
outcome.costUsd += repair.costUsd;
|
|
3191
|
+
if (repair.summary.trim()) {
|
|
3192
|
+
outcome.summary = [outcome.summary, `Repair:
|
|
3193
|
+
${repair.summary}`].filter(Boolean).join("\n\n");
|
|
3194
|
+
logAgentSummary(repair.summary);
|
|
3195
|
+
}
|
|
3196
|
+
repairSpin.stop(theme.success("Repair pass finished"));
|
|
3197
|
+
} catch (err) {
|
|
3198
|
+
repairSpin.stop(
|
|
3199
|
+
theme.warn("Repair pass failed") + theme.muted(` \u2014 ${err.message}`)
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
files = await changedFiles(repoPath, checkpoint);
|
|
3203
|
+
const reverifySpin = p.spinner();
|
|
3204
|
+
reverifySpin.start(`Re-verifying the integration \u2014 ${theme.muted(cmd)}`);
|
|
3205
|
+
verdict = await runVerifyCommand(repoPath, cmd);
|
|
3206
|
+
verified = verdictToVerified(verdict);
|
|
3207
|
+
if (verdict.toolMissing) {
|
|
3208
|
+
reverifySpin.stop(
|
|
3209
|
+
theme.warn("Couldn't verify automatically") + theme.muted(` \u2014 \`${cmd}\` isn't available here. Run it yourself before committing.`)
|
|
3210
|
+
);
|
|
3211
|
+
} else if (verdict.timedOut) {
|
|
3212
|
+
reverifySpin.stop(
|
|
3213
|
+
theme.warn(`Verification timed out \u2014 \`${cmd}\``) + theme.muted(" \u2014 run it yourself before committing.")
|
|
3214
|
+
);
|
|
3215
|
+
} else if (verdict.ok) {
|
|
3216
|
+
reverifySpin.stop(theme.success("Verified \u2713") + theme.muted(` (${cmd})`));
|
|
3217
|
+
} else {
|
|
3218
|
+
reverifySpin.stop(theme.alert(`Verification still failed \u2014 ${cmd}`));
|
|
3219
|
+
}
|
|
3220
|
+
} else {
|
|
3221
|
+
vspin.stop(
|
|
3222
|
+
theme.alert(`Verification failed \u2014 ${cmd}`) + theme.muted(" \u2014 skipped repair because less than $0.50 budget remains")
|
|
3223
|
+
);
|
|
2273
3224
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
3225
|
+
if (!verdict.ok && !verdict.toolMissing && !verdict.timedOut) {
|
|
3226
|
+
if (verdict.output) {
|
|
3227
|
+
p.note(verdict.output, theme.warn("Verifier output"));
|
|
3228
|
+
}
|
|
3229
|
+
const reverted = await maybeRevert(
|
|
3230
|
+
repoPath,
|
|
3231
|
+
checkpoint,
|
|
3232
|
+
"The integration didn't pass its build/lint check, so the edits may be broken."
|
|
3233
|
+
);
|
|
3234
|
+
if (reverted) {
|
|
3235
|
+
p.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
|
|
3236
|
+
return 1;
|
|
3237
|
+
}
|
|
2282
3238
|
}
|
|
2283
3239
|
}
|
|
2284
3240
|
}
|
|
@@ -2291,7 +3247,8 @@ Flutter is live today; ${theme.bright(
|
|
|
2291
3247
|
opportunities,
|
|
2292
3248
|
fingerprint,
|
|
2293
3249
|
checkpoint,
|
|
2294
|
-
remainingBudgetUsd: config.budgetUsd - outcome.costUsd
|
|
3250
|
+
remainingBudgetUsd: config.budgetUsd - outcome.costUsd,
|
|
3251
|
+
repoMap: outcome.repoMap
|
|
2295
3252
|
});
|
|
2296
3253
|
if (instrumentationSnapshot && chosen.playbook.verifyCommand) {
|
|
2297
3254
|
const cmd = chosen.playbook.verifyCommand;
|
|
@@ -2346,19 +3303,33 @@ Flutter is live today; ${theme.bright(
|
|
|
2346
3303
|
status: wiredMap.has(eventType) ? "wired" : "skipped",
|
|
2347
3304
|
file: wiredMap.get(eventType)
|
|
2348
3305
|
}));
|
|
2349
|
-
await postRunReport(config, session, {
|
|
3306
|
+
const reportResult = await postRunReport(config, session, {
|
|
2350
3307
|
target: chosen.playbook.target.id,
|
|
2351
3308
|
repo_fingerprint: fingerprint,
|
|
2352
3309
|
identify_wired: outcome.coreOk,
|
|
2353
3310
|
verified,
|
|
2354
3311
|
cost_usd: outcome.costUsd,
|
|
2355
3312
|
duration_ms: outcome.durationMs,
|
|
2356
|
-
summary: outcome.summary.slice(0, 4e3),
|
|
3313
|
+
summary: scrubSummary(outcome.summary).slice(0, 4e3),
|
|
2357
3314
|
events: reportEvents
|
|
2358
3315
|
});
|
|
3316
|
+
if (!reportResult.ok) {
|
|
3317
|
+
p.log.warn(
|
|
3318
|
+
theme.warn("Couldn't record this run's coverage on the server") + theme.muted(
|
|
3319
|
+
` (${formatReportFailure(reportResult)}) \u2014 future runs may re-propose events already wired here.`
|
|
3320
|
+
)
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
const eventLines = renderEventOutcomeLines(outcome.eventOutcomes, wiredMap);
|
|
3324
|
+
if (eventLines) {
|
|
3325
|
+
p.note(eventLines, "Events");
|
|
3326
|
+
}
|
|
3327
|
+
const eventDenominator = manifest.universeSummary?.wireableHere ?? eventTypesToScan.length;
|
|
3328
|
+
const eventStatsLabel = manifest.universeSummary ? "events wired here" : "events wired";
|
|
3329
|
+
const timingDetails = outcome.phaseTimings.map(({ phase, ms }) => `${shortPhaseLabel(phase)} ${formatDuration(ms)}`).join(" \xB7 ");
|
|
2359
3330
|
p.log.info(
|
|
2360
3331
|
theme.muted(
|
|
2361
|
-
`${wiredMap.size}/${
|
|
3332
|
+
`${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
3333
|
)
|
|
2363
3334
|
);
|
|
2364
3335
|
if (!config.offline) {
|
|
@@ -2432,10 +3403,9 @@ async function offerOpportunities(opts) {
|
|
|
2432
3403
|
hint: i.description ?? i.rationale
|
|
2433
3404
|
}))
|
|
2434
3405
|
],
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
],
|
|
3406
|
+
// Opt-in per item: nothing is pre-selected, so adding to the universe is an
|
|
3407
|
+
// explicit choice rather than an opt-out the user has to notice and undo.
|
|
3408
|
+
initialValues: [],
|
|
2439
3409
|
required: false
|
|
2440
3410
|
});
|
|
2441
3411
|
if (p.isCancel(selection) || selection.length === 0) {
|
|
@@ -2497,6 +3467,13 @@ async function offerOpportunities(opts) {
|
|
|
2497
3467
|
theme.muted("Runtime policy update queued \u2014 the additions go live once it completes.")
|
|
2498
3468
|
);
|
|
2499
3469
|
}
|
|
3470
|
+
if (result.policyRegen?.status === "draft") {
|
|
3471
|
+
lines.push(
|
|
3472
|
+
theme.muted(
|
|
3473
|
+
"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)."
|
|
3474
|
+
)
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
2500
3477
|
p.note(lines.join("\n"), theme.signal("Universe updated"));
|
|
2501
3478
|
if (result.policyRegen?.status === "failed") {
|
|
2502
3479
|
p.log.warn(
|
|
@@ -2520,11 +3497,12 @@ async function offerOpportunities(opts) {
|
|
|
2520
3497
|
session,
|
|
2521
3498
|
playbook: opts.playbook,
|
|
2522
3499
|
acceptedEvents,
|
|
2523
|
-
budgetUsd: opts.remainingBudgetUsd
|
|
3500
|
+
budgetUsd: opts.remainingBudgetUsd,
|
|
3501
|
+
repoMap: opts.repoMap
|
|
2524
3502
|
});
|
|
2525
3503
|
if (pass.ran) {
|
|
2526
3504
|
spin.stop(theme.success("New events instrumented"));
|
|
2527
|
-
|
|
3505
|
+
logAgentSummary(pass.summary);
|
|
2528
3506
|
} else {
|
|
2529
3507
|
spin.stop(
|
|
2530
3508
|
theme.warn("Skipped instrumenting the new events") + theme.muted(
|
|
@@ -2627,15 +3605,98 @@ async function withSpinner(label, fn) {
|
|
|
2627
3605
|
throw err;
|
|
2628
3606
|
}
|
|
2629
3607
|
}
|
|
3608
|
+
function createMessageDeduper() {
|
|
3609
|
+
let last = "";
|
|
3610
|
+
return (message) => {
|
|
3611
|
+
if (message === last) return null;
|
|
3612
|
+
last = message;
|
|
3613
|
+
return message;
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
function formatDuration(ms) {
|
|
3617
|
+
const safeMs = Math.max(0, ms);
|
|
3618
|
+
const totalSeconds = Math.round(safeMs / 1e3);
|
|
3619
|
+
if (safeMs < 6e4) return `${totalSeconds}s`;
|
|
3620
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
3621
|
+
const seconds = String(totalSeconds % 60).padStart(2, "0");
|
|
3622
|
+
return `${minutes}m${seconds}s`;
|
|
3623
|
+
}
|
|
3624
|
+
function filterEventPlan(plan, selectedEvents) {
|
|
3625
|
+
const selected = new Set(selectedEvents);
|
|
3626
|
+
return plan.map(
|
|
3627
|
+
(entry) => entry.decision === "place" && !selected.has(entry.event) ? { ...entry, decision: "skip", reason: "Deselected in plan review." } : entry
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
function renderPlacementPlan(plan) {
|
|
3631
|
+
return plan.map((entry) => {
|
|
3632
|
+
if (entry.decision === "place") {
|
|
3633
|
+
return theme.success("\u2713 ") + entry.event + theme.muted(
|
|
3634
|
+
` \u2014 ${entry.file ?? "unknown file"} @ ${entry.anchor ?? "unknown anchor"}`
|
|
3635
|
+
);
|
|
3636
|
+
}
|
|
3637
|
+
const reason = entry.reason || "No reason provided.";
|
|
3638
|
+
const line = `${entry.decision === "skip" ? "\u25CB" : "?"} ${entry.event} \u2014 ${reason}`;
|
|
3639
|
+
return entry.decision === "skip" ? theme.muted(line) : theme.warn(line);
|
|
3640
|
+
}).join("\n");
|
|
3641
|
+
}
|
|
3642
|
+
function renderEventOutcomeLines(outcomes, wiredMap) {
|
|
3643
|
+
const maxLines = 40;
|
|
3644
|
+
const visibleCount = outcomes.length > maxLines ? maxLines - 1 : outcomes.length;
|
|
3645
|
+
const lines = outcomes.slice(0, visibleCount).map((outcome) => {
|
|
3646
|
+
if (outcome.outcome === "wired") {
|
|
3647
|
+
const file = wiredMap.get(outcome.event);
|
|
3648
|
+
return theme.success("\u2713 ") + outcome.event + (file ? theme.muted(` \u2014 ${file}`) : "");
|
|
3649
|
+
}
|
|
3650
|
+
return theme.muted(
|
|
3651
|
+
`\u25CB ${outcome.event} \u2014 ${outcome.reason || "No track() call was detected."}`
|
|
3652
|
+
);
|
|
3653
|
+
});
|
|
3654
|
+
const remainder = outcomes.length - visibleCount;
|
|
3655
|
+
if (remainder > 0) lines.push(theme.muted(`\u2026 ${remainder} more events`));
|
|
3656
|
+
return lines.join("\n");
|
|
3657
|
+
}
|
|
3658
|
+
function logAgentSummary(summary) {
|
|
3659
|
+
const cleaned = scrubSummary(summary).trim();
|
|
3660
|
+
if (cleaned) p.log.message(cleaned);
|
|
3661
|
+
}
|
|
3662
|
+
function shortPhaseLabel(phase) {
|
|
3663
|
+
switch (phase) {
|
|
3664
|
+
case "Mapping your codebase":
|
|
3665
|
+
return "map";
|
|
3666
|
+
case "Installing the SDK & wiring identify()":
|
|
3667
|
+
return "core";
|
|
3668
|
+
case "Planning event placements":
|
|
3669
|
+
return "plan";
|
|
3670
|
+
case "Instrumenting planned events":
|
|
3671
|
+
case "Instrumenting your events":
|
|
3672
|
+
return "wire";
|
|
3673
|
+
case "Reconciling missed events":
|
|
3674
|
+
return "reconcile";
|
|
3675
|
+
case "Reviewing & correcting placements":
|
|
3676
|
+
return "review";
|
|
3677
|
+
default:
|
|
3678
|
+
return phase;
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
2630
3681
|
function summarizeManifest(m) {
|
|
2631
3682
|
const events = [...m.events].sort((a, b) => (b.importance ?? 0) - (a.importance ?? 0)).map((e) => theme.muted("\u2022 ") + e.eventType);
|
|
2632
3683
|
const channels = m.identify.channels?.length ? theme.muted(`channels: ${m.identify.channels.join(", ")}`) : "";
|
|
3684
|
+
const firstLine2 = m.universeSummary ? theme.bright(`${m.universeSummary.total} events in your universe`) + theme.muted(
|
|
3685
|
+
` \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`
|
|
3686
|
+
) : theme.bright("identify()") + theme.muted(" + ") + theme.bright(`${m.events.length} events`);
|
|
2633
3687
|
return [
|
|
2634
|
-
|
|
3688
|
+
firstLine2,
|
|
2635
3689
|
...events,
|
|
2636
3690
|
channels
|
|
2637
3691
|
].filter(Boolean).join("\n");
|
|
2638
3692
|
}
|
|
3693
|
+
function formatReportFailure(result) {
|
|
3694
|
+
const detail = result.detail?.replace(/\s+/g, " ").trim();
|
|
3695
|
+
if (result.status !== void 0) {
|
|
3696
|
+
return `HTTP ${result.status}${detail ? `: ${detail}` : ""}`;
|
|
3697
|
+
}
|
|
3698
|
+
return detail || "request failed";
|
|
3699
|
+
}
|
|
2639
3700
|
|
|
2640
3701
|
// src/index.ts
|
|
2641
3702
|
var modulePath = fileURLToPath(import.meta.url);
|