@autonoma-ai/planner 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -84,6 +84,195 @@ var init_model = __esm({
84
84
  }
85
85
  });
86
86
 
87
+ // src/core/analytics.ts
88
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
89
+ import { join as join5 } from "path";
90
+ import { homedir as homedir3 } from "os";
91
+ import { randomUUID } from "crypto";
92
+ function resolveKey() {
93
+ return (process.env.AUTONOMA_POSTHOG_KEY ?? POSTHOG_PUBLIC_KEY).trim();
94
+ }
95
+ function resolveHost() {
96
+ return (process.env.AUTONOMA_POSTHOG_HOST ?? DEFAULT_HOST).replace(/\/+$/, "");
97
+ }
98
+ function trackingDisabled() {
99
+ const v = process.env.DONT_TRACK;
100
+ return v === "1" || v === "true";
101
+ }
102
+ function getIdentity() {
103
+ const id = process.env.AUTONOMA_DISTINCT_ID?.trim();
104
+ return id && id.length > 0 ? id : void 0;
105
+ }
106
+ function getDeviceId() {
107
+ if (cachedDeviceId) return cachedDeviceId;
108
+ try {
109
+ cachedDeviceId = readFileSync3(DEVICE_ID_PATH, "utf-8").trim();
110
+ if (cachedDeviceId) return cachedDeviceId;
111
+ } catch {
112
+ }
113
+ cachedDeviceId = randomUUID();
114
+ try {
115
+ mkdirSync2(AUTONOMA_HOME3, { recursive: true });
116
+ writeFileSync2(DEVICE_ID_PATH, cachedDeviceId, { encoding: "utf-8", mode: 384 });
117
+ } catch {
118
+ }
119
+ return cachedDeviceId;
120
+ }
121
+ function isEnabled() {
122
+ if (enabled === null) {
123
+ enabled = !trackingDisabled() && resolveKey().length > 0;
124
+ }
125
+ return enabled;
126
+ }
127
+ function track(event, properties = {}) {
128
+ if (!isEnabled()) return;
129
+ const identity = getIdentity();
130
+ const body = JSON.stringify({
131
+ api_key: resolveKey(),
132
+ event,
133
+ distinct_id: identity ?? getDeviceId(),
134
+ properties: {
135
+ ...properties,
136
+ run_id: RUN_ID,
137
+ // Only build a person profile when we have a real identity from the app,
138
+ // so the CLI joins the existing funnel person instead of creating a new one.
139
+ $process_person_profile: identity != null,
140
+ cli_version: process.env.npm_package_version
141
+ }
142
+ });
143
+ const promise = fetch(`${resolveHost()}/capture/`, {
144
+ method: "POST",
145
+ headers: { "Content-Type": "application/json" },
146
+ body
147
+ }).catch(() => {
148
+ }).finally(() => pending.delete(promise));
149
+ pending.add(promise);
150
+ }
151
+ function trackError(error, properties = {}, handled = true) {
152
+ const err = error instanceof Error ? error : new Error(String(error));
153
+ track("$exception", {
154
+ ...properties,
155
+ $exception_list: [
156
+ {
157
+ type: err.name,
158
+ value: err.message,
159
+ mechanism: { handled, synthetic: !(error instanceof Error) }
160
+ }
161
+ ],
162
+ error_stack: err.stack
163
+ });
164
+ }
165
+ async function flushAnalytics(timeoutMs = 1500) {
166
+ if (pending.size === 0) return;
167
+ await Promise.race([
168
+ Promise.allSettled([...pending]),
169
+ new Promise((resolve5) => setTimeout(resolve5, timeoutMs))
170
+ ]);
171
+ }
172
+ var AUTONOMA_HOME3, DEVICE_ID_PATH, POSTHOG_PUBLIC_KEY, DEFAULT_HOST, RUN_ID, cachedDeviceId, enabled, pending;
173
+ var init_analytics = __esm({
174
+ "src/core/analytics.ts"() {
175
+ "use strict";
176
+ init_esm_shims();
177
+ AUTONOMA_HOME3 = join5(homedir3(), ".autonoma");
178
+ DEVICE_ID_PATH = join5(AUTONOMA_HOME3, ".device-id");
179
+ POSTHOG_PUBLIC_KEY = "phc_mUOwUj62r8vyiisFPvXLC3G5RftETIBMnKNSHqTBdka";
180
+ DEFAULT_HOST = "https://us.i.posthog.com";
181
+ RUN_ID = randomUUID();
182
+ cachedDeviceId = null;
183
+ enabled = null;
184
+ pending = /* @__PURE__ */ new Set();
185
+ }
186
+ });
187
+
188
+ // src/core/errors.ts
189
+ import { APICallError, RetryError, LoadAPIKeyError, InvalidPromptError, NoSuchModelError } from "ai";
190
+ function sleep(ms) {
191
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
192
+ }
193
+ function formatException(err) {
194
+ if (!(err instanceof Error)) return String(err);
195
+ let out = err.stack ?? `${err.name}: ${err.message}`;
196
+ if (err.cause != null) {
197
+ out += `
198
+ Caused by: ${formatException(err.cause)}`;
199
+ }
200
+ return out;
201
+ }
202
+ function classifyAgentError(err) {
203
+ const message = err instanceof Error ? err.message : String(err);
204
+ const msg = message.toLowerCase();
205
+ if (msg.includes("timed out") || msg.includes("timeout") || msg.includes("abort")) {
206
+ return "timeout";
207
+ }
208
+ if (APICallError.isInstance(err)) {
209
+ if (err.statusCode != null && FATAL_STATUS_CODES.has(err.statusCode)) return "fatal";
210
+ return "transient";
211
+ }
212
+ if (LoadAPIKeyError.isInstance(err) || InvalidPromptError.isInstance(err) || NoSuchModelError.isInstance(err)) {
213
+ return "fatal";
214
+ }
215
+ if (RetryError.isInstance(err)) {
216
+ if (err.reason === "errorNotRetryable" && err.lastError !== err) {
217
+ return classifyAgentError(err.lastError);
218
+ }
219
+ return "transient";
220
+ }
221
+ if (TRANSIENT_MESSAGE_PATTERNS.some((pattern) => msg.includes(pattern))) {
222
+ return "transient";
223
+ }
224
+ return "transient";
225
+ }
226
+ var FATAL_STATUS_CODES, TRANSIENT_MESSAGE_PATTERNS;
227
+ var init_errors = __esm({
228
+ "src/core/errors.ts"() {
229
+ "use strict";
230
+ init_esm_shims();
231
+ FATAL_STATUS_CODES = /* @__PURE__ */ new Set([400, 401, 403, 404, 422]);
232
+ TRANSIENT_MESSAGE_PATTERNS = [
233
+ "econnreset",
234
+ "econnrefused",
235
+ "etimedout",
236
+ "socket hang up",
237
+ "fetch failed",
238
+ "network",
239
+ "overloaded",
240
+ "rate limit",
241
+ "too many requests",
242
+ "429",
243
+ "500",
244
+ "502",
245
+ "503",
246
+ "529"
247
+ ];
248
+ }
249
+ });
250
+
251
+ // src/core/notify.ts
252
+ import { platform } from "os";
253
+ import { execFile } from "child_process";
254
+ function notify(title, message) {
255
+ process.stderr.write("\x07");
256
+ const os = platform();
257
+ if (os === "darwin") {
258
+ execFile(
259
+ "osascript",
260
+ ["-e", `display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`],
261
+ () => {
262
+ }
263
+ );
264
+ } else if (os === "linux") {
265
+ execFile("notify-send", [title, message], () => {
266
+ });
267
+ }
268
+ }
269
+ var init_notify = __esm({
270
+ "src/core/notify.ts"() {
271
+ "use strict";
272
+ init_esm_shims();
273
+ }
274
+ });
275
+
87
276
  // src/core/display.ts
