@whisperr/wizard 0.1.10 → 0.1.12

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 (2) hide show
  1. package/dist/index.js +114 -19
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,8 +6,8 @@ import * as p from "@clack/prompts";
6
6
 
7
7
  // src/core/config.ts
8
8
  var DEFAULT_API_BASE = "https://api.whisperr.net";
9
- var DEFAULT_MODEL = "claude-opus-4-8";
10
- var DEFAULT_EFFORT = "low";
9
+ var DEFAULT_MODEL = "claude-sonnet-4-6";
10
+ var DEFAULT_EFFORT = "high";
11
11
  var EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
12
12
  function resolveEffort() {
13
13
  const raw = (process.env.WHISPERR_WIZARD_EFFORT ?? "").toLowerCase();
@@ -25,7 +25,13 @@ function resolveConfig(flags = {}) {
25
25
  llmBaseUrl,
26
26
  model: flags.model ?? process.env.WHISPERR_WIZARD_MODEL ?? DEFAULT_MODEL,
27
27
  effort: resolveEffort(),
28
- maxTurns: Number(process.env.WHISPERR_WIZARD_MAX_TURNS ?? 55),
28
+ // Cost is the real limiter (budgetUsd below). maxTurns is only a high safety
29
+ // backstop against a stuck loop — it should NOT bind on a normal run. (It
30
+ // used to be 55, which guillotined high-effort multi-phase runs mid-work.)
31
+ maxTurns: Number(process.env.WHISPERR_WIZARD_MAX_TURNS) || 200,
32
+ // Hard ceiling on TOTAL spend across all phases. If hit, the run stops
33
+ // cleanly and keeps whatever already landed. Real runs finish well under it.
34
+ budgetUsd: Number(process.env.WHISPERR_WIZARD_BUDGET_USD) || 10,
29
35
  directAnthropicKey,
30
36
  offline
31
37
  };
@@ -1022,6 +1028,31 @@ describe the SAME person, so they belong on the SAME surface. If you wired
1022
1028
  account_created into one controller and are about to put identify() in a
1023
1029
  different one, stop \u2014 that's the tell you've split the subject. Co-locate them.
1024
1030
 
1031
+ WHAT THE EVENT MEANS \u2014 place by SEMANTICS, not by keyword. This is where most
1032
+ mistakes happen. An event names a SPECIFIC moment, not "something happened
1033
+ nearby." For each event, before you place it:
1034
+ - Model the exact transition. \`subscription_started\` is the FIRST subscription,
1035
+ NOT every payment \u2014 renewals, retries, and reactivations are DIFFERENT events.
1036
+ Firing one event on a path that also handles the other cases is a real bug.
1037
+ - Read the control flow and find the branch that separates look-alike cases.
1038
+ Payment/callback handlers routinely route initial vs recurring/renewal, success
1039
+ vs retry, trial vs paid through ONE function. Key the event to the right branch
1040
+ (e.g. a \`RENEW_CARD\` / \`is_recurring\` / \`type\` check), or branch yourself and
1041
+ emit the correct event per case. Do not drop one event on the shared success
1042
+ line and move on \u2014 open the function and see which cases it actually covers.
1043
+ - Read the codebase's own flags literally. A field like \`type: promo|standard\`
1044
+ is pricing tier, not lifecycle \u2014 confirm what a flag means before keying on it.
1045
+ - Capture the discriminators as properties. The fields that tell cases apart \u2014
1046
+ charge_type, is_recurring, payment_source/gateway, amount, plan, currency \u2014 are
1047
+ exactly what makes the event usable for churn (a failed RENEWAL is a churn
1048
+ signal; a failed first charge is not). If the code sets such a field right
1049
+ there (e.g. \`type => 'initial'\`), pass it. A billing event without them is
1050
+ half-blind.
1051
+ - Cover the whole lifecycle. Two gateways each with an initial and a recurring
1052
+ path is FOUR call sites \u2014 instrument all of them. A recurring/secondary
1053
+ callback with no events is a churn blind spot. If you deliberately skip a path,
1054
+ say so in the summary.
1055
+
1025
1056
  Every step costs time and the user is watching. Work FAST and DECISIVELY.
1026
1057
 
1027
1058
  EFFICIENCY \u2014 non-negotiable:
@@ -1183,14 +1214,21 @@ async function runIntegrationAgent(opts) {
1183
1214
  repoPath,
1184
1215
  model: config.model,
1185
1216
  effort: config.effort,
1186
- maxTurns: 35,
1217
+ maxTurns: 50,
1218
+ budgetUsd: config.budgetUsd - costUsd,
1187
1219
  progress
1188
1220
  });
1189
1221
  costUsd += core.costUsd;
1190
1222
  if (core.summary) summaries.push(`Core setup:
1191
1223
  ${core.summary}`);
1224
+ const BUDGET_FLOOR = 0.25;
1192
1225
  let eventsComplete = true;
1193
- if (manifest.events.length > 0) {
1226
+ if (manifest.events.length > 0 && config.budgetUsd - costUsd <= BUDGET_FLOOR) {
1227
+ eventsComplete = false;
1228
+ summaries.push(
1229
+ "Events: skipped \u2014 the spend limit was reached during core setup. Re-run with a higher WHISPERR_WIZARD_BUDGET_USD to instrument events."
1230
+ );
1231
+ } else if (manifest.events.length > 0) {
1194
1232
  progress?.onPhase?.("Instrumenting your events");
1195
1233
  const eventsPrompt = [
1196
1234
  `The Whisperr ${playbook.target.displayName} SDK is already installed and`,
@@ -1216,12 +1254,52 @@ ${core.summary}`);
1216
1254
  model: config.model,
