@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.
Files changed (3) hide show
  1. package/README.md +48 -10
  2. package/dist/index.js +1171 -110
  3. 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) || 10,
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 { query } from "@anthropic-ai/claude-agent-sdk";
1150
-
1151
- // src/core/opportunities.ts
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
- re: new RegExp(
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 = await readFile2(join2(repoPath, file), "utf8");
1288
+ contents.push({ file, content: await readFile2(join2(repoPath, file), "utf8") });
1288
1289
  } catch {
1289
1290
  continue;
1290
1291
  }
1291
- for (const { eventType, re } of patterns) {
1292
- if (!wired.has(eventType) && re.test(content)) {
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((resolve2) => {
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) => resolve2({ ok: code === 0, stdout, stderr }));
1310
- child.on("error", () => resolve2({ ok: false, stdout, stderr }));
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
- progress?.onPhase?.("Installing the SDK & wiring identify()");
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 runPass({
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
- progress?.onPhase?.("Instrumenting your events");
1772
- const eventsPrompt = [
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.model,
1794
- effort: config.effort,
1795
- maxTurns: config.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 += events.costUsd;
1800
- eventsComplete = !events.maxedOut;
1801
- if (events.summary) summaries.push(`Events:
1802
- ${events.summary}`);
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 runPass({
2234
+ const review = await runTimedPass("Reviewing & correcting placements", {
1834
2235
  prompt: reviewPrompt,
1835
2236
  systemPrompt: systemPrompt9,
1836
2237
  repoPath,
1837
- model: config.model,
1838
- effort: config.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 { prompt, systemPrompt: systemPrompt9, repoPath, model, effort, maxTurns, budgetUsd, progress } = opts;
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
- allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
1924
- permissionMode: "bypassPermissions",
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
- progress?.onActivity?.(firstLine(block.text));
2717
+ emitActivity(firstLine(block.text));
1939
2718
  } else if (isToolUseBlock(block)) {
1940
- progress?.onActivity?.(describeToolUse(block));
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((resolve2) => {
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
- resolve2({ ran: true, ok: false, command, output: tail(out), timedOut: true });
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
- resolve2({
2885
+ resolve3({
2065
2886
  ran: true,
2066
2887
  ok: code === 0,
2067
2888
  command,
2068
2889
  exitCode: code,
2069
- output: tail(out),
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
- resolve2({ ran: true, ok: false, command, output: tail(out), toolMissing: true });
2897
+ resolve3({ ran: true, ok: false, command, output: tail2(out), toolMissing: true });
2077
2898
  });
2078
2899
  });
2079
2900
  }
2080
- function tail(s) {
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
- } catch {
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 = resolve(options.path ?? process.cwd());
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
- spin.start(theme.bright(phaseLabel));
2200
- const tick = setInterval(() => {
3052
+ const nextIntegrationMessage = createMessageDeduper();
3053
+ const updateIntegrationMessage = () => {
2201
3054
  const secs = Math.round((Date.now() - startedAt) / 1e3);
2202
- spin.message(
2203
- theme.bright(phaseLabel) + theme.muted(` \xB7 ${secs}s`) + (lastLine ? theme.muted(` \u2014 ${lastLine}`) : "")
2204
- );
2205
- }, 1e3);
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
- spin.stop(theme.alert("Integration stopped"));
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
- spin.stop(stopLabel);
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
- const files = await changedFiles(repoPath, checkpoint);
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
- if (outcome.summary.trim()) {
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
- const verdict = await runVerifyCommand(repoPath, cmd);
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
- vspin.stop(theme.alert(`Verification failed \u2014 ${cmd}`));
2271
- if (verdict.output) {
2272
- p.note(verdict.output, theme.warn("Verifier output"));
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
- const reverted = await maybeRevert(
2275
- repoPath,
2276
- checkpoint,
2277
- "The integration didn't pass its build/lint check, so the edits may be broken."
2278
- );
2279
- if (reverted) {
2280
- p.outro(theme.muted("Reverted \u2014 nothing was left in your working tree."));
2281
- return 1;
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}/${eventTypesToScan.length} events wired \xB7 ${files.length} file${files.length === 1 ? "" : "s"} changed \xB7 ` + (verified === true ? "verified \xB7 " : verified === false ? "unverified \xB7 " : "") + `${Math.round(outcome.durationMs / 1e3)}s`
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
- initialValues: [
2436
- ...opportunities.events.map((e) => `e:${e.code}`),
2437
- ...opportunities.interventions.map((i) => `i:${i.code}`)
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
- if (pass.summary.trim()) p.log.message(pass.summary.trim());
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
- theme.bright("identify()") + theme.muted(" + ") + theme.bright(`${m.events.length} events`),
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);