88
277
  function formatArgs(input, keys) {
89
278
  const parts = [];
@@ -243,6 +432,13 @@ import {
243
432
  hasToolCall,
244
433
  stepCountIs
245
434
  } from "ai";
435
+ function formatRetryGuidance(guidance) {
436
+ if (!guidance?.trim()) return "";
437
+ return `
438
+ A previous attempt at this task did not complete successfully. The user provided this guidance for the retry:
439
+ "${guidance.trim()}"
440
+ `;
441
+ }
246
442
  function buildDefaultStepLogger(agentId, maxSteps) {
247
443
  const logger = createStepLogger(agentId, maxSteps);
248
444
  return {
@@ -302,26 +498,49 @@ async function runAgent(config, prompt, extractResult) {
302
498
  onStepFinish: buildStepHandler(config)
303
499
  });
304
500
  try {
305
- await agent.generate({
306
- messages: [{ role: "user", content: prompt }],
501
+ const messages = [{ role: "user", content: prompt }];
502
+ let generation = await agent.generate({
503
+ messages,
307
504
  timeout: { stepMs: stepTimeout }
308
505
  });
506
+ let nudges = 0;
507
+ while (extractResult() === void 0 && nudges < MAX_NUDGES) {
508
+ nudges++;
509
+ console.log(
510
+ ` ${YELLOW5}[${config.id}] agent stopped without finishing \u2014 nudging (${nudges}/${MAX_NUDGES})...${RESET8}`
511
+ );
512
+ track("cli_agent_nudged", { agent: config.id, nudge: nudges });
513
+ messages.push(...generation.response.messages);
514
+ messages.push({ role: "user", content: NUDGE_PROMPT });
515
+ generation = await agent.generate({
516
+ messages,
517
+ timeout: { stepMs: stepTimeout }
518
+ });
519
+ }
309
520
  return extractResult();
310
521
  } catch (err) {
522
+ const errorClass = classifyAgentError(err);
523
+ if (errorClass === "fatal") throw err;
311
524
  const msg = err instanceof Error ? err.message : String(err);
312
- const isTimeout = msg.includes("timed out") || msg.includes("timeout") || msg.includes("abort");
313
- if (!isTimeout) throw err;
314
- console.log(` ${YELLOW5}[${config.id}] step timed out after ${stepTimeout / 1e3}s${RESET8}`);
525
+ if (errorClass === "timeout") {
526
+ console.log(` ${YELLOW5}[${config.id}] step timed out after ${stepTimeout / 1e3}s${RESET8}`);
527
+ } else {
528
+ console.log(` ${YELLOW5}[${config.id}] provider error: ${msg}${RESET8}`);
529
+ }
530
+ track("cli_agent_retryable_error", { agent: config.id, error_class: errorClass, retry });
315
531
  if (retry < RETRIES_BEFORE_FALLBACK - 1) {
316
532
  console.log(
317
533
  ` ${YELLOW5}[${config.id}] retrying (${retry + 1}/${RETRIES_BEFORE_FALLBACK})...${RESET8}`
318
534
  );
535
+ if (errorClass === "transient") {
536
+ await sleep(Math.min(2e3 * 2 ** retry, 1e4));
537
+ }
319
538
  continue;
320
539
  }
321
540
  if (modelIdx < modelsToTry.length - 1) {
322
541
  const nextModel = FALLBACK_MODELS[modelIdx];
323
542
  console.log(
324
- ` ${YELLOW5}[${config.id}] ${RETRIES_BEFORE_FALLBACK} timeouts, switching to ${nextModel}${RESET8}`
543
+ ` ${YELLOW5}[${config.id}] ${RETRIES_BEFORE_FALLBACK} failed attempts, switching to ${nextModel}${RESET8}`
325
544
  );
326
545
  break;
327
546
  }
@@ -331,12 +550,14 @@ async function runAgent(config, prompt, extractResult) {
331
550
  }
332
551
  return extractResult();
333
552
  }
334
- var FALLBACK_MODELS, RETRIES_BEFORE_FALLBACK, STEP_TIMEOUT_MS;
553
+ var FALLBACK_MODELS, RETRIES_BEFORE_FALLBACK, STEP_TIMEOUT_MS, MAX_NUDGES, NUDGE_PROMPT;
335
554
  var init_agent = __esm({
336
555
  "src/core/agent.ts"() {
337
556
  "use strict";
338
557
  init_esm_shims();
558
+ init_analytics();
339
559
  init_display();
560
+ init_errors();
340
561
  init_model();
341
562
  FALLBACK_MODELS = [
342
563
  "moonshotai/kimi-k2.6",
@@ -345,6 +566,8 @@ var init_agent = __esm({
345
566
  ];
346
567
  RETRIES_BEFORE_FALLBACK = 3;
347
568
  STEP_TIMEOUT_MS = 12e4;
569
+ MAX_NUDGES = 2;
570
+ NUDGE_PROMPT = "You stopped before completing the task. The task is only complete once the finish tool has been called and succeeded. If your last finish call returned an error, fix exactly what it reported. Otherwise, continue where you left off and call the finish tool when done.";
348
571
  }
349
572
  });
350
573
 
@@ -412,7 +635,7 @@ var init_gitignore = __esm({
412
635
  });
413
636
 
414
637
  // src/tools/bash.ts
415
- import { execFile as execFile2 } from "child_process";
638
+ import { execFile as execFile3 } from "child_process";
416
639
  import { promisify as promisify2 } from "util";
417
640
  import { tool } from "ai";
418
641
  import { z } from "zod";
@@ -479,7 +702,7 @@ var init_bash = __esm({
479
702
  "src/tools/bash.ts"() {
480
703
  "use strict";
481
704
  init_esm_shims();
482
- execFileAsync2 = promisify2(execFile2);
705
+ execFileAsync2 = promisify2(execFile3);
483
706
  CHAINING_OPERATORS = /;|&&|\|\||`|\$\(|>>|<<|&\s*$/;
484
707
  DEFAULT_ALLOWED = /* @__PURE__ */ new Set([
485
708
  "git",
@@ -540,7 +763,7 @@ var init_glob = __esm({
540
763
  });
541
764
 
542
765
  // src/tools/grep.ts
543
- import { execFile as execFile3 } from "child_process";
766
+ import { execFile as execFile4 } from "child_process";
544
767
  import { promisify as promisify3 } from "util";
545
768
  import { tool as tool3 } from "ai";
546
769
  import { z as z3 } from "zod";
@@ -585,7 +808,7 @@ var init_grep = __esm({
585
808
  "src/tools/grep.ts"() {
586
809
  "use strict";
587
810
  init_esm_shims();
588
- execFileAsync3 = promisify3(execFile3);
811
+ execFileAsync3 = promisify3(execFile4);
589
812
  inputSchema3 = z3.object({
590
813
  pattern: z3.string().describe("Regex pattern to search for in file contents"),
591
814
  glob: z3.string().optional().describe("Glob to filter files (e.g. '*.ts')"),
@@ -1062,31 +1285,6 @@ var init_pages_finder = __esm({
1062
1285
  }
1063
1286
  });
1064
1287
 
1065
- // src/core/notify.ts
1066
- import { platform } from "os";
1067
- import { execFile as execFile4 } from "child_process";
1068
- function notify(title, message) {
1069
- process.stderr.write("\x07");
1070
- const os = platform();
1071
- if (os === "darwin") {
1072
- execFile4(
1073
- "osascript",
1074
- ["-e", `display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"`],
1075
- () => {
1076
- }
1077
- );
1078
- } else if (os === "linux") {
1079
- execFile4("notify-send", [title, message], () => {
1080
- });
1081
- }
1082
- }
1083
- var init_notify = __esm({
1084
- "src/core/notify.ts"() {
1085
- "use strict";
1086
- init_esm_shims();
1087
- }
1088
- });
1089
-
1090
1288
  // src/core/review.ts
1091
1289
  import * as p3 from "@clack/prompts";
1092
1290
  import { access } from "fs/promises";
@@ -1540,7 +1738,7 @@ async function runKBGenerator(input) {
1540
1738
  let result;
1541
1739
  const tracker = new PageTracker();
1542
1740
  const { logger, onStepFinish } = buildDefaultStepLogger("kb", 150);
1543
- const contextBlock = input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "";
1741
+ const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + formatRetryGuidance(input.retryGuidance);
1544
1742
  const pages = input.projectContext?.pages;
1545
1743
  if (pages?.length) {
1546
1744
  tracker.register(pages.map((p10) => p10.path));
@@ -1644,7 +1842,7 @@ Call page_coverage to see current state. When done with changes, call finish aga
1644
1842
  return reviewed ?? {
1645
1843
  success: false,
1646
1844
  artifacts: [],
1647
- summary: "KB generator did not produce a result"
1845
+ summary: "KB generator agent stopped without producing AUTONOMA.md"
1648
1846
  };
1649
1847
  }
1650
1848
  var PageTracker;
@@ -2211,7 +2409,7 @@ async function runEntityAudit(input) {
2211
2409
  preRegisteredCount = detection.models.length;
2212
2410
  }
2213
2411
  const { logger, onStepFinish } = buildDefaultStepLogger("entity-audit", 200);
2214
- const contextBlock = input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "";
2412
+ const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + formatRetryGuidance(input.retryGuidance);
2215
2413
  const preRegBlock = preRegisteredCount > 0 ? `
2216
2414
  ## Pre-registered models (${preRegisteredCount} found via ${detection.framework} schema at ${detection.schemaFile})
2217
2415
 
@@ -2260,11 +2458,13 @@ write_file already targets the output directory \u2014 use just the filename.`;
2260
2458
  },
2261
2459
  onStepFinish
2262
2460
  };
2461
+ let agentError;
2263
2462
  try {
2264
2463
  await runAgent(agentConfig, prompt, () => result);
2265
2464
  } catch (err) {
2266
- const message = err instanceof Error ? err.message : String(err);
2267
- console.error(`Entity audit agent error: ${message}`);
2465
+ agentError = err instanceof Error ? err.message : String(err);
2466
+ console.error(`Entity audit agent error:
2467
+ ${formatException(err)}`);
2268
2468
  }
2269
2469
  logger.summary();
2270
2470
  if (!result && tracker.auditedModels.size > 0) {
@@ -2316,7 +2516,7 @@ When done with changes, call finish again.`;
2316
2516
  return reviewed ?? {
2317
2517
  success: false,
2318
2518
  artifacts: [],
2319
- summary: "Entity audit did not produce a result"
2519
+ summary: agentError ? `Entity audit failed: ${agentError}` : "Entity audit agent stopped without producing entity-audit.md"
2320
2520
  };
2321
2521
  }
2322
2522
  var ModelTracker;
@@ -2325,6 +2525,7 @@ var init_entity_audit = __esm({
2325
2525
  "use strict";
2326
2526
  init_esm_shims();
2327
2527
  init_agent();
2528
+ init_errors();
2328
2529
  init_context();
2329
2530
  init_model();
2330
2531
  init_review();
@@ -2653,7 +2854,7 @@ async function runScenarioRecipe(input) {
2653
2854
  const model = getModel(input.modelId);
2654
2855
  let result;
2655
2856
  const { logger, onStepFinish } = buildDefaultStepLogger("scenario", 40);
2656
- const contextBlock = input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "";
2857
+ const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + formatRetryGuidance(input.retryGuidance);
2657
2858
  const requiredEntities = await parseEntityNames(input.outputDir);
2658
2859
  const entityListBlock = requiredEntities.length > 0 ? `
2659
2860
  ## Required entities (${requiredEntities.length} total \u2014 ALL must appear in the scenario)
@@ -2726,7 +2927,7 @@ When done with changes, call finish again.`;
2726
2927
  return reviewed ?? {
2727
2928
  success: false,
2728
2929
  artifacts: [],
2729
- summary: "Scenario recipe agent did not produce a result"
2930
+ summary: "Scenario agent stopped without producing scenarios.md"
2730
2931
  };
2731
2932
  }
2732
2933
  async function feedbackToScenario(input, feedback) {
@@ -2920,7 +3121,8 @@ async function rankEntitiesByImportance(models, auditMarkdown, model) {
2920
3121
  return new Map(reconciled.order.map((name, i) => [name, i]));
2921
3122
  } catch (err) {
2922
3123
  console.warn(
2923
- `[recipe-builder] Importance ranking failed, falling back to alphabetical order: ${err instanceof Error ? err.message : String(err)}`
3124
+ `[recipe-builder] Importance ranking failed, falling back to alphabetical order:
3125
+ ${formatException(err)}`
2924
3126
  );
2925
3127
  return /* @__PURE__ */ new Map();
2926
3128
  }
@@ -2930,6 +3132,7 @@ var init_entity_relevance = __esm({
2930
3132
  "src/agents/04-recipe-builder/entity-relevance.ts"() {
2931
3133
  "use strict";
2932
3134
  init_esm_shims();
3135
+ init_errors();
2933
3136
  rankedSchema = z13.object({
2934
3137
  ranked: z13.array(z13.string()).describe("Every entity name, ordered most-important first.")
2935
3138
  });
@@ -3478,7 +3681,8 @@ async function testUpDown(entityName, entityIndex, totalEntities, sdkConfig, rec
3478
3681
  try {
3479
3682
  upResult = await up(sdkConfig, recipe, testRunId);
3480
3683
  } catch (err) {
3481
- p5.log.error(`UP request failed: ${err instanceof Error ? err.message : String(err)}`);
3684
+ p5.log.error(`UP request failed:
3685
+ ${formatException(err)}`);
3482
3686
  const action = await promptOnFailure(entityName, null);
3483
3687
  if (action === "skip") return "skip";
3484
3688
  if (action === "retry") continue;
@@ -3506,7 +3710,8 @@ async function testUpDown(entityName, entityIndex, totalEntities, sdkConfig, rec
3506
3710
  try {
3507
3711
  downResult = await down(sdkConfig, refsToken);
3508
3712
  } catch (err) {
3509
- p5.log.error(`DOWN request failed: ${err instanceof Error ? err.message : String(err)}`);
3713
+ p5.log.error(`DOWN request failed:
3714
+ ${formatException(err)}`);
3510
3715
  const action = await promptOnFailure(entityName, null);
3511
3716
  if (action === "skip") return "skip";
3512
3717
  if (action === "retry") continue;
@@ -3707,6 +3912,7 @@ var init_entity_loop = __esm({
3707
3912
  "use strict";
3708
3913
  init_esm_shims();
3709
3914
  init_agent();
3915
+ init_errors();
3710
3916
  init_tools();
3711
3917
  init_ask_user();
3712
3918
  init_gitignore();
@@ -3809,7 +4015,8 @@ async function teardown(sdkConfig, refsToken, successMessage) {
3809
4015
  try {
3810
4016
  downResult = await down(sdkConfig, refsToken);
3811
4017
  } catch (err) {
3812
- p6.log.error(`Full DOWN request failed: ${err instanceof Error ? err.message : String(err)}`);
4018
+ p6.log.error(`Full DOWN request failed:
4019
+ ${formatException(err)}`);
3813
4020
  return false;
3814
4021
  }
3815
4022
  if (!downResult.ok) {
@@ -3847,7 +4054,8 @@ async function runFullValidation(state, _models, outputDir, model) {
3847
4054
  try {
3848
4055
  upResult = await up(sdkConfig, fullRecipe, testRunId);
3849
4056
  } catch (err) {
3850
- p6.log.error(`Full UP request failed: ${err instanceof Error ? err.message : String(err)}`);
4057
+ p6.log.error(`Full UP request failed:
4058
+ ${formatException(err)}`);
3851
4059
  notify("Autonoma", "Full validation UP failed, action needed");
3852
4060
  const action = await p6.select({
3853
4061
  message: "What would you like to do?",
@@ -3934,6 +4142,7 @@ var init_full_validation = __esm({
3934
4142
  init_esm_shims();
3935
4143
  init_notify();
3936
4144
  init_agent();
4145
+ init_errors();
3937
4146
  init_tools();
3938
4147
  init_highlight();
3939
4148
  init_state();
@@ -5543,7 +5752,7 @@ ${scenariosMd}
5543
5752
  const preseedContext = existingState ? "" : await preseedQueue(state, input.projectRoot, input.pages, features ?? void 0);
5544
5753
  const resumeContext = existingState ? `
5545
5754
  You are RESUMING a previous run. ${existingState.summary().tested} nodes tested, ${existingState.summary().totalTests} tests written. Call next_node to continue.` : "";
5546
- const contextBlock = input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "";
5755
+ const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + formatRetryGuidance(input.retryGuidance);
5547
5756
  let prompt = `Generate E2E test cases by processing every node in the queue.
5548
5757
  ${contextBlock}${kbContext}${resumeContext}${preseedContext}
5549
5758
 
@@ -5608,8 +5817,8 @@ Do NOT try to finish early. Process EVERY node via next_node until it returns do
5608
5817
  try {
5609
5818
  await runAgent(agentConfig, prompt, () => result);
5610
5819
  } catch (err) {
5611
- const message = err instanceof Error ? err.message : String(err);
5612
- console.log(` [chunk] Agent error (will retry next chunk): ${message}`);
5820
+ console.log(` [chunk] Agent error (will retry next chunk):
5821
+ ${formatException(err)}`);
5613
5822
  }
5614
5823
  totalSteps += CHUNK_STEPS;
5615
5824
  if (result) break;
@@ -5970,8 +6179,8 @@ Write 5-8 journey tests using the write_test tool with folder "journeys". Then c
5970
6179
  try {
5971
6180
  await runAgent(config, journeyPrompt, () => journeyResult);
5972
6181
  } catch (err) {
5973
- const message = err instanceof Error ? err.message : String(err);
5974
- console.error(`Journey generator error: ${message}`);
6182
+ console.error(`Journey generator error:
6183
+ ${formatException(err)}`);
5975
6184
  }
5976
6185
  logger.summary();
5977
6186
  return journeyState.allTestPaths().length;
@@ -5982,6 +6191,7 @@ var init_test_generator = __esm({
5982
6191
  "use strict";
5983
6192
  init_esm_shims();
5984
6193
  init_agent();
6194
+ init_errors();
5985
6195
  init_context();
5986
6196
  init_display();
5987
6197
  init_gitignore();
@@ -6197,86 +6407,10 @@ function restoreTerminal() {
6197
6407
  process.stdout.write(SHOW_CURSOR);
6198
6408
  }
6199
6409
 
6200
- // src/core/analytics.ts
6201
- init_esm_shims();
6202
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
6203
- import { join as join5 } from "path";
6204
- import { homedir as homedir3 } from "os";
6205
- import { randomUUID } from "crypto";
6206
- var AUTONOMA_HOME3 = join5(homedir3(), ".autonoma");
6207
- var DEVICE_ID_PATH = join5(AUTONOMA_HOME3, ".device-id");
6208
- var POSTHOG_PUBLIC_KEY = "phc_mUOwUj62r8vyiisFPvXLC3G5RftETIBMnKNSHqTBdka";
6209
- var DEFAULT_HOST = "https://us.i.posthog.com";
6210
- function resolveKey() {
6211
- return (process.env.AUTONOMA_POSTHOG_KEY ?? POSTHOG_PUBLIC_KEY).trim();
6212
- }
6213
- function resolveHost() {
6214
- return (process.env.AUTONOMA_POSTHOG_HOST ?? DEFAULT_HOST).replace(/\/+$/, "");
6215
- }
6216
- function trackingDisabled() {
6217
- const v = process.env.DONT_TRACK;
6218
- return v === "1" || v === "true";
6219
- }
6220
- function getIdentity() {
6221
- const id = process.env.AUTONOMA_DISTINCT_ID?.trim();
6222
- return id && id.length > 0 ? id : void 0;
6223
- }
6224
- var RUN_ID = randomUUID();
6225
- var cachedDeviceId = null;
6226
- function getDeviceId() {
6227
- if (cachedDeviceId) return cachedDeviceId;
6228
- try {
6229
- cachedDeviceId = readFileSync3(DEVICE_ID_PATH, "utf-8").trim();
6230
- if (cachedDeviceId) return cachedDeviceId;
6231
- } catch {
6232
- }
6233
- cachedDeviceId = randomUUID();
6234
- try {
6235
- mkdirSync2(AUTONOMA_HOME3, { recursive: true });
6236
- writeFileSync2(DEVICE_ID_PATH, cachedDeviceId, { encoding: "utf-8", mode: 384 });
6237
- } catch {
6238
- }
6239
- return cachedDeviceId;
6240
- }
6241
- var enabled = null;
6242
- function isEnabled() {
6243
- if (enabled === null) {
6244
- enabled = !trackingDisabled() && resolveKey().length > 0;
6245
- }
6246
- return enabled;
6247
- }
6248
- var pending = /* @__PURE__ */ new Set();
6249
- function track(event, properties = {}) {
6250
- if (!isEnabled()) return;
6251
- const identity = getIdentity();
6252
- const body = JSON.stringify({
6253
- api_key: resolveKey(),
6254
- event,
6255
- distinct_id: identity ?? getDeviceId(),
6256
- properties: {
6257
- ...properties,
6258
- run_id: RUN_ID,
6259
- // Only build a person profile when we have a real identity from the app,
6260
- // so the CLI joins the existing funnel person instead of creating a new one.
6261
- $process_person_profile: identity != null,
6262
- cli_version: process.env.npm_package_version
6263
- }
6264
- });
6265
- const promise = fetch(`${resolveHost()}/capture/`, {
6266
- method: "POST",
6267
- headers: { "Content-Type": "application/json" },
6268
- body
6269
- }).catch(() => {
6270
- }).finally(() => pending.delete(promise));
6271
- pending.add(promise);
6272
- }
6273
- async function flushAnalytics(timeoutMs = 1500) {
6274
- if (pending.size === 0) return;
6275
- await Promise.race([
6276
- Promise.allSettled([...pending]),
6277
- new Promise((resolve5) => setTimeout(resolve5, timeoutMs))
6278
- ]);
6279
- }
6410
+ // src/index.ts
6411
+ init_analytics();
6412
+ init_errors();
6413
+ init_notify();
6280
6414
 
6281
6415
  // src/core/upload.ts
6282
6416
  init_esm_shims();
@@ -6287,11 +6421,11 @@ import { glob } from "glob";
6287
6421
 
6288
6422
  // src/core/git.ts
6289
6423
  init_esm_shims();
6290
- import { execFile } from "child_process";
6424
+ import { execFile as execFile2 } from "child_process";
6291
6425
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
6292
6426
  import { join as join6 } from "path";
6293
6427
  import { promisify } from "util";
6294
- var execFileAsync = promisify(execFile);
6428
+ var execFileAsync = promisify(execFile2);
6295
6429
  var GIT_INFO_FILE = ".git-info.json";
6296
6430
  async function git(projectRoot, args) {
6297
6431
  try {
@@ -6506,7 +6640,7 @@ var STEP_INTROS = {
6506
6640
  recipeBuilder: "Helping you wire up small helpers that create and clean up test data in your own database. We give you a copy-paste guide for each one and test it live against your app running locally - you deploy later, once everything passes.",
6507
6641
  testGenerator: "Writing the actual end-to-end tests, covering every page and feature with depth proportional to its complexity."
6508
6642
  };
6509
- async function runStep(step, outputDir, state, config, projectContext, nonInteractive) {
6643
+ async function runStep(step, outputDir, state, config, projectContext, nonInteractive, retryGuidance) {
6510
6644
  const label = STEP_LABELS[step];
6511
6645
  p9.note(STEP_INTROS[step], `Step: ${label}`);
6512
6646
  const stepStartedAt = Date.now();
@@ -6539,7 +6673,8 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
6539
6673
  outputDir,
6540
6674
  modelId: config.modelId,
6541
6675
  projectContext,
6542
- nonInteractive
6676
+ nonInteractive,
6677
+ retryGuidance
6543
6678
  });
6544
6679
  break;
6545
6680
  }
@@ -6550,7 +6685,8 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
6550
6685
  outputDir,
6551
6686
  modelId: config.modelId,
6552
6687
  projectContext,
6553
- nonInteractive
6688
+ nonInteractive,
6689
+ retryGuidance
6554
6690
  });
6555
6691
  break;
6556
6692
  }
@@ -6562,7 +6698,8 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
6562
6698
  modelId: config.modelId,
6563
6699
  config,
6564
6700
  projectContext,
6565
- nonInteractive
6701
+ nonInteractive,
6702
+ retryGuidance
6566
6703
  });
6567
6704
  break;
6568
6705
  }
@@ -6574,7 +6711,8 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
6574
6711
  modelId: config.modelId,
6575
6712
  config,
6576
6713
  projectContext,
6577
- nonInteractive
6714
+ nonInteractive,
6715
+ retryGuidance
6578
6716
  });
6579
6717
  break;
6580
6718
  }