1217
1255
  effort: config.effort,
1218
1256
  maxTurns: config.maxTurns,
1257
+ budgetUsd: config.budgetUsd - costUsd,
1219
1258
  progress
1220
1259
  });
1221
1260
  costUsd += events.costUsd;
1222
1261
  eventsComplete = !events.maxedOut;
1223
1262
  if (events.summary) summaries.push(`Events:
1224
1263
  ${events.summary}`);
1264
+ }
1265
+ if (core.ok && config.budgetUsd - costUsd > BUDGET_FLOOR) {
1266
+ progress?.onPhase?.("Reviewing & correcting placements");
1267
+ const reviewPrompt = [
1268
+ `You wired the Whisperr ${playbook.target.displayName} SDK into this repo.`,
1269
+ "Now AUDIT YOUR OWN WORK and fix mistakes before finishing.",
1270
+ `Project root: ${repoPath}`,
1271
+ "",
1272
+ "Run `git diff` and read the surrounding code to see exactly what you added.",
1273
+ "For every identify() and track() call, verify \u2014 and FIX in place:",
1274
+ "1. Subject \u2014 keys to the END USER, never an admin/staff/operator/seller.",
1275
+ " identify() sits on the SAME surface as account_created (same person).",
1276
+ "2. Lifecycle \u2014 the event fires ONLY on its real moment. A 'first time'",
1277
+ " event (e.g. subscription_started) must NOT fire on a renewal/recurring",
1278
+ " path; look for RENEW_CARD / is_recurring / type branches in the SAME",
1279
+ " handler. If one path handles both cases, branch and emit the right",
1280
+ " event per case.",
1281
+ "3. Discriminators \u2014 billing/subscription events carry the properties that",
1282
+ " tell cases apart when the code exposes them (charge_type / is_recurring",
1283
+ " / payment_source / amount / plan). Add the ones you missed.",
1284
+ "4. Coverage \u2014 every gateway/callback path that should emit an event does;",
1285
+ " no recurring or secondary path left as a dead zone.",
1286
+ "Change ONLY what is genuinely wrong or incomplete \u2014 do not churn correct",
1287
+ "calls or re-explore the whole repo. Be surgical. End with a one-line,",
1288
+ "plain-text note of what you corrected, or 'No corrections needed.'."
1289
+ ].join("\n");
1290
+ const review = await runPass({
1291
+ prompt: reviewPrompt,
1292
+ systemPrompt: systemPrompt9,
1293
+ repoPath,
1294
+ model: config.model,
1295
+ effort: config.effort,
1296
+ maxTurns: 40,
1297
+ budgetUsd: config.budgetUsd - costUsd,
1298
+ progress
1299
+ });
1300
+ costUsd += review.costUsd;
1301
+ if (review.summary) summaries.push(`Review:
1302
+ ${review.summary}`);
1225
1303
  }
