@autonoma-ai/planner 0.1.8-canary.bed7cd6 → 0.1.8

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,9 +84,39 @@ var init_model = __esm({
84
84
  }
85
85
  });
86
86
 
87
+ // src/core/version.ts
88
+ import { readFileSync as readFileSync3 } from "fs";
89
+ import { fileURLToPath as fileURLToPath2 } from "url";
90
+ import { dirname, join as join5 } from "path";
91
+ function resolveVersion() {
92
+ try {
93
+ const here = dirname(fileURLToPath2(import.meta.url));
94
+ for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
95
+ try {
96
+ const pkg = JSON.parse(readFileSync3(join5(here, rel), "utf-8"));
97
+ if (pkg?.name === PACKAGE_NAME && typeof pkg.version === "string") {
98
+ return pkg.version;
99
+ }
100
+ } catch {
101
+ }
102
+ }
103
+ } catch {
104
+ }
105
+ return "unknown";
106
+ }
107
+ var PACKAGE_NAME, CLI_VERSION;
108
+ var init_version = __esm({
109
+ "src/core/version.ts"() {
110
+ "use strict";
111
+ init_esm_shims();
112
+ PACKAGE_NAME = "@autonoma-ai/planner";
113
+ CLI_VERSION = resolveVersion();
114
+ }
115
+ });
116
+
87
117
  // src/core/analytics.ts
88
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
89
- import { join as join5 } from "path";
118
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
119
+ import { join as join6 } from "path";
90
120
  import { homedir as homedir3 } from "os";
91
121
  import { randomUUID } from "crypto";
