@whisperr/wizard 0.2.1 → 0.3.0

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