@autonoma-ai/planner 0.1.19 → 0.1.20

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
@@ -199,7 +199,7 @@ function trackError(error, properties = {}, handled = true) {
199
199
  }
200
200
  async function flushAnalytics(timeoutMs = 1500) {
201
201
  if (pending.size === 0) return;
202
- await Promise.race([Promise.allSettled([...pending]), new Promise((resolve5) => setTimeout(resolve5, timeoutMs))]);
202
+ await Promise.race([Promise.allSettled([...pending]), new Promise((resolve6) => setTimeout(resolve6, timeoutMs))]);
203
203
  }
204
204
  var AUTONOMA_HOME2, DEVICE_ID_PATH, POSTHOG_PUBLIC_KEY, DEFAULT_HOST, RUN_ID, cachedDeviceId, enabled, pending;
205
205
  var init_analytics = __esm({
@@ -288,7 +288,7 @@ var init_context = __esm({
288
288
  // src/core/errors.ts
289
289
  import { APICallError, RetryError, LoadAPIKeyError, InvalidPromptError, NoSuchModelError } from "ai";
290
290
  function sleep(ms) {
291
- return new Promise((resolve5) => setTimeout(resolve5, ms));
291
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
292
292
  }
293
293
  function isUserCancellation(err) {
294
294
  return err instanceof Error && /\bcancell?ed\b/i.test(err.message);
@@ -492,6 +492,151 @@ var init_notify = __esm({
492
492
  }
493
493
  });
494
494
 
495
+ // src/core/project-map.ts
496
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
497
+ import { join as join8 } from "path";
498
+ import { z as z2 } from "zod";
499
+ async function saveProjectMap(outputDir, map) {
500
+ await writeFile3(join8(outputDir, PROJECT_MAP_FILE), JSON.stringify(map, null, 2), "utf-8");
501
+ }
502
+ async function loadProjectMap(outputDir) {
503
+ const path3 = join8(outputDir, PROJECT_MAP_FILE);
504
+ try {
505
+ const raw = await readFile3(path3, "utf-8");
506
+ const parsed = ProjectMapSchema.safeParse(JSON.parse(raw));
507
+ if (parsed.success) return parsed.data;
508
+ debugLog("project-map.json failed schema validation, ignoring it", { path: path3, issues: parsed.error.issues });
509
+ return void 0;
510
+ } catch (err) {
511
+ const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
512
+ if (!isMissingFile) debugLog("Failed to read project-map.json, ignoring it", { path: path3, err });
513
+ return void 0;
514
+ }
515
+ }
516
+ function renderProjectMap(map) {
517
+ const section = (title, rows, fmt) => {
518
+ if (rows.length === 0) return `${title}: (none)`;
519
+ return `${title}:
520
+ ` + rows.map((r) => ` - ${fmt(r)}`).join("\n");
521
+ };
522
+ return [
523
+ section("Frontend(s)", map.frontends, (f) => {
524
+ const deps = f.dependsOn.length > 0 ? ` needs: ${f.dependsOn.join(", ")}` : "";
525
+ return `${f.path} [${f.framework}]${deps} - ${f.why}`;
526
+ }),
527
+ section("Backend(s)", map.backends, (b) => {
528
+ const dl = b.dataLayer != null ? ` data: ${b.dataLayer.kind}${b.dataLayer.schemaPath != null ? ` @ ${b.dataLayer.schemaPath}` : ""}` : "";
529
+ return `${b.path} [${b.language}/${b.framework}]${dl} - ${b.why}`;
530
+ }),
531
+ section("Ignoring", map.ignore, (i) => `${i.path} - ${i.why}`)
532
+ ].join("\n\n");
533
+ }
534
+ function formatFrontendScope(map) {
535
+ if (map.frontends.length === 0) return void 0;
536
+ const fronts = map.frontends.map((f) => f.path).join(", ");
537
+ const ignore = map.ignore.map((i) => i.path);
538
+ const ignoreLine = ignore.length > 0 ? ` Do NOT spend time in these irrelevant directories: ${ignore.join(", ")}.` : "";
539
+ return `The project has already been mapped. The frontend surface to focus on is: ${fronts}. Confine your exploration to that surface.${ignoreLine}`;
540
+ }
541
+ function relatedPath(a, b) {
542
+ return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
543
+ }
544
+ function matchMapPath(requested, candidates) {
545
+ if (candidates.includes(requested)) return requested;
546
+ return candidates.find((c) => relatedPath(c, requested));
547
+ }
548
+ function matchBackend(requested, backends) {
549
+ const byPath = backends.find((b) => relatedPath(b.path, requested));
550
+ if (byPath != null) return byPath.path;
551
+ const bySchema = backends.find(
552
+ (b) => b.dataLayer?.schemaPath != null && relatedPath(b.dataLayer.schemaPath, requested)
553
+ );
554
+ return bySchema?.path;
555
+ }
556
+ function resolveSelection(map, requested) {
557
+ const frontendPaths = map.frontends.map((f) => f.path);
558
+ return {
559
+ frontend: matchMapPath(requested.frontend, frontendPaths) ?? requested.frontend,
560
+ backends: requested.backends.map((b) => matchBackend(b, map.backends) ?? b)
561
+ };
562
+ }
563
+ function defaultBackendsFor(map, frontendPath) {
564
+ const declared = map.frontends.find((f) => f.path === frontendPath)?.dependsOn ?? [];
565
+ const resolved = declared.map((dep) => matchBackend(dep, map.backends)).filter((p10) => p10 != null);
566
+ return [...new Set(resolved)];
567
+ }
568
+ function pickDefaultSelection(map) {
569
+ if (map.frontends.length !== 1) return void 0;
570
+ const [only] = map.frontends;
571
+ if (only == null) return void 0;
572
+ return { frontend: only.path, backends: defaultBackendsFor(map, only.path) };
573
+ }
574
+ function applySelection(map, selection) {
575
+ const frontend = map.frontends.find((f) => f.path === selection.frontend);
576
+ if (frontend == null) {
577
+ const options = map.frontends.map((f) => f.path).join(", ") || "(none)";
578
+ throw new Error(`Selected frontend "${selection.frontend}" is not in the map. Candidates: ${options}`);
579
+ }
580
+ const chosenBackends = map.backends.filter((b) => selection.backends.includes(b.path));
581
+ const missing = selection.backends.filter((p10) => !map.backends.some((b) => b.path === p10));
582
+ if (missing.length > 0) {
583
+ const options = map.backends.map((b) => b.path).join(", ") || "(none)";
584
+ throw new Error(`Selected backend(s) not in the map: ${missing.join(", ")}. Candidates: ${options}`);
585
+ }
586
+ const droppedFrontends = map.frontends.filter((f) => f.path !== selection.frontend).map((f) => ({ path: f.path, why: "Another frontend in the monorepo; not the app under test." }));
587
+ const droppedBackends = map.backends.filter((b) => !selection.backends.includes(b.path)).map((b) => ({ path: b.path, why: "Backend not required by the selected frontend." }));
588
+ return {
589
+ frontends: [frontend],
590
+ backends: chosenBackends,
591
+ ignore: [...map.ignore, ...droppedFrontends, ...droppedBackends]
592
+ };
593
+ }
594
+ function formatBackendScope(map) {
595
+ if (map.backends.length === 0) return void 0;
596
+ const backs = map.backends.map((b) => b.dataLayer?.schemaPath != null ? `${b.path} (models @ ${b.dataLayer.schemaPath})` : b.path).join(", ");
597
+ const ignore = map.ignore.map((i) => i.path);
598
+ const ignoreLine = ignore.length > 0 ? ` Ignore these irrelevant directories: ${ignore.join(", ")}.` : "";
599
+ return `The project has already been mapped. The backend(s)/data layer(s) that own the models to seed are: ${backs}. Scope your data-model work to those backend(s).${ignoreLine}`;
600
+ }
601
+ var FrontendEntry, BackendEntry, IgnoreEntry, ProjectMapSchema, PROJECT_MAP_FILE;
602
+ var init_project_map = __esm({
603
+ "src/core/project-map.ts"() {
604
+ "use strict";
605
+ init_esm_shims();
606
+ init_debug();
607
+ FrontendEntry = z2.object({
608
+ path: z2.string().min(1).describe("Repo-relative path to the frontend app/directory (the UI surface)."),
609
+ framework: z2.string().describe("Detected UI framework/stack, or 'unknown' if not obvious (e.g. next, react, vue, svelte)."),
610
+ dependsOn: z2.array(z2.string()).describe(
611
+ "Repo-relative paths (each matching a backend in this map) that THIS frontend needs in order to function - the API/service(s) it calls and the data layer(s) that own the records it renders. These are pre-selected when the user picks this frontend, so only list backends this frontend actually depends on. Empty if it needs none."
612
+ ),
613
+ why: z2.string().min(1).describe("One line: the evidence that made you classify this as a frontend.")
614
+ });
615
+ BackendEntry = z2.object({
616
+ path: z2.string().min(1).describe("Repo-relative path to the backend/API/service or the package that owns the data layer."),
617
+ language: z2.string().describe("Primary language, or 'unknown' (e.g. typescript, python, go, rust)."),
618
+ framework: z2.string().describe("Web/service framework or ORM stack, or 'unknown' (e.g. express, hono, fastapi, rails)."),
619
+ dataLayer: z2.object({
620
+ kind: z2.string().describe("How models are defined (e.g. prisma, drizzle, sqlalchemy, typeorm, raw-sql, unknown)."),
621
+ schemaPath: z2.string().optional().describe("Repo-relative path to the schema/models definition, if found.")
622
+ }).optional().describe("Where this backend's database models live. Omit if this backend owns no data layer."),
623
+ why: z2.string().min(1).describe("One line: the evidence that made you classify this as a backend.")
624
+ });
625
+ IgnoreEntry = z2.object({
626
+ path: z2.string().min(1).describe("Repo-relative path to a directory that is NOT relevant to testing this app."),
627
+ why: z2.string().min(1).describe("One line: why it is irrelevant (e.g. docs site, infra, tooling, examples, unrelated app).")
628
+ });
629
+ ProjectMapSchema = z2.object({
630
+ frontends: z2.array(FrontendEntry).describe("The UI surface(s) whose pages/flows get tested. Usually 1; may be more."),
631
+ backends: z2.array(BackendEntry).describe(
632
+ "The API/service(s) and data layer(s) that own the models we seed. May be 0 (frontend-only), 1, or many."
633
+ ),
634
+ ignore: z2.array(IgnoreEntry).describe("Directories judged irrelevant to this app's tests, so later steps skip them.")
635
+ });
636
+ PROJECT_MAP_FILE = "project-map.json";
637
+ }
638
+ });
639
+
495
640
  // src/core/display.ts
496
641
  function formatArgs(input, keys) {
497
642
  const parts = [];
@@ -766,8 +911,8 @@ var init_agent = __esm({
766
911
  });
767
912
 
768
913
  // src/core/gitignore.ts
769
- import { readFile as readFile5 } from "fs/promises";
770
- import { join as join10, relative as relative2 } from "path";
914
+ import { readFile as readFile6 } from "fs/promises";
915
+ import { join as join11, relative as relative2 } from "path";
771
916
  import { glob as glob2 } from "glob";
772
917
  async function loadGitignorePatterns(projectRoot) {
773
918
  const patterns = [
@@ -787,10 +932,10 @@ async function loadGitignorePatterns(projectRoot) {
787
932
  ];
788
933
  const matches = await glob2("**/.gitignore", { cwd: projectRoot, dot: true });
789
934
  for (const match of matches) {
790
- const fullPath = join10(projectRoot, match);
935
+ const fullPath = join11(projectRoot, match);
791
936
  try {
792
- const content = await readFile5(fullPath, "utf-8");
793
- const prefix = relative2(projectRoot, join10(projectRoot, match, ".."));
937
+ const content = await readFile6(fullPath, "utf-8");
938
+ const prefix = relative2(projectRoot, join11(projectRoot, match, ".."));
794
939
  const parsed = parseGitignore(content, prefix);
795
940
  patterns.push(...parsed);
796
941
  } catch (err) {
@@ -853,7 +998,7 @@ var init_exec_error = __esm({
853
998
  import { execFile as execFile3 } from "child_process";
854
999
  import { promisify as promisify2 } from "util";
855
1000
  import { tool } from "ai";
856
- import { z as z2 } from "zod";
1001
+ import { z as z4 } from "zod";
857
1002
  function validateCommand(command, allowed) {
858
1003
  const trimmed = command.trim();
859
1004
  if (trimmed.length === 0) return "Empty command";
@@ -919,8 +1064,8 @@ var init_bash = __esm({
919
1064
  DEFAULT_ALLOWED = /* @__PURE__ */ new Set(["git", "wc", "sort", "head", "tail", "cat", "ls", "find", "diff", "echo"]);
920
1065
  TIMEOUT_MS = 3e4;
921
1066
  MAX_OUTPUT_BYTES = 1024 * 512;
922
- inputSchema = z2.object({
923
- command: z2.string().describe("Shell command to execute")
1067
+ inputSchema = z4.object({
1068
+ command: z4.string().describe("Shell command to execute")
924
1069
  });
925
1070
  }
926
1071
  });
@@ -928,7 +1073,7 @@ var init_bash = __esm({
928
1073
  // src/tools/glob.ts
929
1074
  import { tool as tool2 } from "ai";
930
1075
  import { glob as glob3 } from "glob";
931
- import { z as z3 } from "zod";
1076
+ import { z as z5 } from "zod";
932
1077
  async function executeGlob(pattern, cwd, ignorePatterns = DEFAULT_IGNORE) {
933
1078
  try {
934
1079
  const matches = await glob3(pattern, {
@@ -955,9 +1100,9 @@ var init_glob = __esm({
955
1100
  "src/tools/glob.ts"() {
956
1101
  "use strict";
957
1102
  init_esm_shims();
958
- inputSchema2 = z3.object({
959
- pattern: z3.string().describe("Glob pattern to match files (e.g. '**/*.ts', 'src/**/*.py')"),
960
- cwd: z3.string().optional().describe("Directory to search in. Defaults to working directory.")
1103
+ inputSchema2 = z5.object({
1104
+ pattern: z5.string().describe("Glob pattern to match files (e.g. '**/*.ts', 'src/**/*.py')"),
1105
+ cwd: z5.string().optional().describe("Directory to search in. Defaults to working directory.")
961
1106
  });
962
1107
  DEFAULT_IGNORE = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
963
1108
  }
@@ -967,7 +1112,7 @@ var init_glob = __esm({
967
1112
  import { execFile as execFile4 } from "child_process";
968
1113
  import { promisify as promisify3 } from "util";
969
1114
  import { tool as tool3 } from "ai";
970
- import { z as z4 } from "zod";
1115
+ import { z as z6 } from "zod";
971
1116
  function buildGrepTool(workingDirectory) {
972
1117
  return tool3({
973
1118
  description: "Search file contents with ripgrep. Returns matching lines with file paths and line numbers.",
@@ -1011,10 +1156,10 @@ var init_grep = __esm({
1011
1156
  init_esm_shims();
1012
1157
  init_exec_error();
1013
1158
  execFileAsync3 = promisify3(execFile4);
1014
- inputSchema3 = z4.object({
1015
- pattern: z4.string().describe("Regex pattern to search for in file contents"),
1016
- glob: z4.string().optional().describe("Glob to filter files (e.g. '*.ts')"),
1017
- path: z4.string().optional().describe("File or directory to search in")
1159
+ inputSchema3 = z6.object({
1160
+ pattern: z6.string().describe("Regex pattern to search for in file contents"),
1161
+ glob: z6.string().optional().describe("Glob to filter files (e.g. '*.ts')"),
1162
+ path: z6.string().optional().describe("File or directory to search in")
1018
1163
  });
1019
1164
  }
1020
1165
  });
@@ -1022,10 +1167,10 @@ var init_grep = __esm({
1022
1167
  // src/tools/list-directory.ts
1023
1168
  import { readdir } from "fs/promises";
1024
1169
  import { stat } from "fs/promises";
1025
- import { join as join11, relative as relative3 } from "path";
1170
+ import { join as join12, relative as relative3 } from "path";
1026
1171
  import { tool as tool4 } from "ai";
1027
1172
  import { minimatch } from "minimatch";
1028
- import { z as z5 } from "zod";
1173
+ import { z as z7 } from "zod";
1029
1174
  function buildMatcher(patterns) {
1030
1175
  const positive = patterns.filter((p10) => !p10.startsWith("!"));
1031
1176
  const negative = patterns.filter((p10) => p10.startsWith("!")).map((p10) => p10.slice(1));
@@ -1047,7 +1192,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
1047
1192
  const withTypes = [];
1048
1193
  for (const name of rawEntries) {
1049
1194
  try {
1050
- const s = await stat(join11(dirPath, name));
1195
+ const s = await stat(join12(dirPath, name));
1051
1196
  withTypes.push({ name, isDir: s.isDirectory() });
1052
1197
  } catch {
1053
1198
  withTypes.push({ name, isDir: false });
@@ -1067,7 +1212,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
1067
1212
  }
1068
1213
  if (entry.isDir) {
1069
1214
  const children = await buildTree(
1070
- join11(dirPath, entry.name),
1215
+ join12(dirPath, entry.name),
1071
1216
  maxDepth,
1072
1217
  currentDepth + 1,
1073
1218
  isIgnored,
@@ -1105,10 +1250,10 @@ async function buildListDirectoryTool(workingDirectory) {
1105
1250
  const isIgnored = buildMatcher(patterns);
1106
1251
  return tool4({
1107
1252
  description: "List directory structure as a tree. Use this for an overview of the project layout. Start at the root (path='.') with depth 3, then increase depth or narrow path if needed. Do NOT call this on every subdirectory - use glob to find specific files instead. Returns cached result if the same path+depth was already requested.",
1108
- inputSchema: z5.object({
1109
- path: z5.string().default(".").describe("Directory path relative to project root. Defaults to root."),
1110
- depth: z5.number().min(1).max(15).default(10).describe("Max depth to traverse (1-15). Default 10."),
1111
- gitignore: z5.boolean().describe(
1253
+ inputSchema: z7.object({
1254
+ path: z7.string().default(".").describe("Directory path relative to project root. Defaults to root."),
1255
+ depth: z7.number().min(1).max(15).default(10).describe("Max depth to traverse (1-15). Default 10."),
1256
+ gitignore: z7.boolean().describe(
1112
1257
  "Whether to respect the gitignore or to ignore it. true will respect it. false will ignore it. Default true"
1113
1258
  ).default(true)
1114
1259
  }),
@@ -1120,7 +1265,7 @@ async function buildListDirectoryTool(workingDirectory) {
1120
1265
  };
1121
1266
  }
1122
1267
  seen.add(cacheKey);
1123
- const targetDir = input.path === "." ? workingDirectory : join11(workingDirectory, input.path);
1268
+ const targetDir = input.path === "." ? workingDirectory : join12(workingDirectory, input.path);
1124
1269
  try {
1125
1270
  const s = await stat(targetDir);
1126
1271
  if (!s.isDirectory()) {
@@ -1152,10 +1297,10 @@ var init_list_directory = __esm({
1152
1297
  });
1153
1298
 
1154
1299
  // src/tools/read-file.ts
1155
- import { readFile as readFile6 } from "fs/promises";
1300
+ import { readFile as readFile7 } from "fs/promises";
1156
1301
  import { relative as relative4, resolve as resolve2 } from "path";
1157
1302
  import { tool as tool5 } from "ai";
1158
- import { z as z6 } from "zod";
1303
+ import { z as z8 } from "zod";
1159
1304
  function resolveSandboxedPath(workingDirectory, filePath) {
1160
1305
  const absolutePath = resolve2(workingDirectory, filePath);
1161
1306
  const relativePath = relative4(workingDirectory, absolutePath);
@@ -1180,7 +1325,7 @@ async function executeReadFile(workingDirectory, filePath, offset, limit) {
1180
1325
  const resolved = resolveSandboxedPath(workingDirectory, filePath);
1181
1326
  if ("error" in resolved) return resolved;
1182
1327
  try {
1183
- const content = await readFile6(resolved.absolutePath, "utf-8");
1328
+ const content = await readFile7(resolved.absolutePath, "utf-8");
1184
1329
  const sliced = sliceLines(content, offset ?? 0, limit ?? MAX_LINES);
1185
1330
  return {
1186
1331
  path: resolved.relativePath,
@@ -1208,10 +1353,10 @@ var init_read_file = __esm({
1208
1353
  "use strict";
1209
1354
  init_esm_shims();
1210
1355
  MAX_LINES = 2e3;
1211
- inputSchema4 = z6.object({
1212
- filePath: z6.string().describe("Path to the file (absolute or relative to working directory)"),
1213
- offset: z6.number().int().min(0).optional().describe("Line number to start reading from (0-based)"),
1214
- limit: z6.number().int().min(1).optional().describe("Maximum number of lines to read")
1356
+ inputSchema4 = z8.object({
1357
+ filePath: z8.string().describe("Path to the file (absolute or relative to working directory)"),
1358
+ offset: z8.number().int().min(0).optional().describe("Line number to start reading from (0-based)"),
1359
+ limit: z8.number().int().min(1).optional().describe("Maximum number of lines to read")
1215
1360
  });
1216
1361
  }
1217
1362
  });
@@ -1234,10 +1379,10 @@ var init_pick_string = __esm({
1234
1379
 
1235
1380
  // src/tools/subagent.ts
1236
1381
  import { ToolLoopAgent as ToolLoopAgent2, hasToolCall as hasToolCall2, stepCountIs as stepCountIs2, tool as tool6 } from "ai";
1237
- import { z as z7 } from "zod";
1382
+ import { z as z9 } from "zod";
1238
1383
  function buildSubagentTools(workingDirectory, onFileRead) {
1239
1384
  const baseReadFile = buildReadFileTool(workingDirectory);
1240
- const readFile23 = onFileRead ? tool6({
1385
+ const readFile24 = onFileRead ? tool6({
1241
1386
  description: baseReadFile.description,
1242
1387
  inputSchema: baseReadFile.inputSchema,
1243
1388
  execute: async (input, options) => {
@@ -1250,7 +1395,7 @@ function buildSubagentTools(workingDirectory, onFileRead) {
1250
1395
  bash: buildBashTool(workingDirectory),
1251
1396
  glob: buildGlobTool(workingDirectory),
1252
1397
  grep: buildGrepTool(workingDirectory),
1253
- read_file: readFile23
1398
+ read_file: readFile24
1254
1399
  };
1255
1400
  }
1256
1401
  function buildSubagentTool(model, workingDirectory, onHeartbeat, onFileRead) {
@@ -1301,11 +1446,11 @@ var init_subagent = __esm({
1301
1446
  init_glob();
1302
1447
  init_grep();
1303
1448
  init_read_file();
1304
- inputSchema5 = z7.object({
1305
- instruction: z7.string().describe("Focused task for the subagent. Be specific about files and patterns to investigate.")
1449
+ inputSchema5 = z9.object({
1450
+ instruction: z9.string().describe("Focused task for the subagent. Be specific about files and patterns to investigate.")
1306
1451
  });
1307
- resultSchema = z7.object({
1308
- findings: z7.string().describe("Summary of what was found")
1452
+ resultSchema = z9.object({
1453
+ findings: z9.string().describe("Summary of what was found")
1309
1454
  });
1310
1455
  SYSTEM_PROMPT = `You are a code research assistant. You have tools to explore a codebase: bash (shell commands, mainly git), glob (find files), grep (search content), and read_file (read files).
1311
1456
 
@@ -1316,10 +1461,10 @@ Be thorough but focused - only investigate what's relevant to your instruction.`
1316
1461
  });
1317
1462
 
1318
1463
  // src/tools/write-file.ts
1319
- import { writeFile as writeFile4, mkdir as mkdir2 } from "fs/promises";
1464
+ import { writeFile as writeFile5, mkdir as mkdir2 } from "fs/promises";
1320
1465
  import { dirname as dirname2, relative as relative5, resolve as resolve3 } from "path";
1321
1466
  import { tool as tool7 } from "ai";
1322
- import { z as z8 } from "zod";
1467
+ import { z as z10 } from "zod";
1323
1468
  async function executeWriteFile(outputDirectory, filePath, content) {
1324
1469
  const cleaned = filePath.replace(/^autonoma\//, "");
1325
1470
  const absolutePath = resolve3(outputDirectory, cleaned);
@@ -1329,7 +1474,7 @@ async function executeWriteFile(outputDirectory, filePath, content) {
1329
1474
  }
1330
1475
  try {
1331
1476
  await mkdir2(dirname2(absolutePath), { recursive: true });
1332
- await writeFile4(absolutePath, content, "utf-8");
1477
+ await writeFile5(absolutePath, content, "utf-8");
1333
1478
  return { path: relativePath, bytesWritten: content.length };
1334
1479
  } catch (err) {
1335
1480
  const message = err instanceof Error ? err.message : String(err);
@@ -1348,9 +1493,9 @@ var init_write_file = __esm({
1348
1493
  "src/tools/write-file.ts"() {
1349
1494
  "use strict";
1350
1495
  init_esm_shims();
1351
- inputSchema6 = z8.object({
1352
- filePath: z8.string().describe("Path to write (absolute or relative to output directory)"),
1353
- content: z8.string().describe("File content to write")
1496
+ inputSchema6 = z10.object({
1497
+ filePath: z10.string().describe("Path to write (absolute or relative to output directory)"),
1498
+ content: z10.string().describe("File content to write")
1354
1499
  });
1355
1500
  }
1356
1501
  });
@@ -1358,16 +1503,21 @@ var init_write_file = __esm({
1358
1503
  // src/tools/ask-user.ts
1359
1504
  import * as p2 from "@clack/prompts";
1360
1505
  import { tool as tool8 } from "ai";
1361
- import { z as z9 } from "zod";
1506
+ import { z as z11 } from "zod";
1362
1507
  function buildAskUserTool() {
1363
1508
  return tool8({
1364
1509
  description: "Ask the user a question ONLY when the answer is truly unknowable from the codebase. Valid reasons: untyped JSON/JSONB field schemas, business rules not in code, config values not in source. NEVER ask about: field names (read the schema), field types (read the ORM model), enum values (read the code), relationships (read foreign keys), numeric values (read the seed data or defaults). If you can find it by reading a file, DO NOT ask - read the file instead.",
1365
- inputSchema: z9.object({
1366
- question: z9.string().describe(
1510
+ inputSchema: z11.object({
1511
+ question: z11.string().describe(
1367
1512
  "A clear, plain-language question. State exactly what you need to know and why you can't find it in code. BAD: 'What are the decimal values for checking_balance?' - GOOD: 'Your Account model has a metadata JSON column with no type definition. What fields go inside it?'"
1368
1513
  )
1369
1514
  }),
1370
1515
  execute: async (input) => {
1516
+ if (!process.stdin.isTTY) {
1517
+ return {
1518
+ answer: "No interactive user is available (non-interactive run). Do not ask again - infer the answer by reading the relevant model/schema/service files in the codebase and proceed with your best judgment."
1519
+ };
1520
+ }
1371
1521
  const answer = await p2.text({ message: input.question });
1372
1522
  if (p2.isCancel(answer)) return { answer: "User skipped this question" };
1373
1523
  return { answer };
@@ -1411,6 +1561,58 @@ var init_tools = __esm({
1411
1561
  }
1412
1562
  });
1413
1563
 
1564
+ // src/agents/00-project-mapper/index.ts
1565
+ var project_mapper_exports = {};
1566
+ __export(project_mapper_exports, {
1567
+ runProjectMapper: () => runProjectMapper
1568
+ });
1569
+ import { tool as tool9 } from "ai";
1570
+ async function runProjectMapper(input) {
1571
+ const model = getModel(input.modelId);
1572
+ const { logger, onStepFinish } = buildDefaultStepLogger("project-map", MAX_STEPS);
1573
+ let captured;
1574
+ const prompt = `Map the codebase rooted at ${input.projectRoot}.
1575
+
1576
+ Explore the layout and dependency manifests, then enumerate EVERY candidate frontend and EVERY candidate backend/data-layer, and for each frontend record the backends it depends on. List everything genuinely irrelevant under ignore. Do not prune to a single app - a human picks the one to test afterward. Then call set_project_map and finish.`;
1577
+ const agentConfig = {
1578
+ id: "project-mapper",
1579
+ systemPrompt: SYSTEM_PROMPT2,
1580
+ model,
1581
+ maxSteps: MAX_STEPS,
1582
+ tools: async (heartbeat) => {
1583
+ const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
1584
+ return {
1585
+ ...tools,
1586
+ set_project_map: tool9({
1587
+ description: "Record the final project map: the frontend app(s), the backend(s)/data layer(s), and the directories to ignore. Call this once you are confident in the partition, then call finish.",
1588
+ inputSchema: ProjectMapSchema,
1589
+ execute: (map) => {
1590
+ captured = map;
1591
+ return `Recorded: ${map.frontends.length} frontend(s), ${map.backends.length} backend(s), ${map.ignore.length} ignored. Now call finish.`;
1592
+ }
1593
+ })
1594
+ };
1595
+ },
1596
+ onStepFinish
1597
+ };
1598
+ await runAgent(agentConfig, prompt, () => captured);
1599
+ logger.summary();
1600
+ return captured;
1601
+ }
1602
+ var MAX_STEPS, SYSTEM_PROMPT2;
1603
+ var init_project_mapper = __esm({
1604
+ "src/agents/00-project-mapper/index.ts"() {
1605
+ "use strict";
1606
+ init_esm_shims();
1607
+ init_agent();
1608
+ init_model();
1609
+ init_project_map();
1610
+ init_tools();
1611
+ MAX_STEPS = 60;
1612
+ SYSTEM_PROMPT2 = "You map a codebase into three groups so the rest of the test-planning pipeline knows exactly what to look at and what to skip: FRONTENDS (the UI surfaces whose pages and flows could be tested), BACKENDS (the API/service and the data layer that owns the database models we would seed test data into), and IGNORE (everything irrelevant to testing this product).\n\nYou are a DISCOVERY step, not a decision step. The pipeline tests ONE frontend at a time, but YOU do not choose which - a human (or the agent driving you) picks afterward from what you found. So enumerate EVERY candidate frontend and EVERY candidate backend you can justify; do not prune down to a single app. Pruning happens at selection, using the dependency edges you record.\n\nDiscover the structure - never assume it. Read package/dependency manifests, config files, and the directory layout, then reason from evidence:\n- A FRONTEND renders a user interface (pages/routes/views, a UI framework or bundler, browser entry points).\n- A BACKEND serves an API and/or owns the data layer (a database schema, ORM models, migrations, server routes).\n- IGNORE is the rest: infra, build tooling, examples, generated code, and packages that are neither a UI nor a service/data-layer.\n\nFor EACH frontend, record `dependsOn`: the repo-relative paths (each must be one of the backends you list) that that frontend actually needs in order to work - the API/service(s) it calls and the data layer(s) that own the records it renders. Infer these from evidence: the frontend's dependency manifest, the API clients/SDKs it imports, GraphQL/REST endpoints or gateway URLs it points at, and shared data-layer packages it reads. When the user selects that frontend, its `dependsOn` backends are pre-checked, so be accurate: list the backends it truly needs, not every backend in the repo.\n\nHandle every shape without special-casing any framework:\n- A single fullstack app can be BOTH a frontend and a backend at the SAME path - list that path under both, and put its own path in its `dependsOn`.\n- A monorepo can hold many apps and packages - list every genuine frontend and backend; only truly irrelevant directories go in IGNORE.\n- There may be MANY backends (a main API, separate services, a shared data-layer package, a gateway plus the services behind it) - list each one, and wire each frontend's `dependsOn` to just the ones it uses.\n- The data layer a frontend uses may live in a sibling/shared package OUTSIDE the frontend's own folder - record that package as a backend, point its dataLayer.schemaPath at the schema, and include it in the frontend's `dependsOn`.\n- A codebase may ship only one half. If you find frontends but NO backend/data layer (or vice versa), still record what you found and leave the other group empty - the caller will ask the user to supply the missing half.\n\nBe thorough and evidence-based. When you have enumerated the candidates and their dependency edges, call set_project_map, then call finish. Keep each `why` to a single concrete sentence citing what you saw.";
1613
+ }
1614
+ });
1615
+
1414
1616
  // src/agents/00-pages-finder/index.ts
1415
1617
  var pages_finder_exports = {};
1416
1618
  __export(pages_finder_exports, {
@@ -1418,8 +1620,8 @@ __export(pages_finder_exports, {
1418
1620
  });
1419
1621
  import { existsSync } from "fs";
1420
1622
  import * as path2 from "path";
1421
- import { tool as tool9 } from "ai";
1422
- import { z as z10 } from "zod";
1623
+ import { tool as tool10 } from "ai";
1624
+ import { z as z12 } from "zod";
1423
1625
  async function runPageFinder(input) {
1424
1626
  const model = getModel(input.modelId);
1425
1627
  const pageCollector = new PageCollector();
@@ -1438,7 +1640,7 @@ ${input.extraMessage}`;
1438
1640
  const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
1439
1641
  return {
1440
1642
  ...tools,
1441
- add_page: tool9({
1643
+ add_page: tool10({
1442
1644
  description: "use this tool to add a page that you found",
1443
1645
  inputSchema: Page,
1444
1646
  execute: (input2) => {
@@ -1450,9 +1652,9 @@ ${input.extraMessage}`;
1450
1652
  return `page ${JSON.stringify(input2)} added`;
1451
1653
  }
1452
1654
  }),
1453
- view_pages: tool9({
1655
+ view_pages: tool10({
1454
1656
  description: "use this tool to view all the pages that you already added",
1455
- inputSchema: z10.object(),
1657
+ inputSchema: z12.object(),
1456
1658
  execute: () => pageCollector.viewPages()
1457
1659
  })
1458
1660
  };
@@ -1471,10 +1673,10 @@ var init_pages_finder = __esm({
1471
1673
  init_agent();
1472
1674
  init_model();
1473
1675
  init_tools();
1474
- Page = z10.object({
1475
- route: z10.string().min(1),
1476
- path: z10.string().min(1),
1477
- description: z10.string().min(10)
1676
+ Page = z12.object({
1677
+ route: z12.string().min(1),
1678
+ path: z12.string().min(1),
1679
+ description: z12.string().min(10)
1478
1680
  });
1479
1681
  PageCollector = class {
1480
1682
  // the key is the path
@@ -1504,13 +1706,13 @@ var init_pages_finder = __esm({
1504
1706
 
1505
1707
  // src/core/review.ts
1506
1708
  import { access } from "fs/promises";
1507
- import { join as join12, isAbsolute } from "path";
1709
+ import { join as join13, isAbsolute } from "path";
1508
1710
  import * as p3 from "@clack/prompts";
1509
1711
  import spawn from "cross-spawn";
1510
1712
  import which from "which";
1511
1713
  function resolvePath(artifact, outputDir) {
1512
1714
  if (isAbsolute(artifact)) return artifact;
1513
- return join12(outputDir, artifact);
1715
+ return join13(outputDir, artifact);
1514
1716
  }
1515
1717
  async function detectEditors() {
1516
1718
  if (cachedEditors) return cachedEditors;
@@ -1525,12 +1727,12 @@ async function detectEditors() {
1525
1727
  async function launchEditor(editor, files) {
1526
1728
  const args = editor.args(files);
1527
1729
  const isTerminalEditor = TERMINAL_EDITORS.has(editor.command);
1528
- await new Promise((resolve5) => {
1730
+ await new Promise((resolve6) => {
1529
1731
  let settled = false;
1530
1732
  const settle = () => {
1531
1733
  if (settled) return;
1532
1734
  settled = true;
1533
- resolve5();
1735
+ resolve6();
1534
1736
  };
1535
1737
  const proc = spawn(editor.command, args, { stdio: "inherit" });
1536
1738
  proc.on("error", (err) => {
@@ -1592,7 +1794,7 @@ async function showResults(result, options) {
1592
1794
  if (result.artifacts.length === 0) {
1593
1795
  const knownFiles = ["AUTONOMA.md", "entity-audit.md", "scenarios.md"];
1594
1796
  for (const f of knownFiles) {
1595
- const fullPath = join12(options.outputDir, f);
1797
+ const fullPath = join13(options.outputDir, f);
1596
1798
  try {
1597
1799
  await access(fullPath);
1598
1800
  result.artifacts.push(f);
@@ -1681,13 +1883,13 @@ var init_review = __esm({
1681
1883
  });
1682
1884
 
1683
1885
  // src/agents/01-kb-generator/flows.ts
1684
- import { readFile as readFile7 } from "fs/promises";
1685
- import { join as join13 } from "path";
1886
+ import { readFile as readFile8 } from "fs/promises";
1887
+ import { join as join14 } from "path";
1686
1888
  import matter from "gray-matter";
1687
1889
  async function parseCoreFlows(outputDir) {
1688
1890
  let raw;
1689
1891
  try {
1690
- raw = await readFile7(join13(outputDir, "AUTONOMA.md"), "utf-8");
1892
+ raw = await readFile8(join14(outputDir, "AUTONOMA.md"), "utf-8");
1691
1893
  } catch {
1692
1894
  return [];
1693
1895
  }
@@ -1752,12 +1954,12 @@ var init_flows = __esm({
1752
1954
  });
1753
1955
 
1754
1956
  // src/agents/01-kb-generator/prompt.ts
1755
- var SYSTEM_PROMPT2;
1957
+ var SYSTEM_PROMPT3;
1756
1958
  var init_prompt = __esm({
1757
1959
  "src/agents/01-kb-generator/prompt.ts"() {
1758
1960
  "use strict";
1759
1961
  init_esm_shims();
1760
- SYSTEM_PROMPT2 = `You are a knowledge base generator for E2E test planning. You analyze a frontend codebase and produce a structured guide to EVERY page, flow, and interaction. You must be EXHAUSTIVE - missing a page means missing test coverage.
1962
+ SYSTEM_PROMPT3 = `You are a knowledge base generator for E2E test planning. You analyze a frontend codebase and produce a structured guide to EVERY page, flow, and interaction. You must be EXHAUSTIVE - missing a page means missing test coverage.
1761
1963
 
1762
1964
  ## Your output
1763
1965
 
@@ -1900,15 +2102,15 @@ var kb_generator_exports = {};
1900
2102
  __export(kb_generator_exports, {
1901
2103
  runKBGenerator: () => runKBGenerator
1902
2104
  });
1903
- import { readFile as readFile8 } from "fs/promises";
1904
- import { join as join14 } from "path";
1905
- import { tool as tool10 } from "ai";
1906
- import { z as z11 } from "zod";
2105
+ import { readFile as readFile9 } from "fs/promises";
2106
+ import { join as join15, resolve as resolve5 } from "path";
2107
+ import { tool as tool11 } from "ai";
2108
+ import { z as z13 } from "zod";
1907
2109
  function buildRegisterPagesTool(tracker) {
1908
- return tool10({
2110
+ return tool11({
1909
2111
  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.",
1910
- inputSchema: z11.object({
1911
- pages: z11.array(z11.string()).describe("All page file paths found by glob")
2112
+ inputSchema: z13.object({
2113
+ pages: z13.array(z13.string()).describe("All page file paths found by glob")
1912
2114
  }),
1913
2115
  execute: async (input) => {
1914
2116
  tracker.register(input.pages);
@@ -1920,25 +2122,33 @@ function buildRegisterPagesTool(tracker) {
1920
2122
  });
1921
2123
  }
1922
2124
  function buildPageCoverageTool(tracker) {
1923
- return tool10({
2125
+ return tool11({
1924
2126
  description: "Check how many registered pages you've read vs how many remain.",
1925
- inputSchema: z11.object({}),
2127
+ inputSchema: z13.object({}),
1926
2128
  execute: async () => tracker.coverage()
1927
2129
  });
1928
2130
  }
2131
+ function requiredReads(total) {
2132
+ if (total <= FULL_COVERAGE_MAX_ROUTES) return total;
2133
+ return Math.ceil(total * LARGE_APP_COVERAGE_FLOOR);
2134
+ }
1929
2135
  function buildFinishTool(tracker, onFinish) {
1930
- return tool10({
1931
- description: "Call when you have finished generating the knowledge base. BLOCKED if there are unread pages - call page_coverage first to check.",
1932
- inputSchema: z11.object({
1933
- summary: z11.string().describe("Summary of what was generated"),
1934
- artifacts: z11.array(z11.string()).describe("List of files written")
2136
+ return tool11({
2137
+ description: "Call when you have finished generating the knowledge base. BLOCKED until you have read enough of the registered routes (every route on a small app; a strong majority on a large one) - call page_coverage first to check how many remain.",
2138
+ inputSchema: z13.object({
2139
+ summary: z13.string().describe("Summary of what was generated"),
2140
+ artifacts: z13.array(z13.string()).describe("List of files written")
1935
2141
  }),
1936
2142
  execute: async (input) => {
1937
2143
  const cov = tracker.coverage();
1938
- if (cov.unread.length > 0) {
2144
+ const required = requiredReads(cov.total);
2145
+ if (cov.read < required) {
2146
+ const preview = cov.unread.slice(0, 40).join("\n");
2147
+ const more = cov.unread.length > 40 ? `
2148
+ ...and ${cov.unread.length - 40} more` : "";
1939
2149
  return {
1940
- error: `Cannot finish: ${cov.unread.length}/${cov.total} pages not yet read. Read these files first:
1941
- ${cov.unread.join("\n")}`
2150
+ error: `Cannot finish: only ${cov.read}/${cov.total} routes read - read at least ${required - cov.read} more (target ${required} of ${cov.total}). Start with:
2151
+ ${preview}${more}`
1942
2152
  };
1943
2153
  }
1944
2154
  onFinish({
@@ -1951,7 +2161,7 @@ ${cov.unread.join("\n")}`
1951
2161
  });
1952
2162
  }
1953
2163
  function buildTrackedReadTool(tracker, baseTool) {
1954
- return tool10({
2164
+ return tool11({
1955
2165
  description: baseTool.description,
1956
2166
  inputSchema: baseTool.inputSchema,
1957
2167
  execute: async (input, options) => {
@@ -1961,13 +2171,36 @@ function buildTrackedReadTool(tracker, baseTool) {
1961
2171
  }
1962
2172
  });
1963
2173
  }
2174
+ function buildKbAgentConfig(tracker, model, input, onStepFinish, setResult) {
2175
+ return {
2176
+ id: "kb-generator",
2177
+ systemPrompt: SYSTEM_PROMPT3,
2178
+ model,
2179
+ maxSteps: 150,
2180
+ tools: async (heartbeat) => {
2181
+ const onFileRead = (path3) => tracker.markRead(path3);
2182
+ const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat, onFileRead);
2183
+ return {
2184
+ ...tools,
2185
+ read_file: buildTrackedReadTool(tracker, tools.read_file),
2186
+ register_pages: buildRegisterPagesTool(tracker),
2187
+ page_coverage: buildPageCoverageTool(tracker),
2188
+ finish: buildFinishTool(tracker, setResult)
2189
+ };
2190
+ },
2191
+ onStepFinish
2192
+ };
2193
+ }
1964
2194
  async function runKBGenerator(input) {
1965
2195
  const model = getModel(input.modelId);
1966
2196
  let result;
1967
- const tracker = new PageTracker();
2197
+ const setResult = (r) => {
2198
+ result = r;
2199
+ };
1968
2200
  const { logger, onStepFinish } = buildDefaultStepLogger("kb", 150);
1969
2201
  const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + formatRetryGuidance(input.retryGuidance);
1970
2202
  const pages = input.projectContext?.pages;
2203
+ const tracker = new PageTracker(input.projectRoot);
1971
2204
  if (pages?.length) {
1972
2205
  tracker.register(pages.map((p10) => p10.path));
1973
2206
  }
@@ -1979,7 +2212,7 @@ Pages have already been discovered (${pages.length} routes pre-registered). You
1979
2212
  2. Read EVERY registered page file with read_file - the system tracks this
1980
2213
  3. Write AUTONOMA.md progressively as you go (update it after each major area)
1981
2214
  4. Call page_coverage to verify you've read all pages
1982
- 5. Call finish - it will REJECT if pages are unread
2215
+ 5. Call finish - it will REJECT if you have not read enough of the registered routes
1983
2216
 
1984
2217
  Output files:
1985
2218
  1. AUTONOMA.md - with YAML frontmatter (app_name, app_description, core_flows, feature_count)` : `Analyze the codebase at the working directory and generate a complete knowledge base.
@@ -1991,34 +2224,15 @@ MANDATORY PROCESS:
1991
2224
  4. Read EVERY registered page file with read_file - the system tracks this
1992
2225
  5. Write AUTONOMA.md progressively as you go (update it after each major area)
1993
2226
  6. Call page_coverage to verify you've read all pages
1994
- 7. Call finish - it will REJECT if pages are unread
2227
+ 7. Call finish - it will REJECT if you have not read enough of the registered routes
1995
2228
 
1996
2229
  Output files:
1997
2230
  1. AUTONOMA.md - with YAML frontmatter (app_name, app_description, core_flows, feature_count)`;
1998
- const agentConfig = {
1999
- id: "kb-generator",
2000
- systemPrompt: SYSTEM_PROMPT2,
2001
- model,
2002
- maxSteps: 150,
2003
- tools: async (heartbeat) => {
2004
- const onFileRead = (path3) => tracker.markRead(path3);
2005
- const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat, onFileRead);
2006
- return {
2007
- ...tools,
2008
- read_file: buildTrackedReadTool(tracker, tools.read_file),
2009
- register_pages: buildRegisterPagesTool(tracker),
2010
- page_coverage: buildPageCoverageTool(tracker),
2011
- finish: buildFinishTool(tracker, (r) => {
2012
- result = r;
2013
- })
2014
- };
2015
- },
2016
- onStepFinish
2017
- };
2231
+ const agentConfig = buildKbAgentConfig(tracker, model, input, onStepFinish, setResult);
2018
2232
  await runAgent(agentConfig, prompt, () => result);
2019
2233
  logger.summary();
2020
- const autonomaPath = join14(input.outputDir, "AUTONOMA.md");
2021
- const autonomaExists = await readFile8(autonomaPath, "utf-8").then(() => true).catch((err) => {
2234
+ const autonomaPath = join15(input.outputDir, "AUTONOMA.md");
2235
+ const autonomaExists = await readFile9(autonomaPath, "utf-8").then(() => true).catch((err) => {
2022
2236
  debugLog("AUTONOMA.md not found while checking step completion", { err });
2023
2237
  return false;
2024
2238
  });
@@ -2029,6 +2243,13 @@ Output files:
2029
2243
  summary: "Knowledge base generated."
2030
2244
  };
2031
2245
  }
2246
+ const finalTracker = new PageTracker(input.projectRoot);
2247
+ if (pages?.length) {
2248
+ const paths = pages.map((p10) => p10.path);
2249
+ finalTracker.register(paths);
2250
+ for (const path3 of paths) finalTracker.markRead(path3);
2251
+ }
2252
+ const finalConfig = buildKbAgentConfig(finalTracker, model, input, onStepFinish, setResult);
2032
2253
  const declaredCriticalFlows = input.projectContext?.criticalFlows?.trim();
2033
2254
  if (result?.success && declaredCriticalFlows) {
2034
2255
  const beforeSelfReview = result;
@@ -2045,7 +2266,7 @@ Read your AUTONOMA.md output. For EACH critical flow the user named:
2045
2266
  If any declared critical flow is missing, mismatched, or left core: false, FIX AUTONOMA.md now - add the feature if it is genuinely absent, or flip core to true with a coreReason. Do not downgrade or drop anything the user declared critical.
2046
2267
 
2047
2268
  When AUTONOMA.md correctly reflects every declared critical flow, call finish.`;
2048
- await runAgent(agentConfig, selfReviewPrompt, () => result);
2269
+ await runAgent(finalConfig, selfReviewPrompt, () => result);
2049
2270
  if (!result) result = beforeSelfReview;
2050
2271
  }
2051
2272
  const reviewed = await reviewLoop(result, {
@@ -2066,7 +2287,7 @@ When AUTONOMA.md correctly reflects every declared critical flow, call finish.`;
2066
2287
  Read your previous output file (AUTONOMA.md) from the output directory to see what you produced.
2067
2288
  Adjust based on the feedback. You can read source files again if needed.
2068
2289
  Call page_coverage to see current state. When done with changes, call finish again.`;
2069
- await runAgent(agentConfig, feedbackPrompt, () => result);
2290
+ await runAgent(finalConfig, feedbackPrompt, () => result);
2070
2291
  return result;
2071
2292
  }
2072
2293
  });
@@ -2076,7 +2297,7 @@ Call page_coverage to see current state. When done with changes, call finish aga
2076
2297
  summary: "KB generator agent stopped without producing AUTONOMA.md"
2077
2298
  };
2078
2299
  }
2079
- var PageTracker;
2300
+ var PageTracker, FULL_COVERAGE_MAX_ROUTES, LARGE_APP_COVERAGE_FLOOR;
2080
2301
  var init_kb_generator = __esm({
2081
2302
  "src/agents/01-kb-generator/index.ts"() {
2082
2303
  "use strict";
@@ -2091,14 +2312,26 @@ var init_kb_generator = __esm({
2091
2312
  init_flows();
2092
2313
  init_prompt();
2093
2314
  PageTracker = class {
2315
+ // Registered pages (from pages.json) are absolute paths, but the agent reads them
2316
+ // with paths relative to the working directory. Canonicalize both to an absolute
2317
+ // path against projectRoot so coverage matches regardless of how each side spelled
2318
+ // it - otherwise the finish gate never clears and the agent nudges out re-reading.
2319
+ constructor(projectRoot) {
2320
+ this.projectRoot = projectRoot;
2321
+ }
2322
+ projectRoot;
2094
2323
  registered = /* @__PURE__ */ new Set();
2095
2324
  read = /* @__PURE__ */ new Set();
2325
+ normalize(filePath) {
2326
+ return resolve5(this.projectRoot, filePath);
2327
+ }
2096
2328
  register(pages) {
2097
- for (const p10 of pages) this.registered.add(p10);
2329
+ for (const p10 of pages) this.registered.add(this.normalize(p10));
2098
2330
  }
2099
2331
  markRead(filePath) {
2100
- if (this.registered.has(filePath)) {
2101
- this.read.add(filePath);
2332
+ const normalized = this.normalize(filePath);
2333
+ if (this.registered.has(normalized)) {
2334
+ this.read.add(normalized);
2102
2335
  }
2103
2336
  }
2104
2337
  unread() {
@@ -2112,16 +2345,18 @@ var init_kb_generator = __esm({
2112
2345
  };
2113
2346
  }
2114
2347
  };
2348
+ FULL_COVERAGE_MAX_ROUTES = 40;
2349
+ LARGE_APP_COVERAGE_FLOOR = 0.5;
2115
2350
  }
2116
2351
  });
2117
2352
 
2118
2353
  // src/agents/04-recipe-builder/entity-order.ts
2119
- import { readFile as readFile9 } from "fs/promises";
2120
- import { join as join15 } from "path";
2354
+ import { readFile as readFile10 } from "fs/promises";
2355
+ import { join as join16 } from "path";
2121
2356
  import matter2 from "gray-matter";
2122
- import { z as z12 } from "zod";
2357
+ import { z as z14 } from "zod";
2123
2358
  async function parseEntityAudit(outputDir) {
2124
- const raw = await readFile9(join15(outputDir, "entity-audit.md"), "utf-8");
2359
+ const raw = await readFile10(join16(outputDir, "entity-audit.md"), "utf-8");
2125
2360
  try {
2126
2361
  const parsed = frontmatterSchema.safeParse(matter2(raw).data);
2127
2362
  if (parsed.success && parsed.data.models.length > 0) {
@@ -2282,22 +2517,22 @@ var init_entity_order = __esm({
2282
2517
  "src/agents/04-recipe-builder/entity-order.ts"() {
2283
2518
  "use strict";
2284
2519
  init_esm_shims();
2285
- createdBySchema = z12.object({
2286
- owner: z12.string(),
2287
- via: z12.string().optional(),
2288
- why: z12.string().optional()
2520
+ createdBySchema = z14.object({
2521
+ owner: z14.string(),
2522
+ via: z14.string().optional(),
2523
+ why: z14.string().optional()
2289
2524
  });
2290
- auditedModelSchema = z12.object({
2291
- name: z12.string(),
2292
- independently_created: z12.coerce.boolean().default(false),
2293
- creation_file: z12.string().optional(),
2294
- creation_function: z12.string().optional(),
2295
- side_effects: z12.array(z12.string()).optional(),
2525
+ auditedModelSchema = z14.object({
2526
+ name: z14.string(),
2527
+ independently_created: z14.coerce.boolean().default(false),
2528
+ creation_file: z14.string().optional(),
2529
+ creation_function: z14.string().optional(),
2530
+ side_effects: z14.array(z14.string()).optional(),
2296
2531
  // Tolerate a stray `created_by:` with no entries (parsed as null by YAML).
2297
- created_by: z12.array(createdBySchema).nullish().transform((v) => v ?? [])
2532
+ created_by: z14.array(createdBySchema).nullish().transform((v) => v ?? [])
2298
2533
  });
2299
- frontmatterSchema = z12.object({
2300
- models: z12.array(auditedModelSchema).nullish().transform((v) => v ?? [])
2534
+ frontmatterSchema = z14.object({
2535
+ models: z14.array(auditedModelSchema).nullish().transform((v) => v ?? [])
2301
2536
  });
2302
2537
  }
2303
2538
  });
@@ -2364,12 +2599,12 @@ var init_audit_table = __esm({
2364
2599
  });
2365
2600
 
2366
2601
  // src/agents/02-entity-audit/prompt.ts
2367
- var SYSTEM_PROMPT3;
2602
+ var SYSTEM_PROMPT4;
2368
2603
  var init_prompt2 = __esm({
2369
2604
  "src/agents/02-entity-audit/prompt.ts"() {
2370
2605
  "use strict";
2371
2606
  init_esm_shims();
2372
- SYSTEM_PROMPT3 = `You audit a codebase to discover EVERY database model and every way each is created. You must find ALL models - missing one means the test data layer has a gap. This audit drives factory generation and scenario planning.
2607
+ SYSTEM_PROMPT4 = `You audit a codebase to discover EVERY database model and every way each is created. You must find ALL models - missing one means the test data layer has a gap. This audit drives factory generation and scenario planning.
2373
2608
 
2374
2609
  ## The two orthogonal questions
2375
2610
 
@@ -2507,17 +2742,17 @@ var entity_audit_exports = {};
2507
2742
  __export(entity_audit_exports, {
2508
2743
  runEntityAudit: () => runEntityAudit
2509
2744
  });
2510
- import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
2511
- import { join as join16 } from "path";
2512
- import { tool as tool11 } from "ai";
2745
+ import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
2746
+ import { join as join17 } from "path";
2747
+ import { tool as tool12 } from "ai";
2513
2748
  import { glob as glob4 } from "glob";
2514
- import { z as z13 } from "zod";
2749
+ import { z as z15 } from "zod";
2515
2750
  function buildRegisterModelsTool(tracker) {
2516
- return tool11({
2751
+ return tool12({
2517
2752
  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.",
2518
- inputSchema: z13.object({
2519
- models: z13.array(z13.string()).describe("All model/table names found by grep"),
2520
- framework: z13.string().describe("Database framework detected (e.g. 'sqlalchemy', 'prisma', 'drizzle')")
2753
+ inputSchema: z15.object({
2754
+ models: z15.array(z15.string()).describe("All model/table names found by grep"),
2755
+ framework: z15.string().describe("Database framework detected (e.g. 'sqlalchemy', 'prisma', 'drizzle')")
2521
2756
  }),
2522
2757
  execute: async (input) => {
2523
2758
  tracker.register(input.models);
@@ -2531,9 +2766,9 @@ function buildRegisterModelsTool(tracker) {
2531
2766
  });
2532
2767
  }
2533
2768
  function buildNextModelTool(tracker) {
2534
- return tool11({
2769
+ return tool12({
2535
2770
  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.",
2536
- inputSchema: z13.object({}),
2771
+ inputSchema: z15.object({}),
2537
2772
  execute: async () => {
2538
2773
  const next = tracker.nextModel();
2539
2774
  if (!next) {
@@ -2551,19 +2786,19 @@ function buildNextModelTool(tracker) {
2551
2786
  });
2552
2787
  }
2553
2788
  function buildMarkModelAuditedTool(tracker) {
2554
- return tool11({
2789
+ return tool12({
2555
2790
  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).",
2556
- inputSchema: z13.object({
2557
- model: z13.string().describe("Model name"),
2558
- independently_created: z13.boolean(),
2559
- creation_file: z13.string().optional().describe("File containing the creation function"),
2560
- creation_function: z13.string().optional().describe("Function/method name (e.g. 'UserService.create' or 'create_user')"),
2561
- side_effects: z13.array(z13.string()).optional().describe("Side effects of creation (e.g. 'creates default Settings row', 'hashes password')"),
2562
- created_by: z13.array(
2563
- z13.object({
2564
- owner: z13.string().describe("Owner model name"),
2565
- via: z13.string().optional().describe("Function that creates this model (e.g. 'OrganizationService.create')"),
2566
- why: z13.string().optional().describe("One sentence explaining why this model is created as a side effect")
2791
+ inputSchema: z15.object({
2792
+ model: z15.string().describe("Model name"),
2793
+ independently_created: z15.boolean(),
2794
+ creation_file: z15.string().optional().describe("File containing the creation function"),
2795
+ creation_function: z15.string().optional().describe("Function/method name (e.g. 'UserService.create' or 'create_user')"),
2796
+ side_effects: z15.array(z15.string()).optional().describe("Side effects of creation (e.g. 'creates default Settings row', 'hashes password')"),
2797
+ created_by: z15.array(
2798
+ z15.object({
2799
+ owner: z15.string().describe("Owner model name"),
2800
+ via: z15.string().optional().describe("Function that creates this model (e.g. 'OrganizationService.create')"),
2801
+ why: z15.string().optional().describe("One sentence explaining why this model is created as a side effect")
2567
2802
  })
2568
2803
  ).describe("List of owner models that create this as a side effect, empty array if none")
2569
2804
  }),
@@ -2600,18 +2835,18 @@ function buildMarkModelAuditedTool(tracker) {
2600
2835
  });
2601
2836
  }
2602
2837
  function buildModelCoverageTool(tracker) {
2603
- return tool11({
2838
+ return tool12({
2604
2839
  description: "Check how many registered models you've audited vs how many remain.",
2605
- inputSchema: z13.object({}),
2840
+ inputSchema: z15.object({}),
2606
2841
  execute: async () => tracker.coverage()
2607
2842
  });
2608
2843
  }
2609
2844
  function buildFinishTool2(tracker, onFinish) {
2610
- return tool11({
2845
+ return tool12({
2611
2846
  description: "Call when entity audit is complete. BLOCKED if there are unaudited models - call model_coverage first to check.",
2612
- inputSchema: z13.object({
2613
- summary: z13.string().describe("Summary of the audit"),
2614
- artifacts: z13.array(z13.string()).describe("Files written")
2847
+ inputSchema: z15.object({
2848
+ summary: z15.string().describe("Summary of the audit"),
2849
+ artifacts: z15.array(z15.string()).describe("Files written")
2615
2850
  }),
2616
2851
  execute: async (input) => {
2617
2852
  const cov = tracker.coverage();
@@ -2639,7 +2874,7 @@ async function findPrismaSchema(projectRoot) {
2639
2874
  return candidates[0] ?? void 0;
2640
2875
  }
2641
2876
  async function extractPrismaModels(schemaPath) {
2642
- const content = await readFile10(schemaPath, "utf-8");
2877
+ const content = await readFile11(schemaPath, "utf-8");
2643
2878
  return content.split("\n").filter((line) => line.startsWith("model ")).map((line) => line.split(/\s+/)[1]).filter((name) => name != null);
2644
2879
  }
2645
2880
  async function detectFrameworkAndModels(projectRoot) {
@@ -2662,8 +2897,11 @@ async function runEntityAudit(input) {
2662
2897
  tracker.initQueue();
2663
2898
  preRegisteredCount = detection.models.length;
2664
2899
  }
2665
- const { logger, onStepFinish } = buildDefaultStepLogger("entity-audit", 200);
2666
- const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + formatRetryGuidance(input.retryGuidance);
2900
+ const { logger, onStepFinish } = buildDefaultStepLogger("entity-audit", MAX_STEPS2);
2901
+ const scopeBlock = input.scopeHint != null ? `
2902
+ ${input.scopeHint}
2903
+ ` : "";
2904
+ const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + scopeBlock + formatRetryGuidance(input.retryGuidance);
2667
2905
  const preRegBlock = preRegisteredCount > 0 ? `
2668
2906
  ## Pre-registered models (${preRegisteredCount} found via ${detection.framework} schema at ${detection.schemaFile})
2669
2907
 
@@ -2693,9 +2931,9 @@ After every 10 mark_model_audited calls, use write_file to update entity-audit.m
2693
2931
  write_file already targets the output directory - use just the filename.`;
2694
2932
  const agentConfig = {
2695
2933
  id: "entity-audit",
2696
- systemPrompt: SYSTEM_PROMPT3,
2934
+ systemPrompt: SYSTEM_PROMPT4,
2697
2935
  model,
2698
- maxSteps: 200,
2936
+ maxSteps: MAX_STEPS2,
2699
2937
  tools: async (heartbeat) => {
2700
2938
  const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
2701
2939
  return {
@@ -2723,8 +2961,8 @@ ${formatException(err)}`);
2723
2961
  logger.summary();
2724
2962
  const writeCanonicalAudit = async () => {
2725
2963
  if (tracker.auditedModels.size === 0) return void 0;
2726
- const auditPath = join16(input.outputDir, "entity-audit.md");
2727
- await writeFile5(auditPath, tracker.generateAuditMarkdown(), "utf-8");
2964
+ const auditPath = join17(input.outputDir, "entity-audit.md");
2965
+ await writeFile6(auditPath, tracker.generateAuditMarkdown(), "utf-8");
2728
2966
  return auditPath;
2729
2967
  };
2730
2968
  const canonicalPath = await writeCanonicalAudit();
@@ -2761,9 +2999,9 @@ When done with changes, call finish again.`;
2761
2999
  }
2762
3000
  });
2763
3001
  if (!reviewed) {
2764
- const auditPath = join16(input.outputDir, "entity-audit.md");
3002
+ const auditPath = join17(input.outputDir, "entity-audit.md");
2765
3003
  try {
2766
- await readFile10(auditPath, "utf-8");
3004
+ await readFile11(auditPath, "utf-8");
2767
3005
  return {
2768
3006
  success: true,
2769
3007
  artifacts: ["entity-audit.md"],
@@ -2779,7 +3017,7 @@ When done with changes, call finish again.`;
2779
3017
  summary: agentError ? `Entity audit failed: ${agentError}` : "Entity audit agent stopped without producing entity-audit.md"
2780
3018
  };
2781
3019
  }
2782
- var ModelTracker;
3020
+ var MAX_STEPS2, ModelTracker;
2783
3021
  var init_entity_audit = __esm({
2784
3022
  "src/agents/02-entity-audit/index.ts"() {
2785
3023
  "use strict";
@@ -2794,6 +3032,7 @@ var init_entity_audit = __esm({
2794
3032
  init_ask_user();
2795
3033
  init_audit_table();
2796
3034
  init_prompt2();
3035
+ MAX_STEPS2 = 400;
2797
3036
  ModelTracker = class {
2798
3037
  registered = /* @__PURE__ */ new Set();
2799
3038
  auditedModels = /* @__PURE__ */ new Map();
@@ -2904,11 +3143,11 @@ ${duals.length > 0 ? duals.map((m) => `- **${m.name}** - standalone: ${m.creatio
2904
3143
  });
2905
3144
 
2906
3145
  // src/core/parse-entity-audit.ts
2907
- import { readFile as readFile11 } from "fs/promises";
2908
- import { join as join17 } from "path";
3146
+ import { readFile as readFile12 } from "fs/promises";
3147
+ import { join as join18 } from "path";
2909
3148
  async function parseEntityNames(outputDir) {
2910
3149
  try {
2911
- const content = await readFile11(join17(outputDir, "entity-audit.md"), "utf-8");
3150
+ const content = await readFile12(join18(outputDir, "entity-audit.md"), "utf-8");
2912
3151
  const names = [];
2913
3152
  for (const line of content.split("\n")) {
2914
3153
  const match = line.match(/^\s+-\s+name:\s+(.+)$/);
@@ -2981,13 +3220,13 @@ values; the recipe builder generates the exact records from what you write.`;
2981
3220
  });
2982
3221
 
2983
3222
  // src/agents/03-scenario-recipe/scenario-table.ts
2984
- import { readFile as readFile12 } from "fs/promises";
2985
- import { join as join18 } from "path";
3223
+ import { readFile as readFile13 } from "fs/promises";
3224
+ import { join as join19 } from "path";
2986
3225
  import matter3 from "gray-matter";
2987
3226
  async function parseScenario(outputDir) {
2988
3227
  let raw;
2989
3228
  try {
2990
- raw = await readFile12(join18(outputDir, "scenarios.md"), "utf-8");
3229
+ raw = await readFile13(join19(outputDir, "scenarios.md"), "utf-8");
2991
3230
  } catch {
2992
3231
  return { scenarioNames: [], entityTypes: [] };
2993
3232
  }
@@ -3068,22 +3307,22 @@ __export(scenario_recipe_exports, {
3068
3307
  feedbackToScenario: () => feedbackToScenario,
3069
3308
  runScenarioRecipe: () => runScenarioRecipe
3070
3309
  });
3071
- import { readFile as readFile13 } from "fs/promises";
3072
- import { join as join19 } from "path";
3073
- import { tool as tool12 } from "ai";
3074
- import { z as z14 } from "zod";
3310
+ import { readFile as readFile14 } from "fs/promises";
3311
+ import { join as join20 } from "path";
3312
+ import { tool as tool13 } from "ai";
3313
+ import { z as z16 } from "zod";
3075
3314
  function buildFinishTool3(requiredEntities, outputDir, onFinish) {
3076
- return tool12({
3315
+ return tool13({
3077
3316
  description: "Call when scenario design is complete and scenarios.md is written. BLOCKED if any required entities are missing from the scenario.",
3078
- inputSchema: z14.object({
3079
- summary: z14.string().describe("Summary of the scenario"),
3080
- entityCount: z14.number().describe("Number of entity types in the scenario"),
3081
- artifacts: z14.array(z14.string()).describe("Files written")
3317
+ inputSchema: z16.object({
3318
+ summary: z16.string().describe("Summary of the scenario"),
3319
+ entityCount: z16.number().describe("Number of entity types in the scenario"),
3320
+ artifacts: z16.array(z16.string()).describe("Files written")
3082
3321
  }),
3083
3322
  execute: async (input) => {
3084
3323
  let content;
3085
3324
  try {
3086
- content = await readFile13(join19(outputDir, "scenarios.md"), "utf-8");
3325
+ content = await readFile14(join20(outputDir, "scenarios.md"), "utf-8");
3087
3326
  } catch {
3088
3327
  return { error: "Cannot finish: scenarios.md not found. Write it first." };
3089
3328
  }
@@ -3118,7 +3357,10 @@ async function runScenarioRecipe(input) {
3118
3357
  const model = getModel(input.modelId);
3119
3358
  let result;
3120
3359
  const { logger, onStepFinish } = buildDefaultStepLogger("scenario", 40);
3121
- const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + formatRetryGuidance(input.retryGuidance);
3360
+ const scopeBlock = input.scopeHint != null ? `
3361
+ ${input.scopeHint}
3362
+ ` : "";
3363
+ const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + scopeBlock + formatRetryGuidance(input.retryGuidance);
3122
3364
  const requiredEntities = await parseEntityNames(input.outputDir);
3123
3365
  const entityListBlock = requiredEntities.length > 0 ? `
3124
3366
  ## Required entities (${requiredEntities.length} total - ALL must appear in the scenario)
@@ -3180,9 +3422,9 @@ When done with changes, call finish again.`;
3180
3422
  }
3181
3423
  });
3182
3424
  if (!reviewed) {
3183
- const scenariosPath = join19(input.outputDir, "scenarios.md");
3425
+ const scenariosPath = join20(input.outputDir, "scenarios.md");
3184
3426
  try {
3185
- await readFile13(scenariosPath, "utf-8");
3427
+ await readFile14(scenariosPath, "utf-8");
3186
3428
  return {
3187
3429
  success: true,
3188
3430
  artifacts: ["scenarios.md"],
@@ -3243,7 +3485,7 @@ var init_scenario_recipe = __esm({
3243
3485
 
3244
3486
  // src/agents/04-recipe-builder/entity-relevance.ts
3245
3487
  import { Output, generateText } from "ai";
3246
- import { z as z15 } from "zod";
3488
+ import { z as z17 } from "zod";
3247
3489
  async function callRanker(model, prompt) {
3248
3490
  const result = await generateText({
3249
3491
  model,
@@ -3331,19 +3573,19 @@ var init_entity_relevance = __esm({
3331
3573
  init_esm_shims();
3332
3574
  init_errors();
3333
3575
  init_model();
3334
- rankedSchema = z15.object({
3335
- ranked: z15.array(z15.string()).describe("Every entity name, ordered most-important first.")
3576
+ rankedSchema = z17.object({
3577
+ ranked: z17.array(z17.string()).describe("Every entity name, ordered most-important first.")
3336
3578
  });
3337
3579
  }
3338
3580
  });
3339
3581
 
3340
3582
  // src/core/detect-pkg-manager.ts
3341
3583
  import { existsSync as existsSync2 } from "fs";
3342
- import { join as join20 } from "path";
3584
+ import { join as join21 } from "path";
3343
3585
  function detectPackageManager(projectRoot) {
3344
- if (existsSync2(join20(projectRoot, "bun.lock")) || existsSync2(join20(projectRoot, "bun.lockb"))) return "bun";
3345
- if (existsSync2(join20(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3346
- if (existsSync2(join20(projectRoot, "yarn.lock"))) return "yarn";
3586
+ if (existsSync2(join21(projectRoot, "bun.lock")) || existsSync2(join21(projectRoot, "bun.lockb"))) return "bun";
3587
+ if (existsSync2(join21(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3588
+ if (existsSync2(join21(projectRoot, "yarn.lock"))) return "yarn";
3347
3589
  return "npm";
3348
3590
  }
3349
3591
  function installCommand(pm, ...packages) {
@@ -3614,8 +3856,8 @@ var init_discover_schema = __esm({
3614
3856
  });
3615
3857
 
3616
3858
  // src/agents/04-recipe-builder/recipe.ts
3617
- import { readFile as readFile14, writeFile as writeFile6 } from "fs/promises";
3618
- import { join as join21 } from "path";
3859
+ import { readFile as readFile15, writeFile as writeFile7 } from "fs/promises";
3860
+ import { join as join22 } from "path";
3619
3861
  function collectRefs2(value, out) {
3620
3862
  if (Array.isArray(value)) {
3621
3863
  for (const v of value) collectRefs2(v, out);
@@ -3688,7 +3930,7 @@ function buildSubmittableRecipe(create, description) {
3688
3930
  };
3689
3931
  }
3690
3932
  async function saveRecipe(outputDir, recipe) {
3691
- await writeFile6(join21(outputDir, RECIPE_FILE), JSON.stringify(recipe, null, 2), "utf-8");
3933
+ await writeFile7(join22(outputDir, RECIPE_FILE), JSON.stringify(recipe, null, 2), "utf-8");
3692
3934
  }
3693
3935
  var RECIPE_FILE;
3694
3936
  var init_recipe = __esm({
@@ -3701,8 +3943,8 @@ var init_recipe = __esm({
3701
3943
  });
3702
3944
 
3703
3945
  // src/agents/04-recipe-builder/state.ts
3704
- import { readFile as readFile15, writeFile as writeFile7 } from "fs/promises";
3705
- import { join as join22 } from "path";
3946
+ import { readFile as readFile16, writeFile as writeFile8 } from "fs/promises";
3947
+ import { join as join23 } from "path";
3706
3948
  function adapterKey(a) {
3707
3949
  return `${a.language}:${a.framework}`;
3708
3950
  }
@@ -3724,7 +3966,7 @@ function initialRecipeState() {
3724
3966
  }
3725
3967
  async function loadRecipeState(outputDir) {
3726
3968
  try {
3727
- const raw = await readFile15(join22(outputDir, STATE_FILE2), "utf-8");
3969
+ const raw = await readFile16(join23(outputDir, STATE_FILE2), "utf-8");
3728
3970
  const parsed = JSON.parse(raw);
3729
3971
  return parsed;
3730
3972
  } catch {
@@ -3732,7 +3974,7 @@ async function loadRecipeState(outputDir) {
3732
3974
  }
3733
3975
  }
3734
3976
  async function saveRecipeState(outputDir, state) {
3735
- await writeFile7(join22(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
3977
+ await writeFile8(join23(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
3736
3978
  }
3737
3979
  var ALL_ADAPTERS, ADAPTER_HINTS, STATE_FILE2;
3738
3980
  var init_state = __esm({
@@ -3804,7 +4046,7 @@ var init_state = __esm({
3804
4046
 
3805
4047
  // src/agents/04-recipe-builder/phases/failure-classifier.ts
3806
4048
  import { Output as Output2, generateText as generateText2 } from "ai";
3807
- import { z as z16 } from "zod";
4049
+ import { z as z18 } from "zod";
3808
4050
  function buildClassifierPrompt(args) {
3809
4051
  const errorText = typeof args.error === "string" ? args.error : JSON.stringify(args.error, null, 2);
3810
4052
  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 - the factory tried to insert the recipe records.`;
@@ -3864,11 +4106,11 @@ var init_failure_classifier = __esm({
3864
4106
  init_esm_shims();
3865
4107
  init_errors();
3866
4108
  init_model();
3867
- classificationSchema = z16.object({
3868
- side: z16.enum(["recipe", "implementation", "unclear"]).describe(
4109
+ classificationSchema = z18.object({
4110
+ side: z18.enum(["recipe", "implementation", "unclear"]).describe(
3869
4111
  "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."
3870
4112
  ),
3871
- reason: z16.string().describe("One short, plain-language sentence explaining the verdict for the user. No code, no jargon dumps.")
4113
+ reason: z18.string().describe("One short, plain-language sentence explaining the verdict for the user. No code, no jargon dumps.")
3872
4114
  });
3873
4115
  PRIMER = `## Background - what you are looking at
3874
4116
 
@@ -3883,13 +4125,13 @@ So a failure has exactly two possible origins, and your only job is to tell them
3883
4125
  });
3884
4126
 
3885
4127
  // src/agents/04-recipe-builder/phases/entity-loop.ts
3886
- import { writeFile as writeFile8, readFile as readFile16 } from "fs/promises";
4128
+ import { writeFile as writeFile9, readFile as readFile17 } from "fs/promises";
3887
4129
  import { tmpdir } from "os";
3888
- import { join as join23 } from "path";
4130
+ import { join as join24 } from "path";
3889
4131
  import * as p4 from "@clack/prompts";
3890
- import { tool as tool13 } from "ai";
4132
+ import { tool as tool14 } from "ai";
3891
4133
  import spawn2 from "cross-spawn";
3892
- import { z as z17 } from "zod";
4134
+ import { z as z19 } from "zod";
3893
4135
  function summarizeCompletedAliases(completedEntities, excludeName) {
3894
4136
  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");
3895
4137
  }
@@ -3904,10 +4146,10 @@ function summarizeEntityAudit(model) {
3904
4146
  async function proposeRecipeData(entityName, entityIndex, totalEntities, model, outputDir, _projectRoot, completedEntities, schemaSpec) {
3905
4147
  let result;
3906
4148
  const { logger, onStepFinish } = buildDefaultStepLogger(`propose:${entityName}`, 20);
3907
- const finishTool = tool13({
4149
+ const finishTool = tool14({
3908
4150
  description: "Submit the proposed recipe data as a JSON array of records.",
3909
- inputSchema: z17.object({
3910
- records: z17.array(z17.record(z17.string(), z17.unknown())).describe("Array of record objects for this entity")
4151
+ inputSchema: z19.object({
4152
+ records: z19.array(z19.record(z19.string(), z19.unknown())).describe("Array of record objects for this entity")
3911
4153
  }),
3912
4154
  execute: async (input) => {
3913
4155
  result = input.records;
@@ -3950,10 +4192,10 @@ Call finish with the JSON array of records.`;
3950
4192
  }
3951
4193
  async function reviseRecipeData(entityName, entityIndex, totalEntities, current, feedback, model, outputDir, completedEntities, schemaSpec) {
3952
4194
  let revised;
3953
- const finishTool = tool13({
4195
+ const finishTool = tool14({
3954
4196
  description: "Submit the fixed recipe data.",
3955
- inputSchema: z17.object({
3956
- records: z17.array(z17.record(z17.string(), z17.unknown()))
4197
+ inputSchema: z19.object({
4198
+ records: z19.array(z19.record(z19.string(), z19.unknown()))
3957
4199
  }),
3958
4200
  execute: async (input) => {
3959
4201
  revised = input.records;
@@ -4011,10 +4253,10 @@ Read scenarios.md and entity-audit.md to understand the correct aliases and sche
4011
4253
  async function generateInstructions(entityName, entityIndex, totalEntities, isFirst, techStack, auditModel, recipeData, model, projectRoot, outputDir) {
4012
4254
  let result;
4013
4255
  const { logger, onStepFinish } = buildDefaultStepLogger(`instructions:${entityName}`, 15);
4014
- const finishTool = tool13({
4256
+ const finishTool = tool14({
4015
4257
  description: "Submit the implementation instructions.",
4016
- inputSchema: z17.object({
4017
- instructions: z17.string().describe("Complete, copy-pasteable implementation instructions")
4258
+ inputSchema: z19.object({
4259
+ instructions: z19.string().describe("Complete, copy-pasteable implementation instructions")
4018
4260
  }),
4019
4261
  execute: async (input) => {
4020
4262
  result = input.instructions;
@@ -4096,23 +4338,23 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
4096
4338
  if (p4.isCancel(action)) throw new Error("Recipe review cancelled");
4097
4339
  if (action === "keep") return proposed;
4098
4340
  if (action === "edit") {
4099
- const tmpPath = join23(tmpdir(), `autonoma-recipe-${entityName}.json`);
4100
- await writeFile8(tmpPath, JSON.stringify(proposed, null, 2), "utf-8");
4341
+ const tmpPath = join24(tmpdir(), `autonoma-recipe-${entityName}.json`);
4342
+ await writeFile9(tmpPath, JSON.stringify(proposed, null, 2), "utf-8");
4101
4343
  const env = readEnv();
4102
4344
  const editor = env.EDITOR ?? env.VISUAL ?? "vi";
4103
4345
  p4.log.info(`Opening ${editor}... Save and close when done.`);
4104
- const launched = await new Promise((resolve5) => {
4346
+ const launched = await new Promise((resolve6) => {
4105
4347
  const proc = spawn2(editor, [tmpPath], { stdio: "inherit" });
4106
- proc.on("close", () => resolve5(true));
4348
+ proc.on("close", () => resolve6(true));
4107
4349
  proc.on("error", (err) => {
4108
4350
  p4.log.error(
4109
4351
  `Couldn't open ${editor} (${err.message}). Edit this file manually, then choose "edit" again: ${tmpPath}`
4110
4352
  );
4111
- resolve5(false);
4353
+ resolve6(false);
4112
4354
  });
4113
4355
  });
4114
4356
  if (!launched) continue;
4115
- const edited = await readFile16(tmpPath, "utf-8");
4357
+ const edited = await readFile17(tmpPath, "utf-8");
4116
4358
  try {
4117
4359
  proposed = JSON.parse(edited);
4118
4360
  p4.note(JSON.stringify(proposed, null, 2), `Updated data for ${entityName}`, { format: codeNoteFormat });
@@ -4404,8 +4646,8 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
4404
4646
  const secret = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
4405
4647
  state.sharedSecret = secret;
4406
4648
  await saveRecipeState(outputDir, state);
4407
- await writeFile8(
4408
- join23(outputDir, "autonoma-config.json"),
4649
+ await writeFile9(
4650
+ join24(outputDir, "autonoma-config.json"),
4409
4651
  JSON.stringify({ sharedSecret: secret, endpointUrl: state.sdkEndpointUrl }, null, 2),
4410
4652
  "utf-8"
4411
4653
  );
@@ -4416,7 +4658,7 @@ Add this to your server's .env file and restart it.
4416
4658
  This is a 64-character hex key used for HMAC-SHA256 request signing.
4417
4659
  The same value must be set in both your server and the Autonoma dashboard.
4418
4660
 
4419
- Saved to: ${join23(outputDir, "autonoma-config.json")}`,
4661
+ Saved to: ${join24(outputDir, "autonoma-config.json")}`,
4420
4662
  "Shared secret generated"
4421
4663
  );
4422
4664
  const secretReady = await p4.confirm({
@@ -4437,8 +4679,8 @@ Saved to: ${join23(outputDir, "autonoma-config.json")}`,
4437
4679
  if (p4.isCancel(url)) throw new Error("Entity loop cancelled");
4438
4680
  state.sdkEndpointUrl = url.trim() || "http://localhost:3000/api/autonoma";
4439
4681
  await saveRecipeState(outputDir, state);
4440
- await writeFile8(
4441
- join23(outputDir, "autonoma-config.json"),
4682
+ await writeFile9(
4683
+ join24(outputDir, "autonoma-config.json"),
4442
4684
  JSON.stringify({ sharedSecret: state.sharedSecret, endpointUrl: state.sdkEndpointUrl }, null, 2),
4443
4685
  "utf-8"
4444
4686
  );
@@ -4554,14 +4796,14 @@ When done, call finish with the instructions text.`;
4554
4796
 
4555
4797
  // src/agents/04-recipe-builder/phases/full-validation.ts
4556
4798
  import * as p5 from "@clack/prompts";
4557
- import { tool as tool14 } from "ai";
4558
- import { z as z18 } from "zod";
4799
+ import { tool as tool15 } from "ai";
4800
+ import { z as z20 } from "zod";
4559
4801
  async function reviseFullRecipe(current, feedback, model, outputDir, entityOrder, schemaSpec) {
4560
4802
  let revised;
4561
- const finishTool = tool14({
4803
+ const finishTool = tool15({
4562
4804
  description: "Submit the revised full recipe: an object mapping each entity name to its array of records.",
4563
- inputSchema: z18.object({
4564
- recipe: z18.record(z18.string(), z18.array(z18.record(z18.string(), z18.unknown())))
4805
+ inputSchema: z20.object({
4806
+ recipe: z20.record(z20.string(), z20.array(z20.record(z20.string(), z20.unknown())))
4565
4807
  }),
4566
4808
  execute: async (input) => {
4567
4809
  revised = input.recipe;
@@ -4816,18 +5058,18 @@ var init_submit = __esm({
4816
5058
 
4817
5059
  // src/agents/04-recipe-builder/phases/tech-detect.ts
4818
5060
  import * as p7 from "@clack/prompts";
4819
- import { tool as tool15 } from "ai";
4820
- import { z as z19 } from "zod";
5061
+ import { tool as tool16 } from "ai";
5062
+ import { z as z21 } from "zod";
4821
5063
  async function detectTechStack(projectRoot, modelId, nonInteractive) {
4822
5064
  const model = getModel(modelId);
4823
5065
  const ignorePatterns = await loadGitignorePatterns(projectRoot);
4824
5066
  let detected;
4825
5067
  const { logger, onStepFinish } = buildDefaultStepLogger("tech-detect", 10);
4826
- const finishTool = tool15({
5068
+ const finishTool = tool16({
4827
5069
  description: "Report the detected backend technology stack.",
4828
- inputSchema: z19.object({
4829
- language: z19.string().describe("Programming language: typescript, python, go, ruby, java, php, rust, elixir"),
4830
- framework: z19.string().describe(
5070
+ inputSchema: z21.object({
5071
+ language: z21.string().describe("Programming language: typescript, python, go, ruby, java, php, rust, elixir"),
5072
+ framework: z21.string().describe(
4831
5073
  "Web framework: express, node, hono, web, flask, fastapi, django, gin, rails, rack, spring, laravel, axum, actix, plug"
4832
5074
  )
4833
5075
  }),
@@ -4838,7 +5080,7 @@ async function detectTechStack(projectRoot, modelId, nonInteractive) {
4838
5080
  });
4839
5081
  const agentConfig = {
4840
5082
  id: "tech-detect",
4841
- systemPrompt: SYSTEM_PROMPT4,
5083
+ systemPrompt: SYSTEM_PROMPT5,
4842
5084
  model,
4843
5085
  maxSteps: 10,
4844
5086
  tools: (_heartbeat) => ({
@@ -4894,7 +5136,7 @@ async function detectTechStack(projectRoot, modelId, nonInteractive) {
4894
5136
  p7.log.success(`Using ${adapterLabel(adapter)} - SDK: ${adapter.sdkPackage}, Adapter: ${adapter.adapterPackage}`);
4895
5137
  return adapter;
4896
5138
  }
4897
- var DOCS_BASE, SYSTEM_PROMPT4;
5139
+ var DOCS_BASE, SYSTEM_PROMPT5;
4898
5140
  var init_tech_detect = __esm({
4899
5141
  "src/agents/04-recipe-builder/phases/tech-detect.ts"() {
4900
5142
  "use strict";
@@ -4905,7 +5147,7 @@ var init_tech_detect = __esm({
4905
5147
  init_tools();
4906
5148
  init_state();
4907
5149
  DOCS_BASE = "https://docs.autonoma.app";
4908
- SYSTEM_PROMPT4 = `You are a backend technology detector. Your job is to identify the programming language and web framework used by a project's backend/API server.
5150
+ SYSTEM_PROMPT5 = `You are a backend technology detector. Your job is to identify the programming language and web framework used by a project's backend/API server.
4909
5151
 
4910
5152
  Explore the project files to detect:
4911
5153
  1. The programming language (check package.json, requirements.txt, go.mod, Gemfile, pom.xml, composer.json, Cargo.toml, mix.exs)
@@ -4920,8 +5162,8 @@ var recipe_builder_exports = {};
4920
5162
  __export(recipe_builder_exports, {
4921
5163
  runRecipeBuilder: () => runRecipeBuilder
4922
5164
  });
4923
- import { readFile as readFile17 } from "fs/promises";
4924
- import { join as join24 } from "path";
5165
+ import { readFile as readFile18 } from "fs/promises";
5166
+ import { join as join25 } from "path";
4925
5167
  import * as p8 from "@clack/prompts";
4926
5168
  async function runRecipeBuilder(input) {
4927
5169
  const model = getModel(input.modelId);
@@ -4935,7 +5177,7 @@ async function runRecipeBuilder(input) {
4935
5177
  state.techStack = await detectTechStack(input.projectRoot, input.modelId, input.nonInteractive);
4936
5178
  let importanceRank;
4937
5179
  try {
4938
- const auditMarkdown = await readFile17(join24(input.outputDir, "entity-audit.md"), "utf-8");
5180
+ const auditMarkdown = await readFile18(join25(input.outputDir, "entity-audit.md"), "utf-8");
4939
5181
  importanceRank = await rankEntitiesByImportance(models, auditMarkdown, model);
4940
5182
  } catch {
4941
5183
  importanceRank = void 0;
@@ -5031,21 +5273,21 @@ var init_recipe_builder = __esm({
5031
5273
  });
5032
5274
 
5033
5275
  // src/agents/05-test-generator/rubrics.ts
5034
- import { z as z20 } from "zod";
5276
+ import { z as z22 } from "zod";
5035
5277
  function reviewResultSchema(shape) {
5036
- return z20.object(shape);
5278
+ return z22.object(shape);
5037
5279
  }
5038
5280
  var dimensionResultSchema, reviewResultRecordSchema, structuralIntentRubric, flowCompletenessRubric, uiTextRubric, dataAccuracyRubric, ALL_RUBRICS;
5039
5281
  var init_rubrics = __esm({
5040
5282
  "src/agents/05-test-generator/rubrics.ts"() {
5041
5283
  "use strict";
5042
5284
  init_esm_shims();
5043
- dimensionResultSchema = z20.object({
5044
- pass: z20.boolean(),
5045
- evidence: z20.string().describe("What you checked and found - cite file paths, line content, or specific strings"),
5046
- suggestion: z20.string().optional().describe("What the planner agent should fix, if failing")
5285
+ dimensionResultSchema = z22.object({
5286
+ pass: z22.boolean(),
5287
+ evidence: z22.string().describe("What you checked and found - cite file paths, line content, or specific strings"),
5288
+ suggestion: z22.string().optional().describe("What the planner agent should fix, if failing")
5047
5289
  });
5048
- reviewResultRecordSchema = z20.record(z20.string(), dimensionResultSchema);
5290
+ reviewResultRecordSchema = z22.record(z22.string(), dimensionResultSchema);
5049
5291
  structuralIntentRubric = {
5050
5292
  name: "structural-intent",
5051
5293
  maxSteps: 8,
@@ -5219,12 +5461,12 @@ When done reviewing, call finish with your structured evaluation.`
5219
5461
  // src/agents/05-test-generator/review-pass.ts
5220
5462
  import { basename as basename2 } from "path";
5221
5463
  import "ai";
5222
- import { tool as tool16 } from "ai";
5464
+ import { tool as tool17 } from "ai";
5223
5465
  async function runReviewPass(testContent, testPath, rubric, projectRoot, model, scenarioData) {
5224
5466
  let result;
5225
5467
  const agentLabel = `review:${rubric.name}:${basename2(testPath)}`;
5226
5468
  const { onStepFinish } = buildDefaultStepLogger(agentLabel, rubric.maxSteps);
5227
- const finishTool = tool16({
5469
+ const finishTool = tool17({
5228
5470
  description: "Submit your structured review. Every dimension must have evidence from your investigation.",
5229
5471
  inputSchema: rubric.resultSchema,
5230
5472
  execute: async (input) => {
@@ -5282,8 +5524,8 @@ var init_review_pass = __esm({
5282
5524
  });
5283
5525
 
5284
5526
  // src/agents/05-test-generator/review.ts
5285
- import { readFile as readFile18 } from "fs/promises";
5286
- import { join as join25, relative as relative6, basename as basename3 } from "path";
5527
+ import { readFile as readFile19 } from "fs/promises";
5528
+ import { join as join26, relative as relative6, basename as basename3 } from "path";
5287
5529
  import "ai";
5288
5530
  import { glob as glob5 } from "glob";
5289
5531
  async function reviewSingleTest(testContent, testPath, projectRoot, model, scenarioData) {
@@ -5312,19 +5554,19 @@ async function reviewSingleTest(testContent, testPath, projectRoot, model, scena
5312
5554
  return merged;
5313
5555
  }
5314
5556
  async function runConsolidatedReview(outputDir, projectRoot, model) {
5315
- const testsDir = join25(outputDir, "qa-tests");
5557
+ const testsDir = join26(outputDir, "qa-tests");
5316
5558
  const logger = createStepLogger("review", 5);
5317
5559
  let scenarioData;
5318
5560
  try {
5319
- scenarioData = await readFile18(join25(outputDir, "scenarios.md"), "utf-8");
5561
+ scenarioData = await readFile19(join26(outputDir, "scenarios.md"), "utf-8");
5320
5562
  } catch {
5321
5563
  }
5322
- const testFiles = await glob5(join25(testsDir, "**/*.md"));
5564
+ const testFiles = await glob5(join26(testsDir, "**/*.md"));
5323
5565
  const tests = [];
5324
5566
  for (const testPath of testFiles) {
5325
5567
  if (basename3(testPath) === "INDEX.md") continue;
5326
5568
  if (testPath.includes("/_invalid/")) continue;
5327
- const content = await readFile18(testPath, "utf-8");
5569
+ const content = await readFile19(testPath, "utf-8");
5328
5570
  const flowMatch = content.match(/^---\n[\s\S]*?flow:\s*["']?([^"'\n]+)["']?\s*\n[\s\S]*?---/m);
5329
5571
  tests.push({
5330
5572
  path: testPath,
@@ -5407,17 +5649,17 @@ var init_review2 = __esm({
5407
5649
  });
5408
5650
 
5409
5651
  // src/agents/00b-feature-discovery/index.ts
5410
- import { readFile as readFile19, writeFile as writeFile9 } from "fs/promises";
5411
- import { join as join26 } from "path";
5412
- import { tool as tool17 } from "ai";
5413
- import { z as z21 } from "zod";
5652
+ import { readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
5653
+ import { join as join27 } from "path";
5654
+ import { tool as tool18 } from "ai";
5655
+ import { z as z23 } from "zod";
5414
5656
  async function saveFeatures(outputDir, features) {
5415
5657
  const obj = Object.fromEntries(features);
5416
- await writeFile9(join26(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
5658
+ await writeFile10(join27(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
5417
5659
  }
5418
5660
  async function loadFeatures(outputDir) {
5419
5661
  try {
5420
- const raw = await readFile19(join26(outputDir, FEATURES_FILE), "utf-8");
5662
+ const raw = await readFile20(join27(outputDir, FEATURES_FILE), "utf-8");
5421
5663
  const obj = JSON.parse(raw);
5422
5664
  return new Map(Object.entries(obj));
5423
5665
  } catch {
@@ -5441,17 +5683,17 @@ ${pagesDescription}
5441
5683
  Process every page. Call add_feature for each sub-feature you discover. When done, call finish.`;
5442
5684
  const agentConfig = {
5443
5685
  id: "feature-discovery",
5444
- systemPrompt: SYSTEM_PROMPT5,
5686
+ systemPrompt: SYSTEM_PROMPT6,
5445
5687
  model,
5446
5688
  maxSteps: 300,
5447
5689
  tools: async (heartbeat) => {
5448
5690
  const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
5449
5691
  return {
5450
5692
  ...tools,
5451
- add_feature: tool17({
5693
+ add_feature: tool18({
5452
5694
  description: "Add a discovered sub-feature",
5453
5695
  inputSchema: Feature.extend({
5454
- id: z21.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
5696
+ id: z23.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
5455
5697
  }),
5456
5698
  execute: (featureInput) => {
5457
5699
  const { id, ...rest } = featureInput;
@@ -5463,19 +5705,19 @@ Process every page. Call add_feature for each sub-feature you discover. When don
5463
5705
  return `Feature "${id}" added (${collector.features.size} total)`;
5464
5706
  }
5465
5707
  }),
5466
- view_features: tool17({
5708
+ view_features: tool18({
5467
5709
  description: "View all discovered features so far",
5468
- inputSchema: z21.object({}),
5710
+ inputSchema: z23.object({}),
5469
5711
  execute: () => collector.viewFeatures()
5470
5712
  }),
5471
- view_pages: tool17({
5713
+ view_pages: tool18({
5472
5714
  description: "View the pages list to know what to analyze",
5473
- inputSchema: z21.object({}),
5715
+ inputSchema: z23.object({}),
5474
5716
  execute: () => pagesDescription
5475
5717
  }),
5476
- finish: tool17({
5718
+ finish: tool18({
5477
5719
  description: "Signal that feature discovery is complete",
5478
- inputSchema: z21.object({ summary: z21.string() }),
5720
+ inputSchema: z23.object({ summary: z23.string() }),
5479
5721
  execute: async (finishInput) => {
5480
5722
  result = {
5481
5723
  success: true,
@@ -5497,7 +5739,7 @@ Process every page. Call add_feature for each sub-feature you discover. When don
5497
5739
  }
5498
5740
  return collector.features;
5499
5741
  }
5500
- var FEATURES_FILE, Feature, FeatureCollector, SYSTEM_PROMPT5;
5742
+ var FEATURES_FILE, Feature, FeatureCollector, SYSTEM_PROMPT6;
5501
5743
  var init_b_feature_discovery = __esm({
5502
5744
  "src/agents/00b-feature-discovery/index.ts"() {
5503
5745
  "use strict";
@@ -5506,13 +5748,13 @@ var init_b_feature_discovery = __esm({
5506
5748
  init_model();
5507
5749
  init_tools();
5508
5750
  FEATURES_FILE = "features.json";
5509
- Feature = z21.object({
5510
- name: z21.string().min(1).describe("Human-readable name (e.g. 'Settings > Notifications Tab', 'Create Project Modal')"),
5511
- type: z21.enum(["tab", "modal", "form", "table", "wizard", "nested-route", "complex-component"]),
5512
- parentPagePath: z21.string().min(1).describe("The page path this feature belongs to (from the pages list)"),
5513
- sourceFiles: z21.array(z21.string()).min(1).describe("Relative paths to the source files for this sub-feature"),
5514
- interactiveElements: z21.number().int().min(0).describe("Count of interactive elements found (buttons, inputs, toggles, etc.)"),
5515
- description: z21.string().min(10).describe("What this sub-feature does")
5751
+ Feature = z23.object({
5752
+ name: z23.string().min(1).describe("Human-readable name (e.g. 'Settings > Notifications Tab', 'Create Project Modal')"),
5753
+ type: z23.enum(["tab", "modal", "form", "table", "wizard", "nested-route", "complex-component"]),
5754
+ parentPagePath: z23.string().min(1).describe("The page path this feature belongs to (from the pages list)"),
5755
+ sourceFiles: z23.array(z23.string()).min(1).describe("Relative paths to the source files for this sub-feature"),
5756
+ interactiveElements: z23.number().int().min(0).describe("Count of interactive elements found (buttons, inputs, toggles, etc.)"),
5757
+ description: z23.string().min(10).describe("What this sub-feature does")
5516
5758
  });
5517
5759
  FeatureCollector = class {
5518
5760
  features = /* @__PURE__ */ new Map();
@@ -5543,7 +5785,7 @@ ${page}:`);
5543
5785
  return lines.join("\n");
5544
5786
  }
5545
5787
  };
5546
- SYSTEM_PROMPT5 = `You are a feature discovery agent. Your job is to explore each page's source code and discover all sub-features that deserve their own test coverage.
5788
+ SYSTEM_PROMPT6 = `You are a feature discovery agent. Your job is to explore each page's source code and discover all sub-features that deserve their own test coverage.
5547
5789
 
5548
5790
  You will be given a list of pages. For each page, you must:
5549
5791
  1. Read the page's source file
@@ -5595,16 +5837,16 @@ Use kebab-case IDs that indicate the parent page and feature type:
5595
5837
  });
5596
5838
 
5597
5839
  // src/agents/05-test-generator/graph.ts
5598
- import { readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
5599
- import { join as join27 } from "path";
5840
+ import { readFile as readFile21, writeFile as writeFile11 } from "fs/promises";
5841
+ import { join as join28 } from "path";
5600
5842
  async function saveBfsState(outputDir, state) {
5601
- const path3 = join27(outputDir, STATE_FILE3);
5602
- await writeFile10(path3, JSON.stringify(state.serialize(), null, 2), "utf-8");
5843
+ const path3 = join28(outputDir, STATE_FILE3);
5844
+ await writeFile11(path3, JSON.stringify(state.serialize(), null, 2), "utf-8");
5603
5845
  }
5604
5846
  async function loadBfsState(outputDir) {
5605
- const path3 = join27(outputDir, STATE_FILE3);
5847
+ const path3 = join28(outputDir, STATE_FILE3);
5606
5848
  try {
5607
- const raw = await readFile20(path3, "utf-8");
5849
+ const raw = await readFile21(path3, "utf-8");
5608
5850
  return CoverageState.deserialize(JSON.parse(raw));
5609
5851
  } catch {
5610
5852
  return void 0;
@@ -5695,12 +5937,12 @@ var init_graph = __esm({
5695
5937
  });
5696
5938
 
5697
5939
  // src/agents/05-test-generator/prompt.ts
5698
- var SYSTEM_PROMPT6;
5940
+ var SYSTEM_PROMPT7;
5699
5941
  var init_prompt4 = __esm({
5700
5942
  "src/agents/05-test-generator/prompt.ts"() {
5701
5943
  "use strict";
5702
5944
  init_esm_shims();
5703
- SYSTEM_PROMPT6 = `You are an E2E test generator that explores a frontend codebase as a BFS graph. You are ONE long-running agent that maintains all state about what's been explored and what hasn't.
5945
+ SYSTEM_PROMPT7 = `You are an E2E test generator that explores a frontend codebase as a BFS graph. You are ONE long-running agent that maintains all state about what's been explored and what hasn't.
5704
5946
 
5705
5947
  ## Your process
5706
5948
 
@@ -6131,11 +6373,11 @@ var init_validation = __esm({
6131
6373
  });
6132
6374
 
6133
6375
  // src/agents/05-test-generator/tools.ts
6134
- import { mkdir as mkdir3, writeFile as writeFile11 } from "fs/promises";
6135
- import { dirname as dirname3, join as join28 } from "path";
6136
- import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as tool18, ToolLoopAgent as ToolLoopAgent3 } from "ai";
6376
+ import { mkdir as mkdir3, writeFile as writeFile12 } from "fs/promises";
6377
+ import { dirname as dirname3, join as join29 } from "path";
6378
+ import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as tool19, ToolLoopAgent as ToolLoopAgent3 } from "ai";
6137
6379
  import matter5 from "gray-matter";
6138
- import { z as z22 } from "zod";
6380
+ import { z as z24 } from "zod";
6139
6381
  function findForbiddenPlaceholder(stepsSection) {
6140
6382
  const placeholderPatterns = [
6141
6383
  { pattern: /Dynamic:\s/gi, name: '"Dynamic:" placeholder' },
@@ -6153,13 +6395,13 @@ function findForbiddenPlaceholder(stepsSection) {
6153
6395
  return void 0;
6154
6396
  }
6155
6397
  function buildWriteTestTool(state, outputDir) {
6156
- return tool18({
6398
+ return tool19({
6157
6399
  description: "Write a test file to qa-tests/{folder}/{filename}.md. Validates frontmatter before writing. Returns error if frontmatter is invalid.",
6158
- inputSchema: z22.object({
6159
- folder: z22.string().describe("Subfolder name under qa-tests/"),
6160
- filename: z22.string().describe("File name (e.g. login-valid-credentials.md)"),
6161
- content: z22.string().describe("Full file content including YAML frontmatter"),
6162
- nodeId: z22.string().describe("The FeatureNode ID this test belongs to")
6400
+ inputSchema: z24.object({
6401
+ folder: z24.string().describe("Subfolder name under qa-tests/"),
6402
+ filename: z24.string().describe("File name (e.g. login-valid-credentials.md)"),
6403
+ content: z24.string().describe("Full file content including YAML frontmatter"),
6404
+ nodeId: z24.string().describe("The FeatureNode ID this test belongs to")
6163
6405
  }),
6164
6406
  execute: async (input) => {
6165
6407
  const frontmatter = extractFrontmatter(input.content);
@@ -6202,11 +6444,11 @@ function buildWriteTestTool(state, outputDir) {
6202
6444
  error: `Test steps contain ${placeholder.name}: "${placeholder.match}". Use EXACT values from scenarios.md - not placeholders or examples.`
6203
6445
  };
6204
6446
  }
6205
- const relPath = join28("qa-tests", input.folder, input.filename);
6206
- const absPath = join28(outputDir, relPath);
6447
+ const relPath = join29("qa-tests", input.folder, input.filename);
6448
+ const absPath = join29(outputDir, relPath);
6207
6449
  try {
6208
6450
  await mkdir3(dirname3(absPath), { recursive: true });
6209
- await writeFile11(absPath, input.content, "utf-8");
6451
+ await writeFile12(absPath, input.content, "utf-8");
6210
6452
  state.markTested(input.nodeId, [relPath]);
6211
6453
  await saveBfsState(outputDir, state);
6212
6454
  return { path: relPath, title: parsed.data.title };
@@ -6218,16 +6460,16 @@ function buildWriteTestTool(state, outputDir) {
6218
6460
  });
6219
6461
  }
6220
6462
  function buildCreateFolderTool(outputDir) {
6221
- return tool18({
6463
+ return tool19({
6222
6464
  description: "Create a folder under qa-tests/ for organizing tests.",
6223
- inputSchema: z22.object({
6224
- folder: z22.string().describe("Folder name (kebab-case)")
6465
+ inputSchema: z24.object({
6466
+ folder: z24.string().describe("Folder name (kebab-case)")
6225
6467
  }),
6226
6468
  execute: async (input) => {
6227
- const absPath = join28(outputDir, "qa-tests", input.folder);
6469
+ const absPath = join29(outputDir, "qa-tests", input.folder);
6228
6470
  try {
6229
6471
  await mkdir3(absPath, { recursive: true });
6230
- return { path: join28("qa-tests", input.folder) };
6472
+ return { path: join29("qa-tests", input.folder) };
6231
6473
  } catch (err) {
6232
6474
  const message = err instanceof Error ? err.message : String(err);
6233
6475
  return { error: `Failed to create folder: ${message}` };
@@ -6236,9 +6478,9 @@ function buildCreateFolderTool(outputDir) {
6236
6478
  });
6237
6479
  }
6238
6480
  function buildNextNodeTool(state, outputDir) {
6239
- return tool18({
6481
+ return tool19({
6240
6482
  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.",
6241
- inputSchema: z22.object({}),
6483
+ inputSchema: z24.object({}),
6242
6484
  execute: async () => {
6243
6485
  const next = state.nextNode();
6244
6486
  await saveBfsState(outputDir, state);
@@ -6265,9 +6507,9 @@ function buildNextNodeTool(state, outputDir) {
6265
6507
  });
6266
6508
  }
6267
6509
  function buildGetProgressTool(state) {
6268
- return tool18({
6510
+ return tool19({
6269
6511
  description: "Check how many nodes have been tested vs how many remain.",
6270
- inputSchema: z22.object({}),
6512
+ inputSchema: z24.object({}),
6271
6513
  execute: async () => {
6272
6514
  const stats = state.summary();
6273
6515
  const nodes = [...state.nodes.values()].map((n) => ({
@@ -6281,14 +6523,14 @@ function buildGetProgressTool(state) {
6281
6523
  });
6282
6524
  }
6283
6525
  function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
6284
- return tool18({
6526
+ return tool19({
6285
6527
  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.",
6286
- inputSchema: z22.object({
6287
- instruction: z22.string().describe("What to research - be specific about files and what to look for")
6528
+ inputSchema: z24.object({
6529
+ instruction: z24.string().describe("What to research - be specific about files and what to look for")
6288
6530
  }),
6289
6531
  execute: async (input) => {
6290
- const resultSchema2 = z22.object({
6291
- findings: z22.string().describe("Summary of what was found")
6532
+ const resultSchema2 = z24.object({
6533
+ findings: z24.string().describe("Summary of what was found")
6292
6534
  });
6293
6535
  let result;
6294
6536
  const subagent = new ToolLoopAgent3({
@@ -6300,7 +6542,7 @@ function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
6300
6542
  glob: buildGlobTool(workingDirectory),
6301
6543
  grep: buildGrepTool(workingDirectory),
6302
6544
  read_file: buildReadFileTool(workingDirectory),
6303
- finish: tool18({
6545
+ finish: tool19({
6304
6546
  description: "Report your findings.",
6305
6547
  inputSchema: resultSchema2,
6306
6548
  execute: async (output) => {
@@ -6342,14 +6584,14 @@ var init_tools2 = __esm({
6342
6584
  init_tools();
6343
6585
  init_graph();
6344
6586
  init_validation();
6345
- testFrontmatterSchema = z22.object({
6346
- title: z22.string().min(1),
6347
- description: z22.string().min(1),
6348
- intent: z22.string().min(30, "Intent must be at least 30 characters - describe the BEHAVIOR being tested, not the steps"),
6349
- criticality: z22.enum(["critical", "high", "mid", "low"]),
6350
- scenario: z22.string().min(1),
6351
- flow: z22.string().min(1),
6352
- verification: z22.string().min(
6587
+ testFrontmatterSchema = z24.object({
6588
+ title: z24.string().min(1),
6589
+ description: z24.string().min(1),
6590
+ intent: z24.string().min(30, "Intent must be at least 30 characters - describe the BEHAVIOR being tested, not the steps"),
6591
+ criticality: z24.enum(["critical", "high", "mid", "low"]),
6592
+ scenario: z24.string().min(1),
6593
+ flow: z24.string().min(1),
6594
+ verification: z24.string().min(
6353
6595
  20,
6354
6596
  "Verification must describe WHERE to navigate and WHAT to assert at the source of truth - not UI acknowledgments like toasts"
6355
6597
  )
@@ -6362,10 +6604,10 @@ var test_generator_exports = {};
6362
6604
  __export(test_generator_exports, {
6363
6605
  runTestGenerator: () => runTestGenerator
6364
6606
  });
6365
- import { mkdir as mkdir4, readFile as readFile21, rmdir, unlink, writeFile as writeFile12 } from "fs/promises";
6366
- import { basename as basename4, join as join29 } from "path";
6367
- import { tool as tool19 } from "ai";
6368
- import { z as z23 } from "zod";
6607
+ import { mkdir as mkdir4, readFile as readFile22, rmdir, unlink, writeFile as writeFile13 } from "fs/promises";
6608
+ import { basename as basename4, join as join30 } from "path";
6609
+ import { tool as tool20 } from "ai";
6610
+ import { z as z25 } from "zod";
6369
6611
  import { glob as glob6 } from "glob";
6370
6612
  async function preseedQueue(state, projectRoot, pages, features) {
6371
6613
  let seeded = 0;
@@ -6413,10 +6655,10 @@ async function runTestGenerator(input) {
6413
6655
  const existingState = await loadBfsState(input.outputDir);
6414
6656
  const state = existingState ?? new CoverageState();
6415
6657
  let result;
6416
- const finishTool = tool19({
6658
+ const finishTool = tool20({
6417
6659
  description: "Call when the BFS queue is empty and all routes have been explored.",
6418
- inputSchema: z23.object({
6419
- summary: z23.string().describe("Coverage summary")
6660
+ inputSchema: z25.object({
6661
+ summary: z25.string().describe("Coverage summary")
6420
6662
  }),
6421
6663
  execute: async (finishInput) => {
6422
6664
  const stats = state.summary();
@@ -6445,7 +6687,7 @@ async function runTestGenerator(input) {
6445
6687
  });
6446
6688
  let kbContext = "";
6447
6689
  try {
6448
- const autonomaMd = await readFile21(join29(input.outputDir, "AUTONOMA.md"), "utf-8");
6690
+ const autonomaMd = await readFile22(join30(input.outputDir, "AUTONOMA.md"), "utf-8");
6449
6691
  kbContext += `
6450
6692
  ## Knowledge Base (AUTONOMA.md)
6451
6693
 
@@ -6454,7 +6696,7 @@ ${autonomaMd}
6454
6696
  } catch {
6455
6697
  }
6456
6698
  try {
6457
- const scenariosMd = await readFile21(join29(input.outputDir, "scenarios.md"), "utf-8");
6699
+ const scenariosMd = await readFile22(join30(input.outputDir, "scenarios.md"), "utf-8");
6458
6700
  kbContext += `
6459
6701
  ## Scenarios
6460
6702
 
@@ -6508,7 +6750,7 @@ Do NOT try to finish early. Process EVERY node via next_node until it returns do
6508
6750
  const listDirectoryFn = await buildListDirectoryTool(input.projectRoot);
6509
6751
  const agentConfig = {
6510
6752
  id: "test-generator",
6511
- systemPrompt: SYSTEM_PROMPT6,
6753
+ systemPrompt: SYSTEM_PROMPT7,
6512
6754
  model,
6513
6755
  maxSteps: CHUNK_STEPS,
6514
6756
  temperature: 0.3,
@@ -6624,20 +6866,20 @@ IMPORTANT: Do NOT try to finish early. Process every node via next_node until it
6624
6866
  }
6625
6867
  console.log(` Fix pass complete`);
6626
6868
  }
6627
- const allTestFiles = await glob6(join29(input.outputDir, "qa-tests", "**/*.md"));
6869
+ const allTestFiles = await glob6(join30(input.outputDir, "qa-tests", "**/*.md"));
6628
6870
  let markedInvalid = 0;
6629
6871
  for (const testPath of allTestFiles) {
6630
6872
  if (basename4(testPath) === "INDEX.md") continue;
6631
6873
  if (testPath.includes("/_invalid/")) continue;
6632
- const content = await readFile21(testPath, "utf-8");
6874
+ const content = await readFile22(testPath, "utf-8");
6633
6875
  const validation = validateTestContent(content);
6634
6876
  if (!validation.valid) {
6635
- const invalidDir = join29(input.outputDir, "qa-tests", "_invalid");
6877
+ const invalidDir = join30(input.outputDir, "qa-tests", "_invalid");
6636
6878
  await mkdir4(invalidDir, { recursive: true });
6637
- const dest = join29(invalidDir, basename4(testPath));
6879
+ const dest = join30(invalidDir, basename4(testPath));
6638
6880
  const annotated = `<!-- VALIDATION ERRORS: ${validation.errors.join("; ")} -->
6639
6881
  ${content}`;
6640
- await writeFile12(dest, annotated, "utf-8");
6882
+ await writeFile13(dest, annotated, "utf-8");
6641
6883
  await unlink(testPath);
6642
6884
  markedInvalid++;
6643
6885
  }
@@ -6645,7 +6887,7 @@ ${content}`;
6645
6887
  if (markedInvalid > 0) {
6646
6888
  console.log(` ${markedInvalid} tests still invalid after review cycles - moved to _invalid/`);
6647
6889
  }
6648
- const dirs = await glob6(join29(input.outputDir, "qa-tests", "**/"), {
6890
+ const dirs = await glob6(join30(input.outputDir, "qa-tests", "**/"), {
6649
6891
  dot: false
6650
6892
  });
6651
6893
  for (const dir of dirs.sort((a, b) => b.length - a.length)) {
@@ -6737,7 +6979,7 @@ async function generateIndex(outputDir, state) {
6737
6979
  for (const paths of state.testsWritten.values()) {
6738
6980
  for (const p10 of paths) {
6739
6981
  try {
6740
- const content2 = await readFile21(join29(outputDir, p10), "utf-8");
6982
+ const content2 = await readFile22(join30(outputDir, p10), "utf-8");
6741
6983
  const critMatch = content2.match(/criticality:\s*(\w+)/);
6742
6984
  const critVal = critMatch?.[1] ?? "";
6743
6985
  if (critCounts.has(critVal)) critCounts.set(critVal, (critCounts.get(critVal) ?? 0) + 1);
@@ -6782,28 +7024,28 @@ ${folders.map((f) => `| ${f.name} | ${f.test_count} |`).join("\n")}
6782
7024
 
6783
7025
  ${[...testsByFolder.entries()].flatMap(([_folder, tests]) => tests.map((t) => `- \`${t}\``)).join("\n")}
6784
7026
  `;
6785
- await writeFile12(join29(outputDir, "qa-tests", "INDEX.md"), content, "utf-8");
7027
+ await writeFile13(join30(outputDir, "qa-tests", "INDEX.md"), content, "utf-8");
6786
7028
  }
6787
7029
  async function generateJourneyTests(outputDir, model, projectRoot) {
6788
7030
  const logger = createStepLogger("journeys", 50);
6789
7031
  let autonomaMd = "";
6790
7032
  let scenariosMd = "";
6791
7033
  try {
6792
- autonomaMd = await readFile21(join29(outputDir, "AUTONOMA.md"), "utf-8");
7034
+ autonomaMd = await readFile22(join30(outputDir, "AUTONOMA.md"), "utf-8");
6793
7035
  } catch (err) {
6794
7036
  debugLog("AUTONOMA.md not present for journey generation", { err });
6795
7037
  }
6796
7038
  try {
6797
- scenariosMd = await readFile21(join29(outputDir, "scenarios.md"), "utf-8");
7039
+ scenariosMd = await readFile22(join30(outputDir, "scenarios.md"), "utf-8");
6798
7040
  } catch (err) {
6799
7041
  debugLog("scenarios.md not present for journey generation", { err });
6800
7042
  }
6801
7043
  if (!autonomaMd) return 0;
6802
- const existingTests = await glob6(join29(outputDir, "qa-tests", "**/*.md"));
7044
+ const existingTests = await glob6(join30(outputDir, "qa-tests", "**/*.md"));
6803
7045
  const existingTitles = [];
6804
7046
  for (const t of existingTests) {
6805
7047
  if (basename4(t) === "INDEX.md") continue;
6806
- const content = await readFile21(t, "utf-8");
7048
+ const content = await readFile22(t, "utf-8");
6807
7049
  const titleMatch = content.match(/title:\s*"([^"]+)"/);
6808
7050
  if (titleMatch) existingTitles.push(titleMatch[1]);
6809
7051
  }
@@ -6846,9 +7088,9 @@ Write 5-8 journey tests using the write_test tool with folder "journeys". Then c
6846
7088
  status: "queued"
6847
7089
  });
6848
7090
  let journeyResult;
6849
- const journeyFinish = tool19({
7091
+ const journeyFinish = tool20({
6850
7092
  description: "Signal journey generation is complete.",
6851
- inputSchema: z23.object({ summary: z23.string() }),
7093
+ inputSchema: z25.object({ summary: z25.string() }),
6852
7094
  execute: async (finishInput) => {
6853
7095
  journeyResult = {
6854
7096
  success: true,
@@ -6860,7 +7102,7 @@ Write 5-8 journey tests using the write_test tool with folder "journeys". Then c
6860
7102
  });
6861
7103
  const config = {
6862
7104
  id: "journey-gen",
6863
- systemPrompt: SYSTEM_PROMPT6,
7105
+ systemPrompt: SYSTEM_PROMPT7,
6864
7106
  model,
6865
7107
  maxSteps: 50,
6866
7108
  temperature: 0.3,
@@ -6937,8 +7179,8 @@ function ensureSupportedNode() {
6937
7179
  ensureSupportedNode();
6938
7180
 
6939
7181
  // src/index.ts
6940
- import { readFile as readFile22, writeFile as writeFile13 } from "fs/promises";
6941
- import { join as join30 } from "path";
7182
+ import { readFile as readFile23, writeFile as writeFile14 } from "fs/promises";
7183
+ import { join as join31 } from "path";
6942
7184
  import * as p9 from "@clack/prompts";
6943
7185
 
6944
7186
  // src/config.ts
@@ -7018,6 +7260,8 @@ function loadConfig(args) {
7018
7260
  projectRoot,
7019
7261
  projectSlug,
7020
7262
  modelId: args.model ?? env.OPENROUTER_MODEL,
7263
+ frontend: args.frontend,
7264
+ backends: args.backends,
7021
7265
  databaseUrl: env.DATABASE_URL,
7022
7266
  sdkEndpointUrl: env.SDK_ENDPOINT_URL,
7023
7267
  sharedSecret: env.AUTONOMA_SHARED_SECRET,
@@ -7092,6 +7336,50 @@ function installInterruptHandler(opts) {
7092
7336
  return iface;
7093
7337
  });
7094
7338
  }
7339
+ function installTerminationDiagnostics() {
7340
+ const memSnapshot = () => {
7341
+ const m = process.memoryUsage();
7342
+ const mb = (n) => Math.round(n / 1024 / 1024);
7343
+ return `rss=${mb(m.rss)}MB heapUsed=${mb(m.heapUsed)}MB heapTotal=${mb(m.heapTotal)}MB`;
7344
+ };
7345
+ for (const signal of ["SIGTERM", "SIGHUP"]) {
7346
+ const forcedCode = signal === "SIGTERM" ? 143 : 129;
7347
+ process.on(signal, () => {
7348
+ process.stderr.write(`
7349
+ [diagnostics] received ${signal} - external termination (${memSnapshot()})
7350
+ `);
7351
+ terminateGracefully(forcedCode);
7352
+ });
7353
+ }
7354
+ process.on("uncaughtException", (err) => {
7355
+ const detail = err instanceof Error ? err.stack ?? err.message : String(err);
7356
+ process.stderr.write(`
7357
+ [diagnostics] uncaughtException (${memSnapshot()}): ${detail}
7358
+ `);
7359
+ restoreTerminal();
7360
+ process.exit(1);
7361
+ });
7362
+ process.on("unhandledRejection", (reason) => {
7363
+ const detail = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
7364
+ process.stderr.write(`
7365
+ [diagnostics] unhandledRejection (${memSnapshot()}): ${detail}
7366
+ `);
7367
+ restoreTerminal();
7368
+ process.exit(1);
7369
+ });
7370
+ }
7371
+ function terminateGracefully(forcedCode) {
7372
+ if (quitting || onExit == null) {
7373
+ restoreTerminal();
7374
+ process.exit(forcedCode);
7375
+ }
7376
+ quitting = true;
7377
+ setTimeout(() => {
7378
+ restoreTerminal();
7379
+ process.exit(forcedCode);
7380
+ }, FORCE_EXIT_MS).unref?.();
7381
+ onExit(forcedCode);
7382
+ }
7095
7383
  function restoreTerminal() {
7096
7384
  try {
7097
7385
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
@@ -7169,15 +7457,31 @@ async function loadGitInfo(outputDir) {
7169
7457
 
7170
7458
  // src/index.ts
7171
7459
  init_notify();
7460
+ init_project_map();
7172
7461
 
7173
7462
  // src/core/state.ts
7174
7463
  init_esm_shims();
7175
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
7176
- import { join as join8 } from "path";
7464
+ init_debug();
7465
+ import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
7466
+ import { join as join9 } from "path";
7467
+ import { z as z3 } from "zod";
7468
+ var StepStatusSchema = z3.enum(["pending", "running", "done", "failed", "paused"]);
7469
+ var PipelineStateSchema = z3.object({
7470
+ steps: z3.object({
7471
+ projectMapper: StepStatusSchema.default("done"),
7472
+ pagesFinder: StepStatusSchema.default("done"),
7473
+ kb: StepStatusSchema.default("done"),
7474
+ entityAudit: StepStatusSchema.default("done"),
7475
+ scenarioRecipe: StepStatusSchema.default("done"),
7476
+ recipeBuilder: StepStatusSchema.default("done"),
7477
+ testGenerator: StepStatusSchema.default("done")
7478
+ })
7479
+ });
7177
7480
  var STATE_FILE = ".pipeline-state.json";
7178
7481
  function initialState() {
7179
7482
  return {
7180
7483
  steps: {
7484
+ projectMapper: "pending",
7181
7485
  pagesFinder: "pending",
7182
7486
  kb: "pending",
7183
7487
  entityAudit: "pending",
@@ -7188,18 +7492,19 @@ function initialState() {
7188
7492
  };
7189
7493
  }
7190
7494
  async function loadState(outputDir) {
7191
- const path3 = join8(outputDir, STATE_FILE);
7495
+ const path3 = join9(outputDir, STATE_FILE);
7192
7496
  try {
7193
- const raw = await readFile3(path3, "utf-8");
7194
- const parsed = JSON.parse(raw);
7195
- return parsed;
7196
- } catch {
7497
+ const raw = await readFile4(path3, "utf-8");
7498
+ return PipelineStateSchema.parse(JSON.parse(raw));
7499
+ } catch (err) {
7500
+ const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
7501
+ if (!isMissingFile) debugLog("Failed to load pipeline state, starting fresh", { path: path3, err });
7197
7502
  return initialState();
7198
7503
  }
7199
7504
  }
7200
7505
  async function saveState(outputDir, state) {
7201
- const path3 = join8(outputDir, STATE_FILE);
7202
- await writeFile3(path3, JSON.stringify(state, null, 2), "utf-8");
7506
+ const path3 = join9(outputDir, STATE_FILE);
7507
+ await writeFile4(path3, JSON.stringify(state, null, 2), "utf-8");
7203
7508
  }
7204
7509
  async function markStep(outputDir, state, step, status) {
7205
7510
  const updated = {
@@ -7210,15 +7515,23 @@ async function markStep(outputDir, state, step, status) {
7210
7515
  return updated;
7211
7516
  }
7212
7517
  function nextPendingStep(state) {
7213
- const order = ["pagesFinder", "kb", "entityAudit", "scenarioRecipe", "recipeBuilder", "testGenerator"];
7518
+ const order = [
7519
+ "projectMapper",
7520
+ "pagesFinder",
7521
+ "kb",
7522
+ "entityAudit",
7523
+ "scenarioRecipe",
7524
+ "recipeBuilder",
7525
+ "testGenerator"
7526
+ ];
7214
7527
  return order.find((s) => state.steps[s] !== "done") ?? void 0;
7215
7528
  }
7216
7529
 
7217
7530
  // src/core/upload.ts
7218
7531
  init_esm_shims();
7219
7532
  init_debug();
7220
- import { readFile as readFile4 } from "fs/promises";
7221
- import { basename, join as join9, relative } from "path";
7533
+ import { readFile as readFile5 } from "fs/promises";
7534
+ import { basename, join as join10, relative } from "path";
7222
7535
  import * as p from "@clack/prompts";
7223
7536
  import { glob } from "glob";
7224
7537
  var ARTIFACT_FILES = ["AUTONOMA.md", "scenarios.md", "entity-audit.md"];
@@ -7226,7 +7539,7 @@ async function readArtifacts(outputDir) {
7226
7539
  const files = [];
7227
7540
  for (const name of ARTIFACT_FILES) {
7228
7541
  try {
7229
- const content = await readFile4(join9(outputDir, name), "utf-8");
7542
+ const content = await readFile5(join10(outputDir, name), "utf-8");
7230
7543
  files.push({ name, content });
7231
7544
  } catch (err) {
7232
7545
  debugLog(`Artifact ${name} not on disk; skipping upload`, { err });
@@ -7235,13 +7548,13 @@ async function readArtifacts(outputDir) {
7235
7548
  return files;
7236
7549
  }
7237
7550
  async function readTestCases(outputDir) {
7238
- const testsDir = join9(outputDir, "qa-tests");
7551
+ const testsDir = join10(outputDir, "qa-tests");
7239
7552
  const matches = await glob("**/*.md", { cwd: testsDir, nodir: true });
7240
7553
  const files = [];
7241
7554
  for (const match of matches) {
7242
7555
  const name = basename(match);
7243
7556
  if (name === "INDEX.md") continue;
7244
- const content = await readFile4(join9(testsDir, match), "utf-8");
7557
+ const content = await readFile5(join10(testsDir, match), "utf-8");
7245
7558
  const folderPath = relative(".", match).split("/").slice(0, -1).join("/");
7246
7559
  files.push({ name, content, folder: folderPath.length > 0 ? folderPath : void 0 });
7247
7560
  }
@@ -7303,11 +7616,11 @@ process.setSourceMapsEnabled(true);
7303
7616
  var PAGES_FILE = "pages.json";
7304
7617
  async function savePages(outputDir, pages) {
7305
7618
  const obj = Object.fromEntries(pages);
7306
- await writeFile13(join30(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
7619
+ await writeFile14(join31(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
7307
7620
  }
7308
7621
  async function loadPages(outputDir) {
7309
7622
  try {
7310
- const raw = await readFile22(join30(outputDir, PAGES_FILE), "utf-8");
7623
+ const raw = await readFile23(join31(outputDir, PAGES_FILE), "utf-8");
7311
7624
  const obj = JSON.parse(raw);
7312
7625
  return new Map(Object.entries(obj));
7313
7626
  } catch {
@@ -7336,6 +7649,7 @@ function strArg(args, key) {
7336
7649
  return typeof value === "string" ? value : void 0;
7337
7650
  }
7338
7651
  var STEP_LABELS = {
7652
+ projectMapper: "Map your project structure",
7339
7653
  pagesFinder: "Find your pages",
7340
7654
  kb: "Build a knowledge base",
7341
7655
  entityAudit: "Map your data models",
@@ -7347,6 +7661,7 @@ function isStepName(value) {
7347
7661
  return value in STEP_LABELS;
7348
7662
  }
7349
7663
  var STEP_SUMMARIES = {
7664
+ projectMapper: "Identify your frontend(s), backend(s), and which folders to ignore.",
7350
7665
  pagesFinder: "Map every page and route in your app.",
7351
7666
  kb: "Learn your app's features, flows, and UI patterns.",
7352
7667
  entityAudit: "Find what your app stores (users, orgs, ...) and how each one is created.",
@@ -7355,6 +7670,7 @@ var STEP_SUMMARIES = {
7355
7670
  testGenerator: "Write the end-to-end tests, covering every page and feature."
7356
7671
  };
7357
7672
  var STEP_INTROS = {
7673
+ projectMapper: "Looking at how your codebase is laid out - which folder(s) are the frontend, which are the backend/data layer, and which are unrelated - so every later step scans only what matters instead of the whole repo.",
7358
7674
  pagesFinder: "Scanning your codebase to find every page and route, so we know the full surface area that needs test coverage.",
7359
7675
  kb: "Reading those pages to learn your app's features, flows, and UI patterns - the context everything after this builds on.",
7360
7676
  entityAudit: "Finding the things your app stores (users, organizations, orders, ...) and how each one gets created, so we can generate realistic test data for them.",
@@ -7362,6 +7678,32 @@ var STEP_INTROS = {
7362
7678
  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.",
7363
7679
  testGenerator: "Writing the actual end-to-end tests, covering every page and feature with depth proportional to its complexity."
7364
7680
  };
7681
+ async function resolveScopeSelection(map, config, nonInteractive) {
7682
+ if (config.frontend != null) {
7683
+ const requestedFrontend = resolveSelection(map, { frontend: config.frontend, backends: [] }).frontend;
7684
+ const backends = config.backends ?? defaultBackendsFor(map, requestedFrontend);
7685
+ return resolveSelection(map, { frontend: config.frontend, backends });
7686
+ }
7687
+ if (!nonInteractive) return promptScopeSelection(map);
7688
+ return pickDefaultSelection(map);
7689
+ }
7690
+ async function promptScopeSelection(map) {
7691
+ const frontend = await p9.select({
7692
+ message: "Which frontend do you want to plan tests for?",
7693
+ options: map.frontends.map((f) => ({ value: f.path, label: `${f.path} [${f.framework}]`, hint: f.why }))
7694
+ });
7695
+ if (p9.isCancel(frontend)) throw new Error("Cancelled");
7696
+ if (map.backends.length === 0) return { frontend, backends: [] };
7697
+ const needed = defaultBackendsFor(map, frontend);
7698
+ const backends = await p9.multiselect({
7699
+ message: "Which backends does it need? (pre-checked: the ones it depends on)",
7700
+ options: map.backends.map((b) => ({ value: b.path, label: `${b.path} [${b.framework}]`, hint: b.why })),
7701
+ initialValues: needed,
7702
+ required: false
7703
+ });
7704
+ if (p9.isCancel(backends)) throw new Error("Cancelled");
7705
+ return { frontend, backends };
7706
+ }
7365
7707
  async function runStep(step, outputDir, state, config, projectContext, nonInteractive, retryGuidance) {
7366
7708
  const label = STEP_LABELS[step];
7367
7709
  p9.note(STEP_INTROS[step], `Step: ${label}`);
@@ -7377,13 +7719,52 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
7377
7719
  try {
7378
7720
  let result;
7379
7721
  switch (step) {
7722
+ case "projectMapper": {
7723
+ const { runProjectMapper: runProjectMapper2 } = await Promise.resolve().then(() => (init_project_mapper(), project_mapper_exports));
7724
+ const map = await runProjectMapper2({
7725
+ projectRoot: config.projectRoot,
7726
+ outputDir,
7727
+ modelId: config.modelId,
7728
+ nonInteractive
7729
+ });
7730
+ if (map == null) {
7731
+ result = { success: false, artifacts: [], summary: "Project mapper did not produce a map." };
7732
+ break;
7733
+ }
7734
+ if (map.frontends.length === 0) {
7735
+ result = {
7736
+ success: false,
7737
+ artifacts: [],
7738
+ summary: "Project mapper found no frontend to test. Point --project at a codebase with a UI."
7739
+ };
7740
+ break;
7741
+ }
7742
+ const selection = await resolveScopeSelection(map, config, nonInteractive);
7743
+ if (selection == null) {
7744
+ await saveProjectMap(outputDir, map);
7745
+ p9.note(renderProjectMap(map), "Project map - candidates (pick one frontend + its backends)");
7746
+ result = {
7747
+ success: false,
7748
+ paused: true,
7749
+ artifacts: [],
7750
+ summary: `Found ${map.frontends.length} candidate frontends. Choose one and its backends, then resume with --frontend <path> --backends <path,path>.`
7751
+ };
7752
+ break;
7753
+ }
7754
+ const scoped = applySelection(map, selection);
7755
+ await saveProjectMap(outputDir, scoped);
7756
+ p9.note(renderProjectMap(scoped), "Project map");
7757
+ break;
7758
+ }
7380
7759
  case "pagesFinder": {
7381
7760
  const { runPageFinder: runPageFinder2 } = await Promise.resolve().then(() => (init_pages_finder(), pages_finder_exports));
7761
+ const projectMap = await loadProjectMap(outputDir);
7382
7762
  const pages = await runPageFinder2({
7383
7763
  projectRoot: config.projectRoot,
7384
7764
  outputDir,
7385
7765
  modelId: config.modelId,
7386
- nonInteractive
7766
+ nonInteractive,
7767
+ extraMessage: projectMap != null ? formatFrontendScope(projectMap) : void 0
7387
7768
  });
7388
7769
  await savePages(outputDir, pages);
7389
7770
  break;
@@ -7402,18 +7783,21 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
7402
7783
  }
7403
7784
  case "entityAudit": {
7404
7785
  const { runEntityAudit: runEntityAudit2 } = await Promise.resolve().then(() => (init_entity_audit(), entity_audit_exports));
7786
+ const auditMap = await loadProjectMap(outputDir);
7405
7787
  result = await runEntityAudit2({
7406
7788
  projectRoot: config.projectRoot,
7407
7789
  outputDir,
7408
7790
  modelId: config.modelId,
7409
7791
  projectContext,
7410
7792
  nonInteractive,
7411
- retryGuidance
7793
+ retryGuidance,
7794
+ scopeHint: auditMap != null ? formatBackendScope(auditMap) : void 0
7412
7795
  });
7413
7796
  break;
7414
7797
  }
7415
7798
  case "scenarioRecipe": {
7416
7799
  const { runScenarioRecipe: runScenarioRecipe2 } = await Promise.resolve().then(() => (init_scenario_recipe(), scenario_recipe_exports));
7800
+ const recipeMap = await loadProjectMap(outputDir);
7417
7801
  result = await runScenarioRecipe2({
7418
7802
  projectRoot: config.projectRoot,
7419
7803
  outputDir,
@@ -7421,7 +7805,8 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
7421
7805
  config,
7422
7806
  projectContext,
7423
7807
  nonInteractive,
7424
- retryGuidance
7808
+ retryGuidance,
7809
+ scopeHint: recipeMap != null ? formatBackendScope(recipeMap) : void 0
7425
7810
  });
7426
7811
  break;
7427
7812
  }
@@ -7574,6 +7959,7 @@ async function gatherProjectContext() {
7574
7959
  };
7575
7960
  }
7576
7961
  async function main() {
7962
+ installTerminationDiagnostics();
7577
7963
  const args = parseArgs(process.argv.slice(2));
7578
7964
  const command = process.argv[2];
7579
7965
  if (command === "status") {
@@ -7591,7 +7977,7 @@ async function main() {
7591
7977
  if (command === "help" || args.help) {
7592
7978
  console.log("Usage:");
7593
7979
  console.log(
7594
- " test-planner [run] [--project <path>] [--model <id>] [--step <name>] [--resume] [--non-interactive]"
7980
+ " test-planner [run] [--project <path>] [--frontend <path>] [--backends <path,path>] [--model <id>] [--step <name>] [--resume] [--non-interactive]"
7595
7981
  );
7596
7982
  console.log(" test-planner status [--project <path>]");
7597
7983
  console.log("");
@@ -7602,19 +7988,25 @@ async function main() {
7602
7988
  p9.intro("Let's generate your test suite");
7603
7989
  const resumeCommand = `autonoma-planner --resume` + (args.project ? ` --project ${args.project}` : "");
7604
7990
  installInterruptHandler({
7605
- onExit: () => {
7991
+ // exitCode defaults to 0 for a user-initiated Ctrl+C (progress saved, a clean stop);
7992
+ // an external SIGTERM/SIGHUP passes the conventional 143/129 so the flushed exit still
7993
+ // carries the signal code a reaper/CI reads, not a 0 that looks like normal completion.
7994
+ onExit: (exitCode = 0) => {
7606
7995
  track("cli_run_exited");
7607
7996
  restoreTerminal();
7608
7997
  console.log("");
7609
7998
  p9.log.warn(`Your progress is saved. To resume, run:
7610
7999
  ${resumeCommand}`);
7611
- void flushAnalytics().finally(() => process.exit(0));
8000
+ void flushAnalytics().finally(() => process.exit(exitCode));
7612
8001
  }
7613
8002
  });
8003
+ const backendsArg = strArg(args, "backends");
7614
8004
  const config = loadConfig({
7615
8005
  project: strArg(args, "project"),
7616
8006
  model: strArg(args, "model"),
7617
- slug: strArg(args, "slug")
8007
+ slug: strArg(args, "slug"),
8008
+ frontend: strArg(args, "frontend"),
8009
+ backends: backendsArg != null ? backendsArg.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0
7618
8010
  });
7619
8011
  if (!ensureAutonomaAuth()) {
7620
8012
  return;
@@ -7705,12 +8097,20 @@ or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
7705
8097
  p9.outro("Done");
7706
8098
  return;
7707
8099
  }
7708
- const startStep = isResuming ? nextPendingStep(state) : "pagesFinder";
8100
+ const startStep = isResuming ? nextPendingStep(state) : "projectMapper";
7709
8101
  if (!startStep) {
7710
8102
  p9.log.success("All steps complete.");
7711
8103
  return;
7712
8104
  }
7713
- const steps = ["pagesFinder", "kb", "entityAudit", "scenarioRecipe", "recipeBuilder", "testGenerator"];
8105
+ const steps = [
8106
+ "projectMapper",
8107
+ "pagesFinder",
8108
+ "kb",
8109
+ "entityAudit",
8110
+ "scenarioRecipe",
8111
+ "recipeBuilder",
8112
+ "testGenerator"
8113
+ ];
7714
8114
  const startIdx = steps.indexOf(startStep);
7715
8115
  p9.note(steps.map((s, idx) => `${idx + 1}. ${STEP_LABELS[s]} - ${STEP_SUMMARIES[s]}`).join("\n"), "Here's the plan");
7716
8116
  try {