@@ -6588,7 +6726,8 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
6588
6726
  config,
6589
6727
  projectContext,
6590
6728
  nonInteractive,
6591
- pages
6729
+ pages,
6730
+ retryGuidance
6592
6731
  });
6593
6732
  break;
6594
6733
  }
@@ -6600,6 +6739,7 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
6600
6739
  } else {
6601
6740
  state = await markStep(outputDir, state, step, "failed");
6602
6741
  p9.log.error(`Failed: ${label} \u2014 ${result.summary}`);
6742
+ trackError(new Error(result.summary), { step, source: "step_result" });
6603
6743
  }
6604
6744
  } else {
6605
6745
  state = await markStep(outputDir, state, step, "done");
@@ -6609,6 +6749,9 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
6609
6749
  state = await markStep(outputDir, state, step, "failed");
6610
6750
  const message = err instanceof Error ? err.message : String(err);
6611
6751
  p9.log.error(`Failed: ${label} \u2014 ${message}`);
6752
+ console.error(`\x1B[2m${formatException(err)}\x1B[0m`);
6753
+ p9.log.info("If you report this, please include the error output above.");
6754
+ trackError(err, { step, source: "step_exception" });
6612
6755
  }
6613
6756
  track("cli_step_completed", {
6614
6757
  step,
@@ -6617,6 +6760,37 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
6617
6760
  });