1226
1304
  return {
1227
1305
  summary: summaries.join("\n\n"),
@@ -1231,8 +1309,12 @@ ${events.summary}`);
1231
1309
  eventsComplete
1232
1310
  };
1233
1311
  }
1312
+ function isLimitStop(err) {
1313
+ const msg = err?.message ?? "";
1314
+ return /max(imum)?[\s_-]*(turn|budget)|budget.*exceeded|number of turns/i.test(msg);
1315
+ }
1234
1316
  async function runPass(opts) {
1235
- const { prompt, systemPrompt: systemPrompt9, repoPath, model, effort, maxTurns, progress } = opts;
1317
+ const { prompt, systemPrompt: systemPrompt9, repoPath, model, effort, maxTurns, budgetUsd, progress } = opts;
1236
1318
  let summary = "";
1237
1319
  let costUsd = 0;
1238
1320
  let ok = false;
@@ -1251,23 +1333,36 @@ async function runPass(opts) {
1251
1333
  allowedTools: ["Read", "Edit", "Write", "Bash", "Glob", "Grep"],
1252
1334
  permissionMode: "bypassPermissions",
1253
1335
  maxTurns,
1336
+ // Native SDK hard cost cap. Stops this pass with an `error_max_budget_usd`
1337
+ // result once spend exceeds the remaining budget — this, not maxTurns, is
1338
+ // the real limiter. Omitted when there's no positive budget left.
1339
+ ...budgetUsd && budgetUsd > 0 ? { maxBudgetUsd: budgetUsd } : {},
1254
1340
  settingSources: []
1255
1341
  }
1256
1342
  });
1257
- for await (const message of response) {
1258
- if (message.type === "assistant") {
1259
- for (const block of message.message?.content ?? []) {
1260
- if (isTextBlock(block) && block.text?.trim()) {
1261
- progress?.onActivity?.(firstLine(block.text));
1262
- } else if (isToolUseBlock(block)) {
1263
- progress?.onActivity?.(describeToolUse(block));
1343
+ try {
1344
+ for await (const message of response) {
1345
+ if (message.type === "assistant") {
1346
+ for (const block of message.message?.content ?? []) {
1347
+ if (isTextBlock(block) && block.text?.trim()) {
1348
+ progress?.onActivity?.(firstLine(block.text));
1349
+ } else if (isToolUseBlock(block)) {
1350
+ progress?.onActivity?.(describeToolUse(block));
1351
+ }
1264
1352
  }
1353
+ } else if (message.type === "result") {
1354
+ ok = message.subtype === "success";
1355
+ const sub = message.subtype ?? "";
1356
+ maxedOut = sub.includes("max_turns") || sub.includes("max_budget");
1357
+ costUsd = message.total_cost_usd ?? 0;
1358
+ summary = message.result ?? extractLastText(message) ?? summary;
1265
1359
  }
1266
- } else if (message.type === "result") {
1267
- ok = message.subtype === "success";
1268
- maxedOut = (message.subtype ?? "").includes("max_turns");
1269
- costUsd = message.total_cost_usd ?? 0;
1270
- summary = message.result ?? extractLastText(message) ?? summary;
1360
+ }
1361
+ } catch (err) {
1362
+ if (isLimitStop(err)) {
1363
+ maxedOut = true;
1364
+ } else {
1365
+ throw err;
1271
1366
  }
1272
1367
  }
1273
1368
  return { summary, costUsd, ok, maxedOut };
@@ -1827,7 +1922,7 @@ async function main() {
1827
1922
  return;
1828
1923
  }
1829
1924
  if ("version" in parsed) {
1830
- console.log("0.1.10");
1925
+ console.log("0.1.12");
1831
1926
  return;
1832
1927
  }
1833
1928
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whisperr/wizard",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Whisperr Wizard — one command to integrate the Whisperr SDK into your app. Authenticates with your onboarded account and uses an AI coding agent to wire up identify() and your business events automatically.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,