92
122
  function resolveKey() {
@@ -109,7 +139,7 @@ function getRunId() {
109
139
  function getDeviceId() {
110
140
  if (cachedDeviceId) return cachedDeviceId;
111
141
  try {
112
- cachedDeviceId = readFileSync3(DEVICE_ID_PATH, "utf-8").trim();
142
+ cachedDeviceId = readFileSync4(DEVICE_ID_PATH, "utf-8").trim();
113
143
  if (cachedDeviceId) return cachedDeviceId;
114
144
  } catch {
115
145
  }
@@ -140,7 +170,7 @@ function track(event, properties = {}) {
140
170
  // Only build a person profile when we have a real identity from the app,
141
171
  // so the CLI joins the existing funnel person instead of creating a new one.
142
172
  $process_person_profile: identity != null,
143
- cli_version: process.env.npm_package_version
173
+ cli_version: CLI_VERSION
144
174
  }
145
175
  });
146
176
  const promise = fetch(`${resolveHost()}/capture/`, {
@@ -177,8 +207,9 @@ var init_analytics = __esm({
177
207
  "src/core/analytics.ts"() {
178
208
  "use strict";
179
209
  init_esm_shims();
180
- AUTONOMA_HOME3 = join5(homedir3(), ".autonoma");
181
- DEVICE_ID_PATH = join5(AUTONOMA_HOME3, ".device-id");
210
+ init_version();
211
+ AUTONOMA_HOME3 = join6(homedir3(), ".autonoma");
212
+ DEVICE_ID_PATH = join6(AUTONOMA_HOME3, ".device-id");
182
213
  POSTHOG_PUBLIC_KEY = "phc_mUOwUj62r8vyiisFPvXLC3G5RftETIBMnKNSHqTBdka";
183
214
  DEFAULT_HOST = "https://us.i.posthog.com";
184
215
  RUN_ID = randomUUID();
@@ -193,6 +224,9 @@ import { APICallError, RetryError, LoadAPIKeyError, InvalidPromptError, NoSuchMo
193
224
  function sleep(ms) {
194
225
  return new Promise((resolve5) => setTimeout(resolve5, ms));
195
226
  }
227
+ function isUserCancellation(err) {
228
+ return err instanceof Error && /\bcancell?ed\b/i.test(err.message);
229
+ }
196
230
  function chainMessages(err) {
197
231
  const parts = [];
198
232
  let cur = err;
@@ -217,10 +251,16 @@ function describeKnownError(err) {
217
251
  hint: "Set a valid OPENROUTER_API_KEY (https://openrouter.ai/keys). If it's already set, the key may be revoked, empty, or have a stray space."
218
252
  };
219
253
  }
254
+ if (msg.includes("fewer max_tokens") || msg.includes("can only afford")) {
255
+ return {
256
+ title: "Your OpenRouter account doesn't have enough credit for this run.",
257
+ hint: "Add credit (even a few dollars goes a long way) at https://openrouter.ai/settings/credits, then re-run. A free balance can't cover a full request."
258
+ };
259
+ }
220
260
  if (msg.includes("insufficient") || msg.includes("credits") || msg.includes("quota") || status === 402) {
221
261
  return {
222
262
  title: "OpenRouter ran out of credits for this account.",
223
- hint: "Top up your balance at https://openrouter.ai/credits, then re-run."
263
+ hint: "Add credit at https://openrouter.ai/settings/credits, then re-run."
224
264
  };
225
265
  }
226
266
  if (NoSuchModelError.isInstance(err) || msg.includes("not a valid model") || msg.includes("no endpoints found") || msg.includes("model not found")) {
@@ -240,7 +280,7 @@ function describeKnownError(err) {
240
280
  function supportReference(extra = {}) {
241
281
  const fields = {
242
282
  ref: getRunId(),
243
- version: process.env.npm_package_version,
283
+ version: CLI_VERSION,
244
284
  node: process.version,
245
285
  platform: `${process.platform}-${process.arch}`,
246
286
  ...extra
@@ -286,6 +326,7 @@ var init_errors = __esm({
286
326
  "use strict";
287
327
  init_esm_shims();
288
328
  init_analytics();
329
+ init_version();
289
330
  AgentError = class extends Error {
290
331
  constructor(message, phase, cause) {
291
332
  super(message);
@@ -650,7 +691,7 @@ var init_agent = __esm({
650
691
 
651
692
  // src/core/gitignore.ts
652
693
  import { readFile as readFile5 } from "fs/promises";
653
- import { join as join9, relative as relative2 } from "path";
694
+ import { join as join10, relative as relative2 } from "path";
654
695
  import { glob as glob2 } from "glob";
655
696
  async function loadGitignorePatterns(projectRoot) {
656
697
  const patterns = [
@@ -670,10 +711,10 @@ async function loadGitignorePatterns(projectRoot) {
670
711
  ];
671
712
  const matches = await glob2("**/.gitignore", { cwd: projectRoot, dot: true });
672
713
  for (const match of matches) {
673
- const fullPath = join9(projectRoot, match);
714
+ const fullPath = join10(projectRoot, match);
674
715
  try {
675
716
  const content = await readFile5(fullPath, "utf-8");
676
- const prefix = relative2(projectRoot, join9(projectRoot, match, ".."));
717
+ const prefix = relative2(projectRoot, join10(projectRoot, match, ".."));
677
718
  const parsed = parseGitignore(content, prefix);
678
719
  patterns.push(...parsed);
679
720
  } catch (err) {
@@ -897,7 +938,7 @@ var init_grep = __esm({
897
938
  // src/tools/list-directory.ts
898
939
  import { readdir } from "fs/promises";
899
940
  import { stat } from "fs/promises";
900
- import { join as join10, relative as relative3 } from "path";
941
+ import { join as join11, relative as relative3 } from "path";
901
942
  import { tool as tool4 } from "ai";
902
943
  import { z as z4 } from "zod";
903
944
  import { minimatch } from "minimatch";
@@ -922,7 +963,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
922
963
  const withTypes = [];
923
964
  for (const name of rawEntries) {
924
965
  try {
925
- const s = await stat(join10(dirPath, name));
966
+ const s = await stat(join11(dirPath, name));
926
967
  withTypes.push({ name, isDir: s.isDirectory() });
927
968
  } catch {
928
969
  withTypes.push({ name, isDir: false });
@@ -942,7 +983,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
942
983
  }
943
984
  if (entry.isDir) {
944
985
  const children = await buildTree(
945
- join10(dirPath, entry.name),
986
+ join11(dirPath, entry.name),
946
987
  maxDepth,
947
988
  currentDepth + 1,
948
989
  isIgnored,
@@ -993,7 +1034,7 @@ async function buildListDirectoryTool(workingDirectory) {
993
1034
  };
994
1035
  }
995
1036
  seen.add(cacheKey);
996
- const targetDir = input.path === "." ? workingDirectory : join10(workingDirectory, input.path);
1037
+ const targetDir = input.path === "." ? workingDirectory : join11(workingDirectory, input.path);
997
1038
  try {
998
1039
  const s = await stat(targetDir);
999
1040
  if (!s.isDirectory()) {
@@ -1176,7 +1217,7 @@ Be thorough but focused - only investigate what's relevant to your instruction.`
1176
1217
 
1177
1218
  // src/tools/write-file.ts
1178
1219
  import { writeFile as writeFile4, mkdir as mkdir2 } from "fs/promises";
1179
- import { dirname, relative as relative5, resolve as resolve3 } from "path";
1220
+ import { dirname as dirname2, relative as relative5, resolve as resolve3 } from "path";
1180
1221
  import { tool as tool7 } from "ai";
1181
1222
  import { z as z7 } from "zod";
1182
1223
  async function executeWriteFile(outputDirectory, filePath, content) {
@@ -1187,7 +1228,7 @@ async function executeWriteFile(outputDirectory, filePath, content) {
1187
1228
  return { error: "Cannot write files outside the output directory" };
1188
1229
  }
1189
1230
  try {
1190
- await mkdir2(dirname(absolutePath), { recursive: true });
1231
+ await mkdir2(dirname2(absolutePath), { recursive: true });
1191
1232
  await writeFile4(absolutePath, content, "utf-8");
1192
1233
  return { path: relativePath, bytesWritten: content.length };
1193
1234
  } catch (err) {
@@ -1365,12 +1406,12 @@ var init_pages_finder = __esm({
1365
1406
  // src/core/review.ts
1366
1407
  import * as p3 from "@clack/prompts";
1367
1408
  import { access } from "fs/promises";
1368
- import { join as join11, isAbsolute } from "path";
1409
+ import { join as join12, isAbsolute } from "path";
1369
1410
  import { spawn } from "child_process";
1370
1411
  import which from "which";
1371
1412
  function resolvePath(artifact, outputDir) {
1372
1413
  if (isAbsolute(artifact)) return artifact;
1373
- return join11(outputDir, artifact);
1414
+ return join12(outputDir, artifact);
1374
1415
  }
1375
1416
  async function detectEditors() {
1376
1417
  if (cachedEditors) return cachedEditors;
@@ -1442,7 +1483,7 @@ async function showResults(result, options) {
1442
1483
  if (result.artifacts.length === 0) {
1443
1484
  const knownFiles = ["AUTONOMA.md", "entity-audit.md", "scenarios.md"];
1444
1485
  for (const f of knownFiles) {
1445
- const fullPath = join11(options.outputDir, f);
1486
+ const fullPath = join12(options.outputDir, f);
1446
1487
  try {
1447
1488
  await access(fullPath);
1448
1489
  result.artifacts.push(f);
@@ -1675,12 +1716,12 @@ After the frontmatter, include:
1675
1716
 
1676
1717
  // src/agents/01-kb-generator/flows.ts
1677
1718
  import { readFile as readFile7 } from "fs/promises";
1678
- import { join as join12 } from "path";
1719
+ import { join as join13 } from "path";
1679
1720
  import matter from "gray-matter";
1680
1721
  async function parseCoreFlows(outputDir) {
1681
1722
  let raw;
1682
1723
  try {
1683
- raw = await readFile7(join12(outputDir, "AUTONOMA.md"), "utf-8");
1724
+ raw = await readFile7(join13(outputDir, "AUTONOMA.md"), "utf-8");
1684
1725
  } catch {
1685
1726
  return [];
1686
1727
  }
@@ -1752,7 +1793,7 @@ __export(kb_generator_exports, {
1752
1793
  import { tool as tool10 } from "ai";
1753
1794
  import { z as z10 } from "zod";
1754
1795
  import { readFile as readFile8 } from "fs/promises";
1755
- import { join as join13 } from "path";
1796
+ import { join as join14 } from "path";
1756
1797
  function buildRegisterPagesTool(tracker) {
1757
1798
  return tool10({
1758
1799
  description: "Register ALL page/route files discovered via glob. Call this ONCE after globbing for page files. The system will track which ones you've read and block finish until all are covered.",
@@ -1866,7 +1907,7 @@ Output files:
1866
1907
  };
1867
1908
  await runAgent(agentConfig, prompt, () => result);
1868
1909
  logger.summary();
1869
- const autonomaPath = join13(input.outputDir, "AUTONOMA.md");
1910
+ const autonomaPath = join14(input.outputDir, "AUTONOMA.md");
1870
1911
  const autonomaExists = await readFile8(autonomaPath, "utf-8").then(() => true).catch(() => false);
1871
1912
  if (!result?.success && autonomaExists) {
1872
1913
  result = {
@@ -1961,12 +2002,30 @@ var init_kb_generator = __esm({
1961
2002
 
1962
2003
  // src/agents/04-recipe-builder/entity-order.ts
1963
2004
  import { readFile as readFile9 } from "fs/promises";
1964
- import { join as join14 } from "path";
2005
+ import { join as join15 } from "path";
2006
+ import matter2 from "gray-matter";
2007
+ import { z as z11 } from "zod";
1965
2008
  async function parseEntityAudit(outputDir) {
1966
- const raw = await readFile9(join14(outputDir, "entity-audit.md"), "utf-8");
2009
+ const raw = await readFile9(join15(outputDir, "entity-audit.md"), "utf-8");
2010
+ try {
2011
+ const parsed = frontmatterSchema.safeParse(matter2(raw).data);
2012
+ if (parsed.success && parsed.data.models.length > 0) {
2013
+ return parsed.data.models.map((m) => ({
2014
+ name: m.name,
2015
+ independently_created: m.independently_created,
2016
+ creation_file: m.creation_file,
2017
+ creation_function: m.creation_function,
2018
+ side_effects: m.side_effects,
2019
+ created_by: m.created_by
2020
+ }));
2021
+ }
2022
+ } catch {
2023
+ }
2024
+ return parseAuditByLineScan(raw);
2025
+ }
2026
+ function parseAuditByLineScan(raw) {
1967
2027
  const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
1968
- if (!fmMatch) throw new Error("entity-audit.md has no YAML frontmatter");
1969
- const yaml = fmMatch[1];
2028
+ const yaml = fmMatch ? fmMatch[1] : raw;
1970
2029
  const models = [];
1971
2030
  let current = null;
1972
2031
  let inCreatedBy = false;
@@ -2120,10 +2179,28 @@ function getEntityDependencyChain(entityName, models, entityOrder) {
2120
2179
  walk(entityName);
2121
2180
  return chain;
2122
2181
  }
2182
+ var createdBySchema, auditedModelSchema, frontmatterSchema;
2123
2183
  var init_entity_order = __esm({
2124
2184
  "src/agents/04-recipe-builder/entity-order.ts"() {
2125
2185
  "use strict";
2126
2186
  init_esm_shims();
2187
+ createdBySchema = z11.object({
2188
+ owner: z11.string(),
2189
+ via: z11.string().optional(),
2190
+ why: z11.string().optional()
2191
+ });
2192
+ auditedModelSchema = z11.object({
2193
+ name: z11.string(),
2194
+ independently_created: z11.coerce.boolean().default(false),
2195
+ creation_file: z11.string().optional(),
2196
+ creation_function: z11.string().optional(),
2197
+ side_effects: z11.array(z11.string()).optional(),
2198
+ // Tolerate a stray `created_by:` with no entries (parsed as null by YAML).
2199
+ created_by: z11.array(createdBySchema).nullish().transform((v) => v ?? [])
2200
+ });
2201
+ frontmatterSchema = z11.object({
2202
+ models: z11.array(auditedModelSchema).nullish().transform((v) => v ?? [])
2203
+ });
2127
2204
  }
2128
2205
  });
2129
2206
 
@@ -2333,16 +2410,16 @@ __export(entity_audit_exports, {
2333
2410
  runEntityAudit: () => runEntityAudit
2334
2411
  });
2335
2412
  import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
2336
- import { join as join15 } from "path";
2413
+ import { join as join16 } from "path";
2337
2414
  import { tool as tool11 } from "ai";
2338
- import { z as z11 } from "zod";
2415
+ import { z as z12 } from "zod";
2339
2416
  import { glob as glob4 } from "glob";
2340
2417
  function buildRegisterModelsTool(tracker) {
2341
2418
  return tool11({
2342
2419
  description: "Register ALL database models discovered via grep. Call this ONCE after grepping for model definitions. After registering, use next_model to process them one at a time.",
2343
- inputSchema: z11.object({
2344
- models: z11.array(z11.string()).describe("All model/table names found by grep"),
2345
- framework: z11.string().describe("Database framework detected (e.g. 'sqlalchemy', 'prisma', 'drizzle')")
2420
+ inputSchema: z12.object({
2421
+ models: z12.array(z12.string()).describe("All model/table names found by grep"),
2422
+ framework: z12.string().describe("Database framework detected (e.g. 'sqlalchemy', 'prisma', 'drizzle')")
2346
2423
  }),
2347
2424
  execute: async (input) => {
2348
2425
  tracker.register(input.models);
@@ -2358,7 +2435,7 @@ function buildRegisterModelsTool(tracker) {
2358
2435
  function buildNextModelTool(tracker) {
2359
2436
  return tool11({
2360
2437
  description: "Get the next model to audit from the queue. If you called next_model before without calling mark_model_audited, the previous model is auto-skipped (marked as no creation path found). Returns done:true when all models are processed.",
2361
- inputSchema: z11.object({}),
2438
+ inputSchema: z12.object({}),
2362
2439
  execute: async () => {
2363
2440
  const next = tracker.nextModel();
2364
2441
  if (!next) {
@@ -2378,16 +2455,16 @@ function buildNextModelTool(tracker) {
2378
2455
  function buildMarkModelAuditedTool(tracker) {
2379
2456
  return tool11({
2380
2457
  description: "Mark a model as audited after you have determined its creation paths. Call this for EACH model after reading its creation code and determining independently_created + created_by. Include creation_function (e.g. 'UserService.create'), side_effects (list of things the creation does beyond the model itself), and for each created_by entry include owner, via (function name), and why (one sentence explaining the relationship).",
2381
- inputSchema: z11.object({
2382
- model: z11.string().describe("Model name"),
2383
- independently_created: z11.boolean(),
2384
- creation_file: z11.string().optional().describe("File containing the creation function"),
2385
- creation_function: z11.string().optional().describe("Function/method name (e.g. 'UserService.create' or 'create_user')"),
2386
- side_effects: z11.array(z11.string()).optional().describe("Side effects of creation (e.g. 'creates default Settings row', 'hashes password')"),
2387
- created_by: z11.array(z11.object({
2388
- owner: z11.string().describe("Owner model name"),
2389
- via: z11.string().optional().describe("Function that creates this model (e.g. 'OrganizationService.create')"),
2390
- why: z11.string().optional().describe("One sentence explaining why this model is created as a side effect")
2458
+ inputSchema: z12.object({
2459
+ model: z12.string().describe("Model name"),
2460
+ independently_created: z12.boolean(),
2461
+ creation_file: z12.string().optional().describe("File containing the creation function"),
2462
+ creation_function: z12.string().optional().describe("Function/method name (e.g. 'UserService.create' or 'create_user')"),
2463
+ side_effects: z12.array(z12.string()).optional().describe("Side effects of creation (e.g. 'creates default Settings row', 'hashes password')"),
2464
+ created_by: z12.array(z12.object({
2465
+ owner: z12.string().describe("Owner model name"),
2466
+ via: z12.string().optional().describe("Function that creates this model (e.g. 'OrganizationService.create')"),
2467
+ why: z12.string().optional().describe("One sentence explaining why this model is created as a side effect")
2391
2468
  })).describe("List of owner models that create this as a side effect, empty array if none")
2392
2469
  }),
2393
2470
  execute: async (input) => {
@@ -2425,16 +2502,16 @@ function buildMarkModelAuditedTool(tracker) {
2425
2502
  function buildModelCoverageTool(tracker) {
2426
2503
  return tool11({
2427
2504
  description: "Check how many registered models you've audited vs how many remain.",
2428
- inputSchema: z11.object({}),
2505
+ inputSchema: z12.object({}),
2429
2506
  execute: async () => tracker.coverage()
2430
2507
  });
2431
2508
  }
2432
2509
  function buildFinishTool2(tracker, onFinish) {
2433
2510
  return tool11({
2434
2511
  description: "Call when entity audit is complete. BLOCKED if there are unaudited models \u2014 call model_coverage first to check.",
2435
- inputSchema: z11.object({
2436
- summary: z11.string().describe("Summary of the audit"),
2437
- artifacts: z11.array(z11.string()).describe("Files written")
2512
+ inputSchema: z12.object({
2513
+ summary: z12.string().describe("Summary of the audit"),
2514
+ artifacts: z12.array(z12.string()).describe("Files written")
2438
2515
  }),
2439
2516
  execute: async (input) => {
2440
2517
  const cov = tracker.coverage();
@@ -2544,14 +2621,18 @@ write_file already targets the output directory \u2014 use just the filename.`;
2544
2621
  ${formatException(err)}`);
2545
2622
  }
2546
2623
  logger.summary();
2547
- if (!result && tracker.auditedModels.size > 0) {
2548
- const markdown = tracker.generateAuditMarkdown();
2549
- const auditPath = join15(input.outputDir, "entity-audit.md");
2550
- await writeFile5(auditPath, markdown, "utf-8");
2624
+ const writeCanonicalAudit = async () => {
2625
+ if (tracker.auditedModels.size === 0) return null;
2626
+ const auditPath = join16(input.outputDir, "entity-audit.md");
2627
+ await writeFile5(auditPath, tracker.generateAuditMarkdown(), "utf-8");
2628
+ return auditPath;
2629
+ };
2630
+ const canonicalPath = await writeCanonicalAudit();
2631
+ if (!result && canonicalPath) {
2551
2632
  const cov = tracker.coverage();
2552
2633
  result = {
2553
2634
  success: true,
2554
- artifacts: [auditPath],
2635
+ artifacts: [canonicalPath],
2555
2636
  summary: `Safety net: agent ran out of steps but audited ${cov.audited}/${cov.totalModels} models. File written from tracker data.`
2556
2637
  };
2557
2638
  }
@@ -2575,11 +2656,12 @@ Call model_coverage to see current state.
2575
2656
  Adjust based on the feedback. You can grep/read source files again if needed.
2576
2657
  When done with changes, call finish again.`;
2577
2658
  await runAgent(agentConfig, feedbackPrompt, () => result);
2659
+ await writeCanonicalAudit();
2578
2660
  return result;
2579
2661
  }
2580
2662
  });
2581
2663
  if (!reviewed) {
2582
- const auditPath = join15(input.outputDir, "entity-audit.md");
2664
+ const auditPath = join16(input.outputDir, "entity-audit.md");
2583
2665
  try {
2584
2666
  await readFile10(auditPath, "utf-8");
2585
2667
  return {
@@ -2721,10 +2803,10 @@ ${duals.length > 0 ? duals.map((m) => `- **${m.name}** \u2014 standalone: ${m.cr
2721
2803
 
2722
2804
  // src/core/parse-entity-audit.ts
2723
2805
  import { readFile as readFile11 } from "fs/promises";
2724
- import { join as join16 } from "path";
2806
+ import { join as join17 } from "path";
2725
2807
  async function parseEntityNames(outputDir) {
2726
2808
  try {
2727
- const content = await readFile11(join16(outputDir, "entity-audit.md"), "utf-8");
2809
+ const content = await readFile11(join17(outputDir, "entity-audit.md"), "utf-8");
2728
2810
  const names = [];
2729
2811
  for (const line of content.split("\n")) {
2730
2812
  const match = line.match(/^\s+-\s+name:\s+(.+)$/);
@@ -2744,17 +2826,17 @@ var init_parse_entity_audit = __esm({
2744
2826
 
2745
2827
  // src/agents/03-scenario-recipe/scenario-table.ts
2746
2828
  import { readFile as readFile12 } from "fs/promises";
2747
- import { join as join17 } from "path";
2748
- import matter2 from "gray-matter";
2829
+ import { join as join18 } from "path";
2830
+ import matter3 from "gray-matter";
2749
2831
  async function parseScenario(outputDir) {
2750
2832
  let raw;
2751
2833
  try {
2752
- raw = await readFile12(join17(outputDir, "scenarios.md"), "utf-8");
2834
+ raw = await readFile12(join18(outputDir, "scenarios.md"), "utf-8");
2753
2835
  } catch {
2754
2836
  return { scenarioNames: [], entityTypes: [] };
2755
2837
  }
2756
2838
  try {
2757
- const data = matter2(raw).data;
2839
+ const data = matter3(raw).data;
2758
2840
  const varsByEntity = /* @__PURE__ */ new Map();
2759
2841
  for (const vf of data.variable_fields ?? []) {
2760
2842
  const entity = vf?.entity != null ? String(vf.entity) : "";
@@ -2889,21 +2971,21 @@ __export(scenario_recipe_exports, {
2889
2971
  runScenarioRecipe: () => runScenarioRecipe
2890
2972
  });
2891
2973
  import { readFile as readFile13 } from "fs/promises";
2892
- import { join as join18 } from "path";
2974
+ import { join as join19 } from "path";
2893
2975
  import { tool as tool12 } from "ai";
2894
- import { z as z12 } from "zod";
2976
+ import { z as z13 } from "zod";
2895
2977
  function buildFinishTool3(requiredEntities, outputDir, onFinish) {
2896
2978
  return tool12({
2897
2979
  description: "Call when scenario design is complete and scenarios.md is written. BLOCKED if any required entities are missing from the scenario.",
2898
- inputSchema: z12.object({
2899
- summary: z12.string().describe("Summary of the scenario"),
2900
- entityCount: z12.number().describe("Number of entity types in the scenario"),
2901
- artifacts: z12.array(z12.string()).describe("Files written")
2980
+ inputSchema: z13.object({
2981
+ summary: z13.string().describe("Summary of the scenario"),
2982
+ entityCount: z13.number().describe("Number of entity types in the scenario"),
2983
+ artifacts: z13.array(z13.string()).describe("Files written")
2902
2984
  }),
2903
2985
  execute: async (input) => {
2904
2986
  if (requiredEntities.length > 0) {
2905
2987
  try {
2906
- const content = await readFile13(join18(outputDir, "scenarios.md"), "utf-8");
2988
+ const content = await readFile13(join19(outputDir, "scenarios.md"), "utf-8");
2907
2989
  const missing = requiredEntities.filter(
2908
2990
  (e) => !content.includes(e)
2909
2991
  );
@@ -2990,7 +3072,7 @@ When done with changes, call finish again.`;
2990
3072
  }
2991
3073
  });
2992
3074
  if (!reviewed) {
2993
- const scenariosPath = join18(input.outputDir, "scenarios.md");
3075
+ const scenariosPath = join19(input.outputDir, "scenarios.md");
2994
3076
  try {
2995
3077
  await readFile13(scenariosPath, "utf-8");
2996
3078
  return {
@@ -3047,7 +3129,7 @@ var init_scenario_recipe = __esm({
3047
3129
 
3048
3130
  // src/agents/04-recipe-builder/state.ts
3049
3131
  import { readFile as readFile14, writeFile as writeFile6 } from "fs/promises";
3050
- import { join as join19 } from "path";
3132
+ import { join as join20 } from "path";
3051
3133
  function adapterKey(a) {
3052
3134
  return `${a.language}:${a.framework}`;
3053
3135
  }
@@ -3074,14 +3156,14 @@ function initialRecipeState() {
3074
3156
  }
3075
3157
  async function loadRecipeState(outputDir) {
3076
3158
  try {
3077
- const raw = await readFile14(join19(outputDir, STATE_FILE2), "utf-8");
3159
+ const raw = await readFile14(join20(outputDir, STATE_FILE2), "utf-8");
3078
3160
  return JSON.parse(raw);
3079
3161
  } catch {
3080
3162
  return null;
3081
3163
  }
3082
3164
  }
3083
3165
  async function saveRecipeState(outputDir, state) {
3084
- await writeFile6(join19(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
3166
+ await writeFile6(join20(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
3085
3167
  }
3086
3168
  var ALL_ADAPTERS, ADAPTER_HINTS, STATE_FILE2;
3087
3169
  var init_state = __esm({
@@ -3128,7 +3210,7 @@ var init_state = __esm({
3128
3210
 
3129
3211
  // src/agents/04-recipe-builder/entity-relevance.ts
3130
3212
  import { Output, generateText } from "ai";
3131
- import { z as z13 } from "zod";
3213
+ import { z as z14 } from "zod";
3132
3214
  async function callRanker(model, prompt) {
3133
3215
  const result = await generateText({
3134
3216
  model,
@@ -3210,8 +3292,8 @@ var init_entity_relevance = __esm({
3210
3292
  "use strict";
3211
3293
  init_esm_shims();
3212
3294
  init_errors();
3213
- rankedSchema = z13.object({
3214
- ranked: z13.array(z13.string()).describe("Every entity name, ordered most-important first.")
3295
+ rankedSchema = z14.object({
3296
+ ranked: z14.array(z14.string()).describe("Every entity name, ordered most-important first.")
3215
3297
  });
3216
3298
  }
3217
3299
  });
@@ -3219,7 +3301,7 @@ var init_entity_relevance = __esm({
3219
3301
  // src/agents/04-recipe-builder/phases/tech-detect.ts
3220
3302
  import * as p4 from "@clack/prompts";
3221
3303
  import { tool as tool13 } from "ai";
3222
- import { z as z14 } from "zod";
3304
+ import { z as z15 } from "zod";
3223
3305
  async function detectTechStack(projectRoot, modelId, nonInteractive) {
3224
3306
  const model = getModel(modelId);
3225
3307
  const ignorePatterns = await loadGitignorePatterns(projectRoot);
@@ -3227,9 +3309,9 @@ async function detectTechStack(projectRoot, modelId, nonInteractive) {
3227
3309
  const { logger, onStepFinish } = buildDefaultStepLogger("tech-detect", 10);
3228
3310
  const finishTool = tool13({
3229
3311
  description: "Report the detected backend technology stack.",
3230
- inputSchema: z14.object({
3231
- language: z14.string().describe("Programming language: typescript, python, go, ruby, java, php, rust, elixir"),
3232
- framework: z14.string().describe("Web framework: express, node, hono, web, flask, fastapi, django, gin, rails, rack, spring, laravel, axum, actix, plug")
3312
+ inputSchema: z15.object({
3313
+ language: z15.string().describe("Programming language: typescript, python, go, ruby, java, php, rust, elixir"),
3314
+ framework: z15.string().describe("Web framework: express, node, hono, web, flask, fastapi, django, gin, rails, rack, spring, laravel, axum, actix, plug")
3233
3315
  }),
3234
3316
  execute: async (input) => {
3235
3317
  detected = input;
@@ -3311,11 +3393,11 @@ When done, call finish with your findings.`;
3311
3393
 
3312
3394
  // src/core/detect-pkg-manager.ts
3313
3395
  import { existsSync as existsSync2 } from "fs";
3314
- import { join as join20 } from "path";
3396
+ import { join as join21 } from "path";
3315
3397
  function detectPackageManager(projectRoot) {
3316
- if (existsSync2(join20(projectRoot, "bun.lock")) || existsSync2(join20(projectRoot, "bun.lockb"))) return "bun";
3317
- if (existsSync2(join20(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3318
- if (existsSync2(join20(projectRoot, "yarn.lock"))) return "yarn";
3398
+ if (existsSync2(join21(projectRoot, "bun.lock")) || existsSync2(join21(projectRoot, "bun.lockb"))) return "bun";
3399
+ if (existsSync2(join21(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3400
+ if (existsSync2(join21(projectRoot, "yarn.lock"))) return "yarn";
3319
3401
  return "npm";
3320
3402
  }
3321
3403
  function installCommand(pm, ...packages) {
@@ -3392,7 +3474,7 @@ var init_highlight = __esm({
3392
3474
 
3393
3475
  // src/agents/04-recipe-builder/recipe.ts
3394
3476
  import { readFile as readFile15, writeFile as writeFile7 } from "fs/promises";
3395
- import { join as join21 } from "path";
3477
+ import { join as join22 } from "path";
3396
3478
  function buildSingleEntityRecipe(entityName, models, entityOrder, allEntities) {
3397
3479
  const chain = getEntityDependencyChain(entityName, models, entityOrder);
3398
3480
  const recipe = {};
@@ -3436,7 +3518,7 @@ function buildSubmittableRecipe(create, description) {
3436
3518
  };
3437
3519
  }
3438
3520
  async function saveRecipe(outputDir, recipe) {
3439
- await writeFile7(join21(outputDir, RECIPE_FILE), JSON.stringify(recipe, null, 2), "utf-8");
3521
+ await writeFile7(join22(outputDir, RECIPE_FILE), JSON.stringify(recipe, null, 2), "utf-8");
3440
3522
  }
3441
3523
  var RECIPE_FILE;
3442
3524
  var init_recipe = __esm({
@@ -3494,24 +3576,108 @@ var init_http_client = __esm({
3494
3576
  }
3495
3577
  });
3496
3578
 
3579
+ // src/agents/04-recipe-builder/phases/failure-classifier.ts
3580
+ import { Output as Output2, generateText as generateText2 } from "ai";
3581
+ import { z as z16 } from "zod";
3582
+ function buildClassifierPrompt(args) {
3583
+ const errorText = typeof args.error === "string" ? args.error : JSON.stringify(args.error, null, 2);
3584
+ const phaseLine = args.phase === "teardown" ? `This was a DOWN (teardown) request. Teardown runs the developer's delete logic against data the create() step already accepted, so teardown failures are usually implementation-side (wrong delete order, foreign-key cleanup bugs) rather than caused by the recipe.` : `This was an UP (create) request \u2014 the factory tried to insert the recipe records.`;
3585
+ const statusLine = args.httpStatus != null ? `HTTP status: ${args.httpStatus}. (A 5xx usually means the handler threw; a 4xx is more often a rejected payload, but use the error text \u2014 status alone is not decisive.)` : `No HTTP status (the request threw before a response \u2014 often a network/server-process problem, lean implementation or unclear).`;
3586
+ return `${PRIMER}
3587
+
3588
+ - RECIPE DATA is wrong \u2014 the JSON the tool sent doesn't fit the developer's (correct) schema. Examples of the *kind* of problem (not an exhaustive list): a _ref points at an alias that does not exist among the valid targets listed below, a field holds a value the backend rejects, a required field is missing or an unknown field was sent, or a value has the wrong type. These are fixable by regenerating the data \u2014 the developer's code is fine.
3589
+ - IMPLEMENTATION is wrong \u2014 the developer's own handler/factory code is broken. Examples of the *kind* of problem: the factory for this entity is not registered, the handler references a column or table that does not exist (even though the recipe never mentioned it), the insert/delete logic has a bug, or the server threw an unhandled exception. No change to the test data can fix this; the developer must edit code.
3590
+
3591
+ ## How to decide
3592
+
3593
+ Ask: "Would sending DIFFERENT, corrected test data \u2014 still matching the intended schema \u2014 plausibly make this request succeed?"
3594
+ - If yes \u2192 **recipe**.
3595
+ - If the data shown below looks valid and the error points at the server's own logic, a missing factory, or a column/table the recipe never referenced \u2192 **implementation**.
3596
+ - If you genuinely cannot tell from the evidence \u2192 **unclear**. Prefer "unclear" over a confident guess; a wrong confident answer is worse than admitting uncertainty.
3597
+
3598
+ Cross-check the error against the actual data below before deciding \u2014 e.g. only call a _ref invalid if its target really is absent from the valid targets list.
3599
+
3600
+ ## Evidence
3601
+
3602
+ ${phaseLine}
3603
+ ${statusLine}
3604
+
3605
+ Entity: "${args.entityName}"
3606
+
3607
+ Valid _ref targets (aliases declared by already-created parent entities):
3608
+ ${args.validRefAliases?.trim() || "(none \u2014 this is a root entity with no parents to reference)"}
3609
+
3610
+ What the entity audit recorded about how "${args.entityName}" is created:
3611
+ ${args.entityAudit?.trim() || "(not available)"}
3612
+
3613
+ Test data sent:
3614
+ ${JSON.stringify(args.recipe, null, 2)}
3615
+
3616
+ Error:
3617
+ ${errorText}`;
3618
+ }
3619
+ async function classifyFailure(model, args) {
3620
+ try {
3621
+ const result = await generateText2({
3622
+ model,
3623
+ prompt: buildClassifierPrompt(args),
3624
+ output: Output2.object({ schema: classificationSchema })
3625
+ });
3626
+ return result.output;
3627
+ } catch (err) {
3628
+ return { side: "unclear", reason: `Could not auto-triage this error (${formatException(err)}).` };
3629
+ }
3630
+ }
3631
+ var classificationSchema, PRIMER;
3632
+ var init_failure_classifier = __esm({
3633
+ "src/agents/04-recipe-builder/phases/failure-classifier.ts"() {
3634
+ "use strict";
3635
+ init_esm_shims();
3636
+ init_errors();
3637
+ classificationSchema = z16.object({
3638
+ side: z16.enum(["recipe", "implementation", "unclear"]).describe(
3639
+ "recipe = the test data we sent is wrong and regenerating it could fix the failure; implementation = the developer's handler/factory code is wrong and only a code change fixes it; unclear = cannot confidently attribute the failure to either side."
3640
+ ),
3641
+ reason: z16.string().describe("One short, plain-language sentence explaining the verdict for the user. No code, no jargon dumps.")
3642
+ });
3643
+ PRIMER = `## Background \u2014 what you are looking at
3644
+
3645
+ The Autonoma SDK lets a developer seed and tear down test data through a single HTTP endpoint on their own backend (the "Environment Factory"). For each database entity the developer registers a *factory* with two functions: a create() that inserts records, and a teardown() that deletes them. Their handler code calls into their app's real service/ORM layer.
3646
+
3647
+ A separate tool (not the developer) generates the *recipe*: JSON test data, one array of records per entity. Each record may carry an "_alias" (a unique handle, e.g. "account_1") and "_ref" fields ({ "_ref": "account_1" }) that point at an alias declared by an already-created parent entity. The tool sends this recipe to the endpoint:
3648
+ - An **UP** request asks the factory to create the records (calls create()).
3649
+ - A **DOWN** request asks the factory to tear them down (calls teardown()).
3650
+
3651
+ So a failure has exactly two possible origins, and your only job is to tell them apart:`;
3652
+ }
3653
+ });
3654
+
3497
3655
  // src/agents/04-recipe-builder/phases/entity-loop.ts
3498
3656
  import * as p5 from "@clack/prompts";
3499
3657
  import { writeFile as writeFile8, readFile as readFile16 } from "fs/promises";
3500
- import { join as join22 } from "path";
3658
+ import { join as join23 } from "path";
3501
3659
  import { tmpdir } from "os";
3502
3660
  import { spawn as spawn2 } from "child_process";
3503
3661
  import { tool as tool14 } from "ai";
3504
- import { z as z15 } from "zod";
3662
+ import { z as z17 } from "zod";
3505
3663
  function summarizeCompletedAliases(completedEntities, excludeName) {
3506
3664
  return Object.entries(completedEntities).filter(([name, e]) => name !== excludeName && e.recipeData && e.recipeData.length > 0).map(([name, e]) => `${name}: aliases ${e.recipeData.map((r) => r._alias ?? "?").join(", ")}`).join("\n");
3507
3665
  }
3666
+ function summarizeEntityAudit(model) {
3667
+ if (!model) return void 0;
3668
+ const parts = [`independently_created: ${model.independently_created}`];
3669
+ if (model.creation_function) parts.push(`creation function: ${model.creation_function}`);
3670
+ if (model.created_by.length > 0) parts.push(`created by: ${model.created_by.map((c) => c.owner).join(", ")}`);
3671
+ if (model.side_effects?.length) parts.push(`side effects: ${model.side_effects.join(", ")}`);
3672
+ return parts.join("; ");
3673
+ }
3508
3674
  async function proposeRecipeData(entityName, entityIndex, totalEntities, model, outputDir, _projectRoot, completedEntities) {
3509
3675
  let result;
3510
3676
  const { logger, onStepFinish } = buildDefaultStepLogger(`propose:${entityName}`, 20);
3511
3677
  const finishTool = tool14({
3512
3678
  description: "Submit the proposed recipe data as a JSON array of records.",
3513
- inputSchema: z15.object({
3514
- records: z15.array(z15.record(z15.string(), z15.unknown())).describe("Array of record objects for this entity")
3679
+ inputSchema: z17.object({
3680
+ records: z17.array(z17.record(z17.string(), z17.unknown())).describe("Array of record objects for this entity")
3515
3681
  }),
3516
3682
  execute: async (input) => {
3517
3683
  result = input.records;
@@ -3552,8 +3718,8 @@ async function reviseRecipeData(entityName, entityIndex, totalEntities, current,
3552
3718
  let revised;
3553
3719
  const finishTool = tool14({
3554
3720
  description: "Submit the fixed recipe data.",
3555
- inputSchema: z15.object({
3556
- records: z15.array(z15.record(z15.string(), z15.unknown()))
3721
+ inputSchema: z17.object({
3722
+ records: z17.array(z17.record(z17.string(), z17.unknown()))
3557
3723
  }),
3558
3724
  execute: async (input) => {
3559
3725
  revised = input.records;
@@ -3609,8 +3775,8 @@ async function generateInstructions(entityName, entityIndex, totalEntities, isFi
3609
3775
  const { logger, onStepFinish } = buildDefaultStepLogger(`instructions:${entityName}`, 15);
3610
3776
  const finishTool = tool14({
3611
3777
  description: "Submit the implementation instructions.",
3612
- inputSchema: z15.object({
3613
- instructions: z15.string().describe("Complete, copy-pasteable implementation instructions")
3778
+ inputSchema: z17.object({
3779
+ instructions: z17.string().describe("Complete, copy-pasteable implementation instructions")
3614
3780
  }),
3615
3781
  execute: async (input) => {
3616
3782
  result = input.instructions;
@@ -3690,7 +3856,7 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
3690
3856
  if (p5.isCancel(action)) throw new Error("Recipe review cancelled");
3691
3857
  if (action === "keep") return proposed;
3692
3858
  if (action === "edit") {
3693
- const tmpPath = join22(tmpdir(), `autonoma-recipe-${entityName}.json`);
3859
+ const tmpPath = join23(tmpdir(), `autonoma-recipe-${entityName}.json`);
3694
3860
  await writeFile8(tmpPath, JSON.stringify(proposed, null, 2), "utf-8");
3695
3861
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
3696
3862
  p5.log.info(`Opening ${editor}... Save and close when done.`);
@@ -3727,30 +3893,74 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
3727
3893
  }
3728
3894
  }
3729
3895
  }
3730
- async function promptOnFailure(entityName, errorBody) {
3896
+ function formatErrorContext(errorBody) {
3897
+ if (errorBody == null) return "";
3898
+ const rendered = typeof errorBody === "string" ? errorBody : JSON.stringify(errorBody);
3899
+ return `
3900
+ Server error: ${rendered}`;
3901
+ }
3902
+ function seedFeedbackFromError(errorContext, reason) {
3903
+ const triage = reason ? ` (Auto-triage: ${reason})` : "";
3904
+ return { feedback: `The request failed \u2014 read the error and fix the recipe data.${triage}${errorContext}` };
3905
+ }
3906
+ async function promptOnFailure(entityName, errorBody, ctx, phase, httpStatus) {
3731
3907
  notify("Autonoma", `${entityName} \u2014 failed, action needed`);
3908
+ const errorContext = formatErrorContext(errorBody);
3909
+ const budgetLeft = ctx.budget.attempts < MAX_AUTOFIX_ATTEMPTS;
3910
+ const { side, reason } = await classifyFailure(ctx.model, {
3911
+ entityName,
3912
+ phase,
3913
+ httpStatus,
3914
+ error: errorBody,
3915
+ recipe: ctx.recipe,
3916
+ validRefAliases: ctx.validRefAliases,
3917
+ entityAudit: ctx.entityAudit
3918
+ });
3919
+ if (side === "recipe" && budgetLeft) {
3920
+ ctx.budget.attempts++;
3921
+ p5.log.info(
3922
+ `Triaged as a recipe-data issue \u2014 fixing automatically (attempt ${ctx.budget.attempts}/${MAX_AUTOFIX_ATTEMPTS}): ${reason}`
3923
+ );
3924
+ return seedFeedbackFromError(errorContext, reason);
3925
+ }
3926
+ const offerAutofix = side !== "implementation" && budgetLeft;
3927
+ if (side === "implementation") {
3928
+ p5.log.warn(`This looks like a handler/code issue, not the test data: ${reason}`);
3929
+ } else if (side === "recipe" && !budgetLeft) {
3930
+ p5.log.warn(`Autofix ran ${MAX_AUTOFIX_ATTEMPTS}\xD7 without resolving it \u2014 over to you. Latest read: ${reason}`);
3931
+ }
3732
3932
  const action = await p5.select({
3733
3933
  message: "What would you like to do?",
3734
3934
  options: [
3735
3935
  { value: "retry", label: "Yes, retry \u2014 I fixed my handler code", hint: "Send the same request again" },
3736
- { value: "feedback", label: "Yes, fix the recipe data", hint: "The request data is wrong \u2014 I'll explain what to change" },
3936
+ ...offerAutofix ? [
3937
+ {
3938
+ value: "autofix",
3939
+ label: "Yes, let the agent fix it from the error",
3940
+ hint: "The error explains itself \u2014 hand it to the agent, no typing"
3941
+ }
3942
+ ] : [],
3943
+ { value: "feedback", label: "Yes, fix the recipe data \u2014 I'll explain what's wrong", hint: "The request data is wrong and I'll describe the change" },
3737
3944
  { value: "skip", label: "No, skip this entity", hint: "Move on to the next entity" }
3738
3945
  ]
3739
3946
  });
3740
3947
  if (p5.isCancel(action)) throw new Error("Entity loop cancelled");
3741
3948
  if (action === "skip") return "skip";
3742
3949
  if (action === "retry") return "retry";
3950
+ if (action === "autofix") {
3951
+ ctx.budget.attempts++;
3952
+ return seedFeedbackFromError(errorContext);
3953
+ }
3743
3954
  const fb = await p5.text({
3744
3955
  message: "What's wrong with the recipe data?",
3745
3956
  placeholder: "e.g. Transaction references acc_1 but Account uses account_1 as its alias"
3746
3957
  });
3747
3958
  if (p5.isCancel(fb)) throw new Error("Entity loop cancelled");
3748
- const errorContext = errorBody ? `
3749
- Server error: ${JSON.stringify(errorBody)}` : "";
3750
3959
  return { feedback: `${fb.trim()}${errorContext}` };
3751
3960
  }
3752
- async function testUpDown(entityName, entityIndex, totalEntities, sdkConfig, recipe) {
3961
+ async function testUpDown(entityName, entityIndex, totalEntities, sdkConfig, recipe, grounding) {
3753
3962
  p5.log.info(`Let's verify this factory works. We'll send a test request to create ${entityName}, then check the database.`);
3963
+ const failureCtx = { ...grounding, recipe };
3754
3964
  while (true) {
3755
3965
  const testRunId = `test-${Date.now()}`;
3756
3966
  p5.log.step(`[${entityIndex + 1}/${totalEntities}] Sending UP request...`);
@@ -3760,7 +3970,7 @@ async function testUpDown(entityName, entityIndex, totalEntities, sdkConfig, rec
3760
3970
  } catch (err) {
3761
3971
  p5.log.error(`UP request failed:
3762
3972
  ${formatException(err)}`);
3763
- const action = await promptOnFailure(entityName, null);
3973
+ const action = await promptOnFailure(entityName, formatException(err), failureCtx, "create");
3764
3974
  if (action === "skip") return "skip";
3765
3975
  if (action === "retry") continue;
3766
3976
  return action;
@@ -3768,7 +3978,7 @@ ${formatException(err)}`);
3768
3978
  if (!upResult.ok) {
3769
3979
  p5.log.error(`UP failed (HTTP ${upResult.status}):`);
3770
3980
  console.log(JSON.stringify(upResult.body, null, 2));
3771
- const action = await promptOnFailure(entityName, upResult.body);
3981
+ const action = await promptOnFailure(entityName, upResult.body, failureCtx, "create", upResult.status);
3772
3982
  if (action === "skip") return "skip";
3773
3983
  if (action === "retry") continue;
3774
3984
  return action;
@@ -3789,7 +3999,7 @@ ${formatException(err)}`);
3789
3999
  } catch (err) {
3790
4000
  p5.log.error(`DOWN request failed:
3791
4001
  ${formatException(err)}`);
3792
- const action = await promptOnFailure(entityName, null);
4002
+ const action = await promptOnFailure(entityName, formatException(err), failureCtx, "teardown");
3793
4003
  if (action === "skip") return "skip";
3794
4004
  if (action === "retry") continue;
3795
4005
  return action;
@@ -3797,7 +4007,7 @@ ${formatException(err)}`);
3797
4007
  if (!downResult.ok) {
3798
4008
  p5.log.error(`DOWN failed (HTTP ${downResult.status}):`);
3799
4009
  console.log(JSON.stringify(downResult.body, null, 2));
3800
- const action = await promptOnFailure(entityName, downResult.body);
4010
+ const action = await promptOnFailure(entityName, downResult.body, failureCtx, "teardown", downResult.status);
3801
4011
  if (action === "skip") return "skip";
3802
4012
  if (action === "retry") continue;
3803
4013
  return action;
@@ -3905,7 +4115,7 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
3905
4115
  state.sharedSecret = secret;
3906
4116
  await saveRecipeState(outputDir, state);
3907
4117
  await writeFile8(
3908
- join22(outputDir, "autonoma-config.json"),
4118
+ join23(outputDir, "autonoma-config.json"),
3909
4119
  JSON.stringify({ sharedSecret: secret, endpointUrl: state.sdkEndpointUrl }, null, 2),
3910
4120
  "utf-8"
3911
4121
  );
@@ -3916,7 +4126,7 @@ Add this to your server's .env file and restart it.
3916
4126
  This is a 64-character hex key used for HMAC-SHA256 request signing.
3917
4127
  The same value must be set in both your server and the Autonoma dashboard.
3918
4128
 
3919
- Saved to: ${join22(outputDir, "autonoma-config.json")}`,
4129
+ Saved to: ${join23(outputDir, "autonoma-config.json")}`,
3920
4130
  "Shared secret generated"
3921
4131
  );
3922
4132
  const secretReady = await p5.confirm({
@@ -3938,7 +4148,7 @@ Saved to: ${join22(outputDir, "autonoma-config.json")}`,
3938
4148
  state.sdkEndpointUrl = url.trim() || "http://localhost:3000/api/autonoma";
3939
4149
  await saveRecipeState(outputDir, state);
3940
4150
  await writeFile8(
3941
- join22(outputDir, "autonoma-config.json"),
4151
+ join23(outputDir, "autonoma-config.json"),
3942
4152
  JSON.stringify({ sharedSecret: state.sharedSecret, endpointUrl: state.sdkEndpointUrl }, null, 2),
3943
4153
  "utf-8"
3944
4154
  );
@@ -3947,10 +4157,17 @@ Saved to: ${join22(outputDir, "autonoma-config.json")}`,
3947
4157
  endpointUrl: state.sdkEndpointUrl,
3948
4158
  sharedSecret: state.sharedSecret
3949
4159
  };
4160
+ const autofixBudget = { attempts: 0 };
4161
+ const grounding = {
4162
+ model,
4163
+ budget: autofixBudget,
4164
+ validRefAliases: summarizeCompletedAliases(state.entities, entityName),
4165
+ entityAudit: summarizeEntityAudit(models.find((m) => m.name === entityName))
4166
+ };
3950
4167
  let testDone = false;
3951
4168
  while (!testDone) {
3952
4169
  const singleRecipe = buildSingleEntityRecipe(entityName, models, state.entityOrder, state.entities);
3953
- const testResult = await testUpDown(entityName, i, total, sdkConfig, singleRecipe);
4170
+ const testResult = await testUpDown(entityName, i, total, sdkConfig, singleRecipe, grounding);
3954
4171
  if (testResult === "success") {
3955
4172
  state.entities[entityName].status = "tested-down";
3956
4173
  p5.log.success(`[${i + 1}/${total}] ${entityName} \u2014 factory verified`);
@@ -3983,7 +4200,7 @@ Saved to: ${join22(outputDir, "autonoma-config.json")}`,
3983
4200
  await saveRecipeState(outputDir, state);
3984
4201
  }
3985
4202
  }
3986
- var PROPOSAL_PROMPT, INSTRUCTIONS_PROMPT;
4203
+ var PROPOSAL_PROMPT, INSTRUCTIONS_PROMPT, MAX_AUTOFIX_ATTEMPTS;
3987
4204
  var init_entity_loop = __esm({
3988
4205
  "src/agents/04-recipe-builder/phases/entity-loop.ts"() {
3989
4206
  "use strict";
@@ -3999,6 +4216,7 @@ var init_entity_loop = __esm({
3999
4216
  init_notify();
4000
4217
  init_recipe();
4001
4218
  init_http_client();
4219
+ init_failure_classifier();
4002
4220
  PROPOSAL_PROMPT = `You are a recipe data designer. Given an entity from the entity audit and the scenario data, produce a JSON array of records for this entity.
4003
4221
 
4004
4222
  Rules:
@@ -4026,19 +4244,20 @@ Include:
4026
4244
  Be specific \u2014 reference actual file paths, function names, and types from the codebase.
4027
4245
 
4028
4246
  When done, call finish with the instructions text.`;
4247
+ MAX_AUTOFIX_ATTEMPTS = 2;
4029
4248
  }
4030
4249
  });
4031
4250
 
4032
4251
  // src/agents/04-recipe-builder/phases/full-validation.ts
4033
4252
  import * as p6 from "@clack/prompts";
4034
4253
  import { tool as tool15 } from "ai";
4035
- import { z as z16 } from "zod";
4254
+ import { z as z18 } from "zod";
4036
4255
  async function reviseFullRecipe(current, feedback, model, outputDir, entityOrder) {
4037
4256
  let revised;
4038
4257
  const finishTool = tool15({
4039
4258
  description: "Submit the revised full recipe: an object mapping each entity name to its array of records.",
4040
- inputSchema: z16.object({
4041
- recipe: z16.record(z16.string(), z16.array(z16.record(z16.string(), z16.unknown())))
4259
+ inputSchema: z18.object({
4260
+ recipe: z18.record(z18.string(), z18.array(z18.record(z18.string(), z18.unknown())))
4042
4261
  }),
4043
4262
  execute: async (input) => {
4044
4263
  revised = input.recipe;
@@ -4271,7 +4490,7 @@ __export(recipe_builder_exports, {
4271
4490
  runRecipeBuilder: () => runRecipeBuilder
4272
4491
  });
4273
4492
  import { readFile as readFile17 } from "fs/promises";
4274
- import { join as join23 } from "path";
4493
+ import { join as join24 } from "path";
4275
4494
  import * as p8 from "@clack/prompts";
4276
4495
  async function runRecipeBuilder(input) {
4277
4496
  const model = getModel(input.modelId);
@@ -4289,7 +4508,7 @@ async function runRecipeBuilder(input) {
4289
4508
  );
4290
4509
  let importanceRank;
4291
4510
  try {
4292
- const auditMarkdown = await readFile17(join23(input.outputDir, "entity-audit.md"), "utf-8");
4511
+ const auditMarkdown = await readFile17(join24(input.outputDir, "entity-audit.md"), "utf-8");
4293
4512
  importanceRank = await rankEntitiesByImportance(models, auditMarkdown, model);
4294
4513
  } catch {
4295
4514
  importanceRank = void 0;
@@ -4391,22 +4610,22 @@ var init_recipe_builder = __esm({
4391
4610
  });
4392
4611
 
4393
4612
  // src/agents/05-test-generator/rubrics.ts
4394
- import { z as z17 } from "zod";
4613
+ import { z as z19 } from "zod";
4395
4614
  var dimensionResultSchema, structuralIntentRubric, flowCompletenessRubric, uiTextRubric, dataAccuracyRubric, ALL_RUBRICS;
4396
4615
  var init_rubrics = __esm({
4397
4616
  "src/agents/05-test-generator/rubrics.ts"() {
4398
4617
  "use strict";
4399
4618
  init_esm_shims();
4400
- dimensionResultSchema = z17.object({
4401
- pass: z17.boolean(),
4402
- evidence: z17.string().describe("What you checked and found \u2014 cite file paths, line content, or specific strings"),
4403
- suggestion: z17.string().optional().describe("What the planner agent should fix, if failing")
4619
+ dimensionResultSchema = z19.object({
4620
+ pass: z19.boolean(),
4621
+ evidence: z19.string().describe("What you checked and found \u2014 cite file paths, line content, or specific strings"),
4622
+ suggestion: z19.string().optional().describe("What the planner agent should fix, if failing")
4404
4623
  });
4405
4624
  structuralIntentRubric = {
4406
4625
  name: "structural-intent",
4407
4626
  maxSteps: 8,
4408
4627
  dimensions: ["structuralValidity", "intentQuality", "missionAlignment"],
4409
- resultSchema: z17.object({
4628
+ resultSchema: z19.object({
4410
4629
  structuralValidity: dimensionResultSchema.describe(
4411
4630
  "Are all step verbs valid (click/type/scroll/assert/hover/drag/read/refresh)? Are asserts visual-only (no URLs, network, console)? No code selectors? No login steps?"
4412
4631
  ),
@@ -4447,7 +4666,7 @@ When done reviewing, call finish with your structured evaluation.`
4447
4666
  name: "flow-completeness",
4448
4667
  maxSteps: 12,
4449
4668
  dimensions: ["actionCompletion", "mutationVerification"],
4450
- resultSchema: z17.object({
4669
+ resultSchema: z19.object({
4451
4670
  actionCompletion: dimensionResultSchema.describe(
4452
4671
  "Does the test complete a core action and reach an OUTCOME? Not just opening a modal or clicking a tab."
4453
4672
  ),
@@ -4483,7 +4702,7 @@ When done reviewing, call finish with your structured evaluation.`
4483
4702
  name: "ui-text",
4484
4703
  maxSteps: 20,
4485
4704
  dimensions: ["uiTextAuthenticity"],
4486
- resultSchema: z17.object({
4705
+ resultSchema: z19.object({
4487
4706
  uiTextAuthenticity: dimensionResultSchema.describe(
4488
4707
  "Do all quoted strings in steps reference text a human would actually see on screen? Not translation keys, config paths, component names, enum identifiers, or CSS classes."
4489
4708
  )
@@ -4522,7 +4741,7 @@ When done reviewing, call finish with your structured evaluation.`
4522
4741
  name: "data-accuracy",
4523
4742
  maxSteps: 20,
4524
4743
  dimensions: ["dataAccuracy"],
4525
- resultSchema: z17.object({
4744
+ resultSchema: z19.object({
4526
4745
  dataAccuracy: dimensionResultSchema.describe(
4527
4746
  "Do the referenced UI elements (buttons, labels, fields, headings, toasts) actually exist in the source code for this page? Are default states correct? Does all test data (names, values, entities) come from the scenario data \u2014 NOT from other tests?"
4528
4747
  )
@@ -4638,7 +4857,7 @@ var init_review_pass = __esm({
4638
4857
 
4639
4858
  // src/agents/05-test-generator/review.ts
4640
4859
  import { readFile as readFile18 } from "fs/promises";
4641
- import { join as join24, relative as relative6, basename as basename3 } from "path";
4860
+ import { join as join25, relative as relative6, basename as basename3 } from "path";
4642
4861
  import { glob as glob5 } from "glob";
4643
4862
  import "ai";
4644
4863
  async function reviewSingleTest(testContent, testPath, projectRoot, model, scenarioData) {
@@ -4665,14 +4884,14 @@ async function reviewSingleTest(testContent, testPath, projectRoot, model, scena
4665
4884
  return merged;
4666
4885
  }
4667
4886
  async function runConsolidatedReview(outputDir, projectRoot, model) {
4668
- const testsDir = join24(outputDir, "qa-tests");
4887
+ const testsDir = join25(outputDir, "qa-tests");
4669
4888
  const logger = createStepLogger("review", 5);
4670
4889
  let scenarioData;
4671
4890
  try {
4672
- scenarioData = await readFile18(join24(outputDir, "scenarios.md"), "utf-8");
4891
+ scenarioData = await readFile18(join25(outputDir, "scenarios.md"), "utf-8");
4673
4892
  } catch {
4674
4893
  }
4675
- const testFiles = await glob5(join24(testsDir, "**/*.md"));
4894
+ const testFiles = await glob5(join25(testsDir, "**/*.md"));
4676
4895
  const tests = [];
4677
4896
  for (const testPath of testFiles) {
4678
4897
  if (basename3(testPath) === "INDEX.md") continue;
@@ -4755,13 +4974,13 @@ var init_review2 = __esm({
4755
4974
 
4756
4975
  // src/agents/05-test-generator/graph.ts
4757
4976
  import { readFile as readFile19, writeFile as writeFile9 } from "fs/promises";
4758
- import { join as join25 } from "path";
4977
+ import { join as join26 } from "path";
4759
4978
  async function saveBfsState(outputDir, state) {
4760
- const path3 = join25(outputDir, STATE_FILE3);
4979
+ const path3 = join26(outputDir, STATE_FILE3);
4761
4980
  await writeFile9(path3, JSON.stringify(state.serialize(), null, 2), "utf-8");
4762
4981
  }
4763
4982
  async function loadBfsState(outputDir) {
4764
- const path3 = join25(outputDir, STATE_FILE3);
4983
+ const path3 = join26(outputDir, STATE_FILE3);
4765
4984
  try {
4766
4985
  const raw = await readFile19(path3, "utf-8");
4767
4986
  return CoverageState.deserialize(JSON.parse(raw));
@@ -4856,16 +5075,16 @@ var init_graph = __esm({
4856
5075
 
4857
5076
  // src/agents/00b-feature-discovery/index.ts
4858
5077
  import { readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
4859
- import { join as join26 } from "path";
4860
- import { z as z18 } from "zod";
5078
+ import { join as join27 } from "path";
5079
+ import { z as z20 } from "zod";
4861
5080
  import { tool as tool17 } from "ai";
4862
5081
  async function saveFeatures(outputDir, features) {
4863
5082
  const obj = Object.fromEntries(features);
4864
- await writeFile10(join26(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
5083
+ await writeFile10(join27(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
4865
5084
  }
4866
5085
  async function loadFeatures(outputDir) {
4867
5086
  try {
4868
- const raw = await readFile20(join26(outputDir, FEATURES_FILE), "utf-8");
5087
+ const raw = await readFile20(join27(outputDir, FEATURES_FILE), "utf-8");
4869
5088
  const obj = JSON.parse(raw);
4870
5089
  return new Map(Object.entries(obj));
4871
5090
  } catch {
@@ -4899,7 +5118,7 @@ Process every page. Call add_feature for each sub-feature you discover. When don
4899
5118
  add_feature: tool17({
4900
5119
  description: "Add a discovered sub-feature",
4901
5120
  inputSchema: Feature.extend({
4902
- id: z18.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
5121
+ id: z20.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
4903
5122
  }),
4904
5123
  execute: (featureInput) => {
4905
5124
  const { id, ...rest } = featureInput;
@@ -4913,17 +5132,17 @@ Process every page. Call add_feature for each sub-feature you discover. When don
4913
5132
  }),
4914
5133
  view_features: tool17({
4915
5134
  description: "View all discovered features so far",
4916
- inputSchema: z18.object({}),
5135
+ inputSchema: z20.object({}),
4917
5136
  execute: () => collector.viewFeatures()
4918
5137
  }),
4919
5138
  view_pages: tool17({
4920
5139
  description: "View the pages list to know what to analyze",
4921
- inputSchema: z18.object({}),
5140
+ inputSchema: z20.object({}),
4922
5141
  execute: () => pagesDescription
4923
5142
  }),
4924
5143
  finish: tool17({
4925
5144
  description: "Signal that feature discovery is complete",
4926
- inputSchema: z18.object({ summary: z18.string() }),
5145
+ inputSchema: z20.object({ summary: z20.string() }),
4927
5146
  execute: async (finishInput) => {
4928
5147
  result = {
4929
5148
  success: true,
@@ -4954,13 +5173,13 @@ var init_b_feature_discovery = __esm({
4954
5173
  init_model();
4955
5174
  init_tools();
4956
5175
  FEATURES_FILE = "features.json";
4957
- Feature = z18.object({
4958
- name: z18.string().min(1).describe("Human-readable name (e.g. 'Settings > Notifications Tab', 'Create Project Modal')"),
4959
- type: z18.enum(["tab", "modal", "form", "table", "wizard", "nested-route", "complex-component"]),
4960
- parentPagePath: z18.string().min(1).describe("The page path this feature belongs to (from the pages list)"),
4961
- sourceFiles: z18.array(z18.string()).min(1).describe("Relative paths to the source files for this sub-feature"),
4962
- interactiveElements: z18.number().int().min(0).describe("Count of interactive elements found (buttons, inputs, toggles, etc.)"),
4963
- description: z18.string().min(10).describe("What this sub-feature does")
5176
+ Feature = z20.object({
5177
+ name: z20.string().min(1).describe("Human-readable name (e.g. 'Settings > Notifications Tab', 'Create Project Modal')"),
5178
+ type: z20.enum(["tab", "modal", "form", "table", "wizard", "nested-route", "complex-component"]),
5179
+ parentPagePath: z20.string().min(1).describe("The page path this feature belongs to (from the pages list)"),
5180
+ sourceFiles: z20.array(z20.string()).min(1).describe("Relative paths to the source files for this sub-feature"),
5181
+ interactiveElements: z20.number().int().min(0).describe("Count of interactive elements found (buttons, inputs, toggles, etc.)"),
5182
+ description: z20.string().min(10).describe("What this sub-feature does")
4964
5183
  });
4965
5184
  FeatureCollector = class {
4966
5185
  features = /* @__PURE__ */ new Map();
@@ -5041,14 +5260,14 @@ Use kebab-case IDs that indicate the parent page and feature type:
5041
5260
  });
5042
5261
 
5043
5262
  // src/agents/05-test-generator/validation.ts
5044
- import matter3 from "gray-matter";
5263
+ import matter4 from "gray-matter";
5045
5264
  function validateTestContent(content) {
5046
5265
  const errors = [];
5047
5266
  if (!/^---\n[\s\S]*?\n---/.test(content)) {
5048
5267
  errors.push("Missing frontmatter");
5049
5268
  } else {
5050
5269
  try {
5051
- const { data } = matter3(content);
5270
+ const { data } = matter4(content);
5052
5271
  if (!data.verification || typeof data.verification !== "string" || data.verification.length < 20) {
5053
5272
  errors.push("Missing or insufficient 'verification' field in frontmatter \u2014 must describe WHERE to navigate and WHAT to assert at the source of truth");
5054
5273
  }
@@ -5103,18 +5322,18 @@ var init_validation = __esm({
5103
5322
 
5104
5323
  // src/agents/05-test-generator/tools.ts
5105
5324
  import { mkdir as mkdir3, writeFile as writeFile11 } from "fs/promises";
5106
- import { dirname as dirname2, join as join27 } from "path";
5325
+ import { dirname as dirname3, join as join28 } from "path";
5107
5326
  import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as tool18, ToolLoopAgent as ToolLoopAgent3 } from "ai";
5108
- import matter4 from "gray-matter";
5109
- import { z as z19 } from "zod";
5327
+ import matter5 from "gray-matter";
5328
+ import { z as z21 } from "zod";
5110
5329
  function buildWriteTestTool(state, outputDir) {
5111
5330
  return tool18({
5112
5331
  description: "Write a test file to qa-tests/{folder}/{filename}.md. Validates frontmatter before writing. Returns error if frontmatter is invalid.",
5113
- inputSchema: z19.object({
5114
- folder: z19.string().describe("Subfolder name under qa-tests/"),
5115
- filename: z19.string().describe("File name (e.g. login-valid-credentials.md)"),
5116
- content: z19.string().describe("Full file content including YAML frontmatter"),
5117
- nodeId: z19.string().describe("The FeatureNode ID this test belongs to")
5332
+ inputSchema: z21.object({
5333
+ folder: z21.string().describe("Subfolder name under qa-tests/"),
5334
+ filename: z21.string().describe("File name (e.g. login-valid-credentials.md)"),
5335
+ content: z21.string().describe("Full file content including YAML frontmatter"),
5336
+ nodeId: z21.string().describe("The FeatureNode ID this test belongs to")
5118
5337
  }),
5119
5338
  execute: async (input) => {
5120
5339
  const frontmatter = extractFrontmatter(input.content);
@@ -5163,10 +5382,10 @@ function buildWriteTestTool(state, outputDir) {
5163
5382
  };
5164
5383
  }
5165
5384
  }
5166
- const relPath = join27("qa-tests", input.folder, input.filename);
5167
- const absPath = join27(outputDir, relPath);
5385
+ const relPath = join28("qa-tests", input.folder, input.filename);
5386
+ const absPath = join28(outputDir, relPath);
5168
5387
  try {
5169
- await mkdir3(dirname2(absPath), { recursive: true });
5388
+ await mkdir3(dirname3(absPath), { recursive: true });
5170
5389
  await writeFile11(absPath, input.content, "utf-8");
5171
5390
  state.markTested(input.nodeId, [relPath]);
5172
5391
  await saveBfsState(outputDir, state);
@@ -5181,14 +5400,14 @@ function buildWriteTestTool(state, outputDir) {
5181
5400
  function buildCreateFolderTool(outputDir) {
5182
5401
  return tool18({
5183
5402
  description: "Create a folder under qa-tests/ for organizing tests.",
5184
- inputSchema: z19.object({
5185
- folder: z19.string().describe("Folder name (kebab-case)")
5403
+ inputSchema: z21.object({
5404
+ folder: z21.string().describe("Folder name (kebab-case)")
5186
5405
  }),
5187
5406
  execute: async (input) => {
5188
- const absPath = join27(outputDir, "qa-tests", input.folder);
5407
+ const absPath = join28(outputDir, "qa-tests", input.folder);
5189
5408
  try {
5190
5409
  await mkdir3(absPath, { recursive: true });
5191
- return { path: join27("qa-tests", input.folder) };
5410
+ return { path: join28("qa-tests", input.folder) };
5192
5411
  } catch (err) {
5193
5412
  const message = err instanceof Error ? err.message : String(err);
5194
5413
  return { error: `Failed to create folder: ${message}` };
@@ -5199,7 +5418,7 @@ function buildCreateFolderTool(outputDir) {
5199
5418
  function buildNextNodeTool(state, outputDir) {
5200
5419
  return tool18({
5201
5420
  description: "Get the next node to write tests for. If you called next_node before without writing any tests (via write_test), the previous node is auto-skipped. Returns done:true when all nodes are processed.",
5202
- inputSchema: z19.object({}),
5421
+ inputSchema: z21.object({}),
5203
5422
  execute: async () => {
5204
5423
  const next = state.nextNode();
5205
5424
  await saveBfsState(outputDir, state);
@@ -5228,7 +5447,7 @@ function buildNextNodeTool(state, outputDir) {
5228
5447
  function buildGetProgressTool(state) {
5229
5448
  return tool18({
5230
5449
  description: "Check how many nodes have been tested vs how many remain.",
5231
- inputSchema: z19.object({}),
5450
+ inputSchema: z21.object({}),
5232
5451
  execute: async () => {
5233
5452
  const stats = state.summary();
5234
5453
  const nodes = [...state.nodes.values()].map((n) => ({
@@ -5244,12 +5463,12 @@ function buildGetProgressTool(state) {
5244
5463
  function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
5245
5464
  return tool18({
5246
5465
  description: "Spawn a research subagent to read and analyze source files without polluting your context. Use for complex sub-features where you don't want to read 20 files yourself.",
5247
- inputSchema: z19.object({
5248
- instruction: z19.string().describe("What to research \u2014 be specific about files and what to look for")
5466
+ inputSchema: z21.object({
5467
+ instruction: z21.string().describe("What to research \u2014 be specific about files and what to look for")
5249
5468
  }),
5250
5469
  execute: async (input) => {
5251
- const resultSchema2 = z19.object({
5252
- findings: z19.string().describe("Summary of what was found")
5470
+ const resultSchema2 = z21.object({
5471
+ findings: z21.string().describe("Summary of what was found")
5253
5472
  });
5254
5473
  let result;
5255
5474
  const subagent = new ToolLoopAgent3({
@@ -5287,7 +5506,7 @@ function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
5287
5506
  }
5288
5507
  function extractFrontmatter(content) {
5289
5508
  try {
5290
- const { data } = matter4(content);
5509
+ const { data } = matter5(content);
5291
5510
  return data && Object.keys(data).length > 0 ? data : null;
5292
5511
  } catch {
5293
5512
  return null;
@@ -5301,14 +5520,14 @@ var init_tools2 = __esm({
5301
5520
  init_tools();
5302
5521
  init_graph();
5303
5522
  init_validation();
5304
- testFrontmatterSchema = z19.object({
5305
- title: z19.string().min(1),
5306
- description: z19.string().min(1),
5307
- intent: z19.string().min(30, "Intent must be at least 30 characters \u2014 describe the BEHAVIOR being tested, not the steps"),
5308
- criticality: z19.enum(["critical", "high", "mid", "low"]),
5309
- scenario: z19.string().min(1),
5310
- flow: z19.string().min(1),
5311
- verification: z19.string().min(20, "Verification must describe WHERE to navigate and WHAT to assert at the source of truth \u2014 not UI acknowledgments like toasts")
5523
+ testFrontmatterSchema = z21.object({
5524
+ title: z21.string().min(1),
5525
+ description: z21.string().min(1),
5526
+ intent: z21.string().min(30, "Intent must be at least 30 characters \u2014 describe the BEHAVIOR being tested, not the steps"),
5527
+ criticality: z21.enum(["critical", "high", "mid", "low"]),
5528
+ scenario: z21.string().min(1),
5529
+ flow: z21.string().min(1),
5530
+ verification: z21.string().min(20, "Verification must describe WHERE to navigate and WHAT to assert at the source of truth \u2014 not UI acknowledgments like toasts")
5312
5531
  });
5313
5532
  }
5314
5533
  });
@@ -5705,9 +5924,9 @@ __export(test_generator_exports, {
5705
5924
  runTestGenerator: () => runTestGenerator
5706
5925
  });
5707
5926
  import { mkdir as mkdir4, readFile as readFile21, rmdir, unlink, writeFile as writeFile12 } from "fs/promises";
5708
- import { basename as basename4, join as join28 } from "path";
5927
+ import { basename as basename4, join as join29 } from "path";
5709
5928
  import { tool as tool19 } from "ai";
5710
- import { z as z20 } from "zod";
5929
+ import { z as z22 } from "zod";
5711
5930
  import { glob as glob6 } from "glob";
5712
5931
  async function preseedQueue(state, projectRoot, pages, features) {
5713
5932
  let seeded = 0;
@@ -5757,8 +5976,8 @@ async function runTestGenerator(input) {
5757
5976
  let result;
5758
5977
  const finishTool = tool19({
5759
5978
  description: "Call when the BFS queue is empty and all routes have been explored.",
5760
- inputSchema: z20.object({
5761
- summary: z20.string().describe("Coverage summary")
5979
+ inputSchema: z22.object({
5980
+ summary: z22.string().describe("Coverage summary")
5762
5981
  }),
5763
5982
  execute: async (finishInput) => {
5764
5983
  const stats = state.summary();
@@ -5788,7 +6007,7 @@ async function runTestGenerator(input) {
5788
6007
  let kbContext = "";
5789
6008
  try {
5790
6009
  const autonomaMd = await readFile21(
5791
- join28(input.outputDir, "AUTONOMA.md"),
6010
+ join29(input.outputDir, "AUTONOMA.md"),
5792
6011
  "utf-8"
5793
6012
  );
5794
6013
  kbContext += `
@@ -5800,7 +6019,7 @@ ${autonomaMd}
5800
6019
  }
5801
6020
  try {
5802
6021
  const scenariosMd = await readFile21(
5803
- join28(input.outputDir, "scenarios.md"),
6022
+ join29(input.outputDir, "scenarios.md"),
5804
6023
  "utf-8"
5805
6024
  );
5806
6025
  kbContext += `
@@ -5996,7 +6215,7 @@ IMPORTANT: Do NOT try to finish early. Process every node via next_node until it
5996
6215
  console.log(` Fix pass complete`);
5997
6216
  }
5998
6217
  const allTestFiles = await glob6(
5999
- join28(input.outputDir, "qa-tests", "**/*.md")
6218
+ join29(input.outputDir, "qa-tests", "**/*.md")
6000
6219
  );
6001
6220
  let markedInvalid = 0;
6002
6221
  for (const testPath of allTestFiles) {
@@ -6005,9 +6224,9 @@ IMPORTANT: Do NOT try to finish early. Process every node via next_node until it
6005
6224
  const content = await readFile21(testPath, "utf-8");
6006
6225
  const validation = validateTestContent(content);
6007
6226
  if (!validation.valid) {
6008
- const invalidDir = join28(input.outputDir, "qa-tests", "_invalid");
6227
+ const invalidDir = join29(input.outputDir, "qa-tests", "_invalid");
6009
6228
  await mkdir4(invalidDir, { recursive: true });
6010
- const dest = join28(invalidDir, basename4(testPath));
6229
+ const dest = join29(invalidDir, basename4(testPath));
6011
6230
  const annotated = `<!-- VALIDATION ERRORS: ${validation.errors.join("; ")} -->
6012
6231
  ${content}`;
6013
6232
  await writeFile12(dest, annotated, "utf-8");
@@ -6020,7 +6239,7 @@ ${content}`;
6020
6239
  ` ${markedInvalid} tests still invalid after review cycles \u2014 moved to _invalid/`
6021
6240
  );
6022
6241
  }
6023
- const dirs = await glob6(join28(input.outputDir, "qa-tests", "**/"), {
6242
+ const dirs = await glob6(join29(input.outputDir, "qa-tests", "**/"), {
6024
6243
  dot: false
6025
6244
  });
6026
6245
  for (const dir of dirs.sort((a, b) => b.length - a.length)) {
@@ -6112,7 +6331,7 @@ async function generateIndex(outputDir, state) {
6112
6331
  for (const paths of state.testsWritten.values()) {
6113
6332
  for (const p10 of paths) {
6114
6333
  try {
6115
- const content2 = await readFile21(join28(outputDir, p10), "utf-8");
6334
+ const content2 = await readFile21(join29(outputDir, p10), "utf-8");
6116
6335
  const critMatch = content2.match(/criticality:\s*(\w+)/);
6117
6336
  const critVal = critMatch?.[1] ?? "";
6118
6337
  if (critCounts.has(critVal))
@@ -6163,22 +6382,22 @@ ${folders.map((f) => `| ${f.name} | ${f.test_count} |`).join("\n")}
6163
6382
 
6164
6383
  ${[...testsByFolder.entries()].flatMap(([_folder, tests]) => tests.map((t) => `- \`${t}\``)).join("\n")}
6165
6384
  `;
6166
- await writeFile12(join28(outputDir, "qa-tests", "INDEX.md"), content, "utf-8");
6385
+ await writeFile12(join29(outputDir, "qa-tests", "INDEX.md"), content, "utf-8");
6167
6386
  }
6168
6387
  async function generateJourneyTests(outputDir, model, projectRoot) {
6169
6388
  const logger = createStepLogger("journeys", 50);
6170
6389
  let autonomaMd = "";
6171
6390
  let scenariosMd = "";
6172
6391
  try {
6173
- autonomaMd = await readFile21(join28(outputDir, "AUTONOMA.md"), "utf-8");
6392
+ autonomaMd = await readFile21(join29(outputDir, "AUTONOMA.md"), "utf-8");
6174
6393
  } catch {
6175
6394
  }
6176
6395
  try {
6177
- scenariosMd = await readFile21(join28(outputDir, "scenarios.md"), "utf-8");
6396
+ scenariosMd = await readFile21(join29(outputDir, "scenarios.md"), "utf-8");
6178
6397
  } catch {
6179
6398
  }
6180
6399
  if (!autonomaMd) return 0;
6181
- const existingTests = await glob6(join28(outputDir, "qa-tests", "**/*.md"));
6400
+ const existingTests = await glob6(join29(outputDir, "qa-tests", "**/*.md"));
6182
6401
  const existingTitles = [];
6183
6402
  for (const t of existingTests) {
6184
6403
  if (basename4(t) === "INDEX.md") continue;
@@ -6227,7 +6446,7 @@ Write 5-8 journey tests using the write_test tool with folder "journeys". Then c
6227
6446
  let journeyResult;
6228
6447
  const journeyFinish = tool19({
6229
6448
  description: "Signal journey generation is complete.",
6230
- inputSchema: z20.object({ summary: z20.string() }),
6449
+ inputSchema: z22.object({ summary: z22.string() }),
6231
6450
  execute: async (finishInput) => {
6232
6451
  journeyResult = {
6233
6452
  success: true,
@@ -6289,7 +6508,7 @@ var init_test_generator = __esm({
6289
6508
  init_esm_shims();
6290
6509
  import * as p9 from "@clack/prompts";
6291
6510
  import { readFile as readFile22, writeFile as writeFile13 } from "fs/promises";
6292
- import { join as join29 } from "path";
6511
+ import { join as join30 } from "path";
6293
6512
 
6294
6513
  // src/config.ts
6295
6514
  init_esm_shims();
@@ -6501,7 +6720,7 @@ init_notify();
6501
6720
  // src/core/upload.ts
6502
6721
  init_esm_shims();
6503
6722
  import { readFile as readFile3 } from "fs/promises";
6504
- import { basename, join as join7, relative } from "path";
6723
+ import { basename, join as join8, relative } from "path";
6505
6724
  import * as p from "@clack/prompts";
6506
6725
  import { glob } from "glob";
6507
6726
 
@@ -6509,7 +6728,7 @@ import { glob } from "glob";
6509
6728
  init_esm_shims();
6510
6729
  import { execFile as execFile2 } from "child_process";
6511
6730
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
6512
- import { join as join6 } from "path";
6731
+ import { join as join7 } from "path";
6513
6732
  import { promisify } from "util";
6514
6733
  var execFileAsync = promisify(execFile2);
6515
6734
  var GIT_INFO_FILE = ".git-info.json";
@@ -6533,11 +6752,11 @@ async function readGitInfo(projectRoot) {
6533
6752
  };
6534
6753
  }
6535
6754
  async function saveGitInfo(outputDir, info) {
6536
- await writeFile2(join6(outputDir, GIT_INFO_FILE), JSON.stringify(info, null, 2), "utf-8");
6755
+ await writeFile2(join7(outputDir, GIT_INFO_FILE), JSON.stringify(info, null, 2), "utf-8");
6537
6756
  }
6538
6757
  async function loadGitInfo(outputDir) {
6539
6758
  try {
6540
- const raw = await readFile2(join6(outputDir, GIT_INFO_FILE), "utf-8");
6759
+ const raw = await readFile2(join7(outputDir, GIT_INFO_FILE), "utf-8");
6541
6760
  const parsed = JSON.parse(raw);
6542
6761
  if (typeof parsed === "object" && parsed != null && "sha" in parsed && typeof parsed.sha === "string") {
6543
6762
  const branch = "branch" in parsed && typeof parsed.branch === "string" ? parsed.branch : void 0;
@@ -6556,7 +6775,7 @@ async function readArtifacts(outputDir) {
6556
6775
  const files = [];
6557
6776
  for (const name of ARTIFACT_FILES) {
6558
6777
  try {
6559
- const content = await readFile3(join7(outputDir, name), "utf-8");
6778
+ const content = await readFile3(join8(outputDir, name), "utf-8");
6560
6779
  files.push({ name, content });
6561
6780
  } catch {
6562
6781
  }
@@ -6564,13 +6783,13 @@ async function readArtifacts(outputDir) {
6564
6783
  return files;
6565
6784
  }
6566
6785
  async function readTestCases(outputDir) {
6567
- const testsDir = join7(outputDir, "qa-tests");
6786
+ const testsDir = join8(outputDir, "qa-tests");
6568
6787
  const matches = await glob("**/*.md", { cwd: testsDir, nodir: true });
6569
6788
  const files = [];
6570
6789
  for (const match of matches) {
6571
6790
  const name = basename(match);
6572
6791
  if (name === "INDEX.md") continue;
6573
- const content = await readFile3(join7(testsDir, match), "utf-8");
6792
+ const content = await readFile3(join8(testsDir, match), "utf-8");
6574
6793
  const folderPath = relative(".", match).split("/").slice(0, -1).join("/");
6575
6794
  files.push({ name, content, folder: folderPath.length > 0 ? folderPath : void 0 });
6576
6795
  }
@@ -6630,7 +6849,7 @@ async function uploadArtifacts(config, outputDir) {
6630
6849
  // src/core/state.ts
6631
6850
  init_esm_shims();
6632
6851
  import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
6633
- import { join as join8 } from "path";
6852
+ import { join as join9 } from "path";
6634
6853
  var STATE_FILE = ".pipeline-state.json";
6635
6854
  function initialState() {
6636
6855
  return {
@@ -6645,7 +6864,7 @@ function initialState() {
6645
6864
  };
6646
6865
  }
6647
6866
  async function loadState(outputDir) {
6648
- const path3 = join8(outputDir, STATE_FILE);
6867
+ const path3 = join9(outputDir, STATE_FILE);
6649
6868
  try {
6650
6869
  const raw = await readFile4(path3, "utf-8");
6651
6870
  return JSON.parse(raw);
@@ -6654,7 +6873,7 @@ async function loadState(outputDir) {
6654
6873
  }
6655
6874
  }
6656
6875
  async function saveState(outputDir, state) {
6657
- const path3 = join8(outputDir, STATE_FILE);
6876
+ const path3 = join9(outputDir, STATE_FILE);
6658
6877
  await writeFile3(path3, JSON.stringify(state, null, 2), "utf-8");
6659
6878
  }
6660
6879
  async function markStep(outputDir, state, step, status) {
@@ -6675,11 +6894,11 @@ process.setSourceMapsEnabled(true);
6675
6894
  var PAGES_FILE = "pages.json";
6676
6895
  async function savePages(outputDir, pages) {
6677
6896
  const obj = Object.fromEntries(pages);
6678
- await writeFile13(join29(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
6897
+ await writeFile13(join30(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
6679
6898
  }
6680
6899
  async function loadPages(outputDir) {
6681
6900
  try {
6682
- const raw = await readFile22(join29(outputDir, PAGES_FILE), "utf-8");
6901
+ const raw = await readFile22(join30(outputDir, PAGES_FILE), "utf-8");
6683
6902
  const obj = JSON.parse(raw);
6684
6903
  return new Map(Object.entries(obj));
6685
6904
  } catch {
@@ -6833,6 +7052,7 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
6833
7052
  p9.log.success(`Completed: ${label}`);
6834
7053
  }
6835
7054
  } catch (err) {
7055
+ if (isUserCancellation(err)) throw err;
6836
7056
  state = await markStep(outputDir, state, step, "failed");
6837
7057
  const known = describeKnownError(err);
6838
7058
  if (known) {
@@ -7111,7 +7331,7 @@ Continue?`
7111
7331
  }
7112
7332
  }
7113
7333
  } catch (err) {
7114
- if (err instanceof Error && (err.message.includes("cancelled") || err.message.includes("Cancelled"))) {
7334
+ if (isUserCancellation(err)) {
7115
7335
  p9.log.warn("Your progress is saved. Run again with --resume to continue from where you left off.");
7116
7336
  return;
7117
7337
  }
@@ -7137,6 +7357,10 @@ Continue?`
7137
7357
  p9.outro("Done");
7138
7358
  }
7139
7359
  main().then(() => flushAnalytics()).catch(async (err) => {
7360
+ if (isUserCancellation(err)) {
7361
+ await flushAnalytics();
7362
+ process.exit(0);
7363
+ }
7140
7364
  const known = describeKnownError(err);
7141
7365
  if (known) {
7142
7366
  console.error(`\x1B[31m${known.title}\x1B[0m`);