6618
6761
  return state;
6619
6762
  }
6763
+ async function promptStepFailure(label) {
6764
+ notify("Autonoma", `${label} failed \u2014 action needed`);
6765
+ const action = await p9.select({
6766
+ message: "This step failed. What would you like to do?",
6767
+ options: [
6768
+ { value: "retry", label: "Retry this step", hint: "Run it again from the top" },
6769
+ { value: "guidance", label: "Retry with guidance", hint: "Tell the agent what went wrong or what to focus on" },
6770
+ { value: "exit", label: "Stop here (progress saved)", hint: "Resume later with --resume" }
6771
+ ]
6772
+ });
6773
+ if (p9.isCancel(action) || action === "exit") return { kind: "exit" };
6774
+ if (action === "retry") return { kind: "retry" };
6775
+ const guidance = await p9.text({
6776
+ message: "What should the agent do differently?",
6777
+ placeholder: "e.g. the part that failed, or what to focus on"
6778
+ });
6779
+ if (p9.isCancel(guidance)) return { kind: "exit" };
6780
+ const trimmed = guidance.trim();
6781
+ return { kind: "retry", guidance: trimmed || void 0 };
6782
+ }
6783
+ async function runStepWithRecovery(step, outputDir, state, config, projectContext, nonInteractive) {
6784
+ let guidance;
6785
+ while (true) {
6786
+ state = await runStep(step, outputDir, state, config, projectContext, nonInteractive, guidance);
6787
+ if (state.steps[step] !== "failed" || nonInteractive) return state;
6788
+ const action = await promptStepFailure(STEP_LABELS[step]);
6789
+ if (action.kind === "exit") return state;
6790
+ guidance = action.guidance;
6791
+ track("cli_step_retried", { step, with_guidance: guidance != null });
6792
+ }
6793
+ }
6620
6794
  async function showStatus(outputDir) {
6621
6795
  const state = await loadState(outputDir);
6622
6796
  console.log("\nPipeline Status:");
@@ -6794,7 +6968,13 @@ or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
6794
6968
  p9.log.error("Cannot run test generation yet \u2014 the scenario recipe step must complete first.");
6795
6969
  return;
6796
6970
  }
6797
- state = await runStep(targetStep, outputDir, state, config, projectContext, nonInteractive);
6971
+ state = await runStepWithRecovery(targetStep, outputDir, state, config, projectContext, nonInteractive);
6972
+ if (state.steps[targetStep] === "failed") {
6973
+ const retryCommand = `autonoma-planner --step ${targetStep}` + (args.project ? ` --project ${args.project}` : "");
6974
+ p9.log.warn(`Your progress is saved. To retry this step, run:
6975
+ ${retryCommand}`);
6976
+ process.exitCode = 1;
6977
+ }
6798
6978
  p9.outro("Done");
6799
6979
  return;
6800
6980
  }
@@ -6812,12 +6992,15 @@ or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
6812
6992
  try {
6813
6993
  for (let i = startIdx; i < steps.length; i++) {
6814
6994
  const step = steps[i];
6815
- state = await runStep(step, outputDir, state, config, projectContext, nonInteractive);
6995
+ state = await runStepWithRecovery(step, outputDir, state, config, projectContext, nonInteractive);
6816
6996
  if (state.steps[step] === "paused") {
6817
6997
  break;
6818
6998
  }
6819
6999
  if (state.steps[step] === "failed") {
6820
7000
  p9.log.error("Pipeline stopped due to failure.");
7001
+ p9.log.warn(`Your progress is saved. To retry this step, run:
7002
+ ${resumeCommand}`);
7003
+ process.exitCode = 1;
6821
7004
  break;
6822
7005
  }
6823
7006
  const skipConfirmAfter = ["pagesFinder"];
@@ -6850,14 +7033,17 @@ Continue?`
6850
7033
  } catch (err) {
6851
7034
  const message = err instanceof Error ? err.message : String(err);
6852
7035
  p9.log.error(`Failed to upload artifacts: ${message}`);
7036
+ console.error(`\x1B[2m${formatException(err)}\x1B[0m`);
6853
7037
  p9.log.info(`Your artifacts are saved in ${outputDir}. Re-run the CLI to retry the upload.`);
6854
7038
  track("cli_artifacts_upload_failed", { message });
7039
+ trackError(err, { source: "artifact_upload" });
6855
7040
  }
6856
7041
  }
6857
7042
  p9.outro("Done");
6858
7043
  }
6859
7044
  main().then(() => flushAnalytics()).catch(async (err) => {
6860
7045
  console.error(err);
7046
+ trackError(err, { source: "uncaught" }, false);
6861
7047
  await flushAnalytics();
6862
7048
  process.exit(1);
6863
7049
  });