@autonoma-ai/planner 0.1.19 → 0.1.21

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
@@ -23,6 +23,202 @@ var init_esm_shims = __esm({
23
23
  }
24
24
  });
25
25
 
26
+ // src/core/to-record.ts
27
+ function toRecord(value) {
28
+ if (typeof value !== "object" || value === null) return {};
29
+ const record = {};
30
+ for (const [key, entry] of Object.entries(value)) {
31
+ record[key] = entry;
32
+ }
33
+ return record;
34
+ }
35
+ var init_to_record = __esm({
36
+ "src/core/to-record.ts"() {
37
+ "use strict";
38
+ init_esm_shims();
39
+ }
40
+ });
41
+
42
+ // src/agents/04-recipe-builder/recipe.ts
43
+ import { readFile, writeFile } from "fs/promises";
44
+ import { join } from "path";
45
+ function collectRefs(value, out) {
46
+ if (Array.isArray(value)) {
47
+ for (const v of value) collectRefs(v, out);
48
+ } else if (value !== null && typeof value === "object") {
49
+ const obj = toRecord(value);
50
+ if (typeof obj._ref === "string") out.add(obj._ref);
51
+ for (const v of Object.values(obj)) collectRefs(v, out);
52
+ }
53
+ }
54
+ function buildSingleEntityRecipe(entityName, models, entityOrder, allEntities) {
55
+ const modelMap = new Map(models.map((m) => [m.name, m]));
56
+ const aliasOwner = /* @__PURE__ */ new Map();
57
+ for (const [name, entity] of Object.entries(allEntities)) {
58
+ for (const rec of entity?.recipeData ?? []) {
59
+ if (typeof rec._alias === "string") aliasOwner.set(rec._alias, name);
60
+ }
61
+ }
62
+ const recipe = {};
63
+ const done = /* @__PURE__ */ new Set();
64
+ const onStack = /* @__PURE__ */ new Set();
65
+ function include(name) {
66
+ if (done.has(name) || onStack.has(name)) return;
67
+ onStack.add(name);
68
+ const records = allEntities[name]?.recipeData ?? [];
69
+ for (const dep of modelMap.get(name)?.created_by ?? []) {
70
+ if (entityOrder.includes(dep.owner)) include(dep.owner);
71
+ }
72
+ const refs = /* @__PURE__ */ new Set();
73
+ collectRefs(records, refs);
74
+ for (const alias of refs) {
75
+ const owner = aliasOwner.get(alias);
76
+ if (owner && owner !== name) include(owner);
77
+ }
78
+ onStack.delete(name);
79
+ done.add(name);
80
+ if (records.length > 0) recipe[name] = records;
81
+ }
82
+ include(entityName);
83
+ return recipe;
84
+ }
85
+ function buildFullRecipe(entityOrder, allEntities) {
86
+ const recipe = {};
87
+ for (const name of entityOrder) {
88
+ const entity = allEntities[name];
89
+ if (entity?.recipeData && entity.recipeData.length > 0) {
90
+ recipe[name] = entity.recipeData;
91
+ }
92
+ }
93
+ return recipe;
94
+ }
95
+ function buildSubmittableRecipe(create, description) {
96
+ return {
97
+ version: 1,
98
+ source: {
99
+ discoverPath: "discover.json",
100
+ scenariosPath: "scenarios.md"
101
+ },
102
+ validationMode: "endpoint-lifecycle",
103
+ recipes: [
104
+ {
105
+ name: "standard",
106
+ description,
107
+ create,
108
+ validation: {
109
+ status: "validated",
110
+ method: "endpoint-up-down"
111
+ }
112
+ }
113
+ ]
114
+ };
115
+ }
116
+ async function saveRecipe(outputDir, recipe) {
117
+ await writeFile(join(outputDir, RECIPE_FILE), JSON.stringify(recipe, null, 2), "utf-8");
118
+ }
119
+ async function loadRecipe(outputDir) {
120
+ try {
121
+ const raw = await readFile(join(outputDir, RECIPE_FILE), "utf-8");
122
+ const parsed = JSON.parse(raw);
123
+ return parsed;
124
+ } catch {
125
+ return void 0;
126
+ }
127
+ }
128
+ var RECIPE_FILE;
129
+ var init_recipe = __esm({
130
+ "src/agents/04-recipe-builder/recipe.ts"() {
131
+ "use strict";
132
+ init_esm_shims();
133
+ init_to_record();
134
+ RECIPE_FILE = "recipe.json";
135
+ }
136
+ });
137
+
138
+ // src/agents/04-recipe-builder/phases/submit.ts
139
+ import * as p from "@clack/prompts";
140
+ async function runSubmit(state, outputDir, autonomaApiUrl, autonomaApiToken, autonomaGenerationId) {
141
+ const fullCreate = buildFullRecipe(state.entityOrder, state.entities);
142
+ const recipe = buildSubmittableRecipe(fullCreate, "Standard test scenario with realistic data");
143
+ await saveRecipe(outputDir, recipe);
144
+ p.log.success(`Recipe saved to ${RECIPE_FILE2}`);
145
+ const uploaded = await submitRecipe(recipe, {
146
+ apiUrl: autonomaApiUrl,
147
+ apiToken: autonomaApiToken,
148
+ generationId: autonomaGenerationId
149
+ });
150
+ return { recipePath: RECIPE_FILE2, uploaded };
151
+ }
152
+ async function uploadRecipeFromDisk(outputDir, creds) {
153
+ const recipe = await loadRecipe(outputDir);
154
+ if (recipe == null) {
155
+ p.log.error(
156
+ `No ${RECIPE_FILE2} found in ${outputDir}. Run the planner's recipe step first to generate it, then retry.`
157
+ );
158
+ return false;
159
+ }
160
+ return submitRecipe(recipe, creds);
161
+ }
162
+ async function submitRecipe(recipe, creds) {
163
+ const { apiUrl, apiToken, generationId } = creds;
164
+ if (!apiUrl || !apiToken || !generationId) {
165
+ p.log.info(
166
+ "Autonoma API credentials not configured - recipe saved locally, not uploaded. Set AUTONOMA_API_URL, AUTONOMA_API_TOKEN and AUTONOMA_GENERATION_ID, then run `" + UPLOAD_COMMAND + "`."
167
+ );
168
+ return false;
169
+ }
170
+ const url = `${apiUrl.replace(/\/+$/, "")}/v1/setup/setups/${generationId}/scenario-recipe-versions`;
171
+ p.log.step("Submitting recipe to Autonoma...");
172
+ let res;
173
+ try {
174
+ res = await fetch(url, {
175
+ method: "POST",
176
+ headers: {
177
+ "Content-Type": "application/json",
178
+ Authorization: `Bearer ${apiToken}`
179
+ },
180
+ body: JSON.stringify(recipe)
181
+ });
182
+ } catch (err) {
183
+ p.log.error(`Recipe submission failed (network error): ${err instanceof Error ? err.message : String(err)}`);
184
+ printRecipeForRecovery(recipe);
185
+ return false;
186
+ }
187
+ if (res.ok) {
188
+ p.log.success(`Recipe submitted successfully (HTTP ${res.status})`);
189
+ return true;
190
+ }
191
+ const text6 = await res.text();
192
+ p.log.error(`Recipe submission failed (HTTP ${res.status}): ${text6}`);
193
+ printRecipeForRecovery(recipe);
194
+ return false;
195
+ }
196
+ function printRecipeForRecovery(recipe) {
197
+ console.log(
198
+ [
199
+ "",
200
+ "\u2500".repeat(72),
201
+ "RECIPE NOT UPLOADED - copy the JSON below into a recipe.json and re-upload with:",
202
+ ` ${UPLOAD_COMMAND}`,
203
+ "(with the same AUTONOMA_API_URL / AUTONOMA_API_TOKEN / AUTONOMA_GENERATION_ID env vars set)",
204
+ "\u2500".repeat(72),
205
+ JSON.stringify(recipe, null, 2),
206
+ "\u2500".repeat(72),
207
+ ""
208
+ ].join("\n")
209
+ );
210
+ }
211
+ var RECIPE_FILE2, UPLOAD_COMMAND;
212
+ var init_submit = __esm({
213
+ "src/agents/04-recipe-builder/phases/submit.ts"() {
214
+ "use strict";
215
+ init_esm_shims();
216
+ init_recipe();
217
+ RECIPE_FILE2 = "recipe.json";
218
+ UPLOAD_COMMAND = "npx @autonoma-ai/planner@latest upload";
219
+ }
220
+ });
221
+
26
222
  // src/env.ts
27
223
  import { createEnv } from "@t3-oss/env-core";
28
224
  import { z } from "zod";
@@ -82,14 +278,14 @@ var init_debug = __esm({
82
278
 
83
279
  // src/core/version.ts
84
280
  import { readFileSync as readFileSync3 } from "fs";
85
- import { dirname, join as join3 } from "path";
281
+ import { dirname, join as join4 } from "path";
86
282
  import { fileURLToPath as fileURLToPath2 } from "url";
87
283
  function resolveVersion() {
88
284
  try {
89
285
  const here = dirname(fileURLToPath2(import.meta.url));
90
286
  for (const rel of ["../package.json", "../../package.json", "../../../package.json"]) {
91
287
  try {
92
- const pkg = JSON.parse(readFileSync3(join3(here, rel), "utf-8"));
288
+ const pkg = JSON.parse(readFileSync3(join4(here, rel), "utf-8"));
93
289
  if (pkg?.name === PACKAGE_NAME && typeof pkg.version === "string") {
94
290
  return pkg.version;
95
291
  }
@@ -114,7 +310,7 @@ var init_version = __esm({
114
310
  import { randomUUID } from "crypto";
115
311
  import { readFileSync as readFileSync4, writeFileSync, mkdirSync } from "fs";
116
312
  import { homedir as homedir2 } from "os";
117
- import { join as join4 } from "path";
313
+ import { join as join5 } from "path";
118
314
  function resolveKey() {
119
315
  return (readEnv().AUTONOMA_POSTHOG_KEY ?? POSTHOG_PUBLIC_KEY).trim();
120
316
  }
@@ -199,7 +395,7 @@ function trackError(error, properties = {}, handled = true) {
199
395
  }
200
396
  async function flushAnalytics(timeoutMs = 1500) {
201
397
  if (pending.size === 0) return;
202
- await Promise.race([Promise.allSettled([...pending]), new Promise((resolve5) => setTimeout(resolve5, timeoutMs))]);
398
+ await Promise.race([Promise.allSettled([...pending]), new Promise((resolve6) => setTimeout(resolve6, timeoutMs))]);
203
399
  }
204
400
  var AUTONOMA_HOME2, DEVICE_ID_PATH, POSTHOG_PUBLIC_KEY, DEFAULT_HOST, RUN_ID, cachedDeviceId, enabled, pending;
205
401
  var init_analytics = __esm({
@@ -209,8 +405,8 @@ var init_analytics = __esm({
209
405
  init_env();
210
406
  init_debug();
211
407
  init_version();
212
- AUTONOMA_HOME2 = join4(homedir2(), ".autonoma");
213
- DEVICE_ID_PATH = join4(AUTONOMA_HOME2, ".device-id");
408
+ AUTONOMA_HOME2 = join5(homedir2(), ".autonoma");
409
+ DEVICE_ID_PATH = join5(AUTONOMA_HOME2, ".device-id");
214
410
  POSTHOG_PUBLIC_KEY = "phc_mUOwUj62r8vyiisFPvXLC3G5RftETIBMnKNSHqTBdka";
215
411
  DEFAULT_HOST = "https://us.i.posthog.com";
216
412
  RUN_ID = randomUUID();
@@ -242,14 +438,14 @@ var init_colors = __esm({
242
438
  });
243
439
 
244
440
  // src/core/context.ts
245
- import { readFile, writeFile } from "fs/promises";
246
- import { join as join5 } from "path";
441
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
442
+ import { join as join6 } from "path";
247
443
  async function saveContext(outputDir, ctx) {
248
- await writeFile(join5(outputDir, CONTEXT_FILE), JSON.stringify(ctx, null, 2), "utf-8");
444
+ await writeFile2(join6(outputDir, CONTEXT_FILE), JSON.stringify(ctx, null, 2), "utf-8");
249
445
  }
250
446
  async function loadContext(outputDir) {
251
447
  try {
252
- const raw = await readFile(join5(outputDir, CONTEXT_FILE), "utf-8");
448
+ const raw = await readFile2(join6(outputDir, CONTEXT_FILE), "utf-8");
253
449
  const parsed = JSON.parse(raw);
254
450
  return parsed;
255
451
  } catch {
@@ -288,7 +484,7 @@ var init_context = __esm({
288
484
  // src/core/errors.ts
289
485
  import { APICallError, RetryError, LoadAPIKeyError, InvalidPromptError, NoSuchModelError } from "ai";
290
486
  function sleep(ms) {
291
- return new Promise((resolve5) => setTimeout(resolve5, ms));
487
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
292
488
  }
293
489
  function isUserCancellation(err) {
294
490
  return err instanceof Error && /\bcancell?ed\b/i.test(err.message);
@@ -492,6 +688,151 @@ var init_notify = __esm({
492
688
  }
493
689
  });
494
690
 
691
+ // src/core/project-map.ts
692
+ import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
693
+ import { join as join9 } from "path";
694
+ import { z as z2 } from "zod";
695
+ async function saveProjectMap(outputDir, map) {
696
+ await writeFile4(join9(outputDir, PROJECT_MAP_FILE), JSON.stringify(map, null, 2), "utf-8");
697
+ }
698
+ async function loadProjectMap(outputDir) {
699
+ const path3 = join9(outputDir, PROJECT_MAP_FILE);
700
+ try {
701
+ const raw = await readFile4(path3, "utf-8");
702
+ const parsed = ProjectMapSchema.safeParse(JSON.parse(raw));
703
+ if (parsed.success) return parsed.data;
704
+ debugLog("project-map.json failed schema validation, ignoring it", { path: path3, issues: parsed.error.issues });
705
+ return void 0;
706
+ } catch (err) {
707
+ const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
708
+ if (!isMissingFile) debugLog("Failed to read project-map.json, ignoring it", { path: path3, err });
709
+ return void 0;
710
+ }
711
+ }
712
+ function renderProjectMap(map) {
713
+ const section = (title, rows, fmt) => {
714
+ if (rows.length === 0) return `${title}: (none)`;
715
+ return `${title}:
716
+ ` + rows.map((r) => ` - ${fmt(r)}`).join("\n");
717
+ };
718
+ return [
719
+ section("Frontend(s)", map.frontends, (f) => {
720
+ const deps = f.dependsOn.length > 0 ? ` needs: ${f.dependsOn.join(", ")}` : "";
721
+ return `${f.path} [${f.framework}]${deps} - ${f.why}`;
722
+ }),
723
+ section("Backend(s)", map.backends, (b) => {
724
+ const dl = b.dataLayer != null ? ` data: ${b.dataLayer.kind}${b.dataLayer.schemaPath != null ? ` @ ${b.dataLayer.schemaPath}` : ""}` : "";
725
+ return `${b.path} [${b.language}/${b.framework}]${dl} - ${b.why}`;
726
+ }),
727
+ section("Ignoring", map.ignore, (i) => `${i.path} - ${i.why}`)
728
+ ].join("\n\n");
729
+ }
730
+ function formatFrontendScope(map) {
731
+ if (map.frontends.length === 0) return void 0;
732
+ const fronts = map.frontends.map((f) => f.path).join(", ");
733
+ const ignore = map.ignore.map((i) => i.path);
734
+ const ignoreLine = ignore.length > 0 ? ` Do NOT spend time in these irrelevant directories: ${ignore.join(", ")}.` : "";
735
+ return `The project has already been mapped. The frontend surface to focus on is: ${fronts}. Confine your exploration to that surface.${ignoreLine}`;
736
+ }
737
+ function relatedPath(a, b) {
738
+ return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
739
+ }
740
+ function matchMapPath(requested, candidates) {
741
+ if (candidates.includes(requested)) return requested;
742
+ return candidates.find((c) => relatedPath(c, requested));
743
+ }
744
+ function matchBackend(requested, backends) {
745
+ const byPath = backends.find((b) => relatedPath(b.path, requested));
746
+ if (byPath != null) return byPath.path;
747
+ const bySchema = backends.find(
748
+ (b) => b.dataLayer?.schemaPath != null && relatedPath(b.dataLayer.schemaPath, requested)
749
+ );
750
+ return bySchema?.path;
751
+ }
752
+ function resolveSelection(map, requested) {
753
+ const frontendPaths = map.frontends.map((f) => f.path);
754
+ return {
755
+ frontend: matchMapPath(requested.frontend, frontendPaths) ?? requested.frontend,
756
+ backends: requested.backends.map((b) => matchBackend(b, map.backends) ?? b)
757
+ };
758
+ }
759
+ function defaultBackendsFor(map, frontendPath) {
760
+ const declared = map.frontends.find((f) => f.path === frontendPath)?.dependsOn ?? [];
761
+ const resolved = declared.map((dep) => matchBackend(dep, map.backends)).filter((p10) => p10 != null);
762
+ return [...new Set(resolved)];
763
+ }
764
+ function pickDefaultSelection(map) {
765
+ if (map.frontends.length !== 1) return void 0;
766
+ const [only] = map.frontends;
767
+ if (only == null) return void 0;
768
+ return { frontend: only.path, backends: defaultBackendsFor(map, only.path) };
769
+ }
770
+ function applySelection(map, selection) {
771
+ const frontend = map.frontends.find((f) => f.path === selection.frontend);
772
+ if (frontend == null) {
773
+ const options = map.frontends.map((f) => f.path).join(", ") || "(none)";
774
+ throw new Error(`Selected frontend "${selection.frontend}" is not in the map. Candidates: ${options}`);
775
+ }
776
+ const chosenBackends = map.backends.filter((b) => selection.backends.includes(b.path));
777
+ const missing = selection.backends.filter((p10) => !map.backends.some((b) => b.path === p10));
778
+ if (missing.length > 0) {
779
+ const options = map.backends.map((b) => b.path).join(", ") || "(none)";
780
+ throw new Error(`Selected backend(s) not in the map: ${missing.join(", ")}. Candidates: ${options}`);
781
+ }
782
+ 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." }));
783
+ const droppedBackends = map.backends.filter((b) => !selection.backends.includes(b.path)).map((b) => ({ path: b.path, why: "Backend not required by the selected frontend." }));
784
+ return {
785
+ frontends: [frontend],
786
+ backends: chosenBackends,
787
+ ignore: [...map.ignore, ...droppedFrontends, ...droppedBackends]
788
+ };
789
+ }
790
+ function formatBackendScope(map) {
791
+ if (map.backends.length === 0) return void 0;
792
+ const backs = map.backends.map((b) => b.dataLayer?.schemaPath != null ? `${b.path} (models @ ${b.dataLayer.schemaPath})` : b.path).join(", ");
793
+ const ignore = map.ignore.map((i) => i.path);
794
+ const ignoreLine = ignore.length > 0 ? ` Ignore these irrelevant directories: ${ignore.join(", ")}.` : "";
795
+ 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}`;
796
+ }
797
+ var FrontendEntry, BackendEntry, IgnoreEntry, ProjectMapSchema, PROJECT_MAP_FILE;
798
+ var init_project_map = __esm({
799
+ "src/core/project-map.ts"() {
800
+ "use strict";
801
+ init_esm_shims();
802
+ init_debug();
803
+ FrontendEntry = z2.object({
804
+ path: z2.string().min(1).describe("Repo-relative path to the frontend app/directory (the UI surface)."),
805
+ framework: z2.string().describe("Detected UI framework/stack, or 'unknown' if not obvious (e.g. next, react, vue, svelte)."),
806
+ dependsOn: z2.array(z2.string()).describe(
807
+ "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."
808
+ ),
809
+ why: z2.string().min(1).describe("One line: the evidence that made you classify this as a frontend.")
810
+ });
811
+ BackendEntry = z2.object({
812
+ path: z2.string().min(1).describe("Repo-relative path to the backend/API/service or the package that owns the data layer."),
813
+ language: z2.string().describe("Primary language, or 'unknown' (e.g. typescript, python, go, rust)."),
814
+ framework: z2.string().describe("Web/service framework or ORM stack, or 'unknown' (e.g. express, hono, fastapi, rails)."),
815
+ dataLayer: z2.object({
816
+ kind: z2.string().describe("How models are defined (e.g. prisma, drizzle, sqlalchemy, typeorm, raw-sql, unknown)."),
817
+ schemaPath: z2.string().optional().describe("Repo-relative path to the schema/models definition, if found.")
818
+ }).optional().describe("Where this backend's database models live. Omit if this backend owns no data layer."),
819
+ why: z2.string().min(1).describe("One line: the evidence that made you classify this as a backend.")
820
+ });
821
+ IgnoreEntry = z2.object({
822
+ path: z2.string().min(1).describe("Repo-relative path to a directory that is NOT relevant to testing this app."),
823
+ why: z2.string().min(1).describe("One line: why it is irrelevant (e.g. docs site, infra, tooling, examples, unrelated app).")
824
+ });
825
+ ProjectMapSchema = z2.object({
826
+ frontends: z2.array(FrontendEntry).describe("The UI surface(s) whose pages/flows get tested. Usually 1; may be more."),
827
+ backends: z2.array(BackendEntry).describe(
828
+ "The API/service(s) and data layer(s) that own the models we seed. May be 0 (frontend-only), 1, or many."
829
+ ),
830
+ ignore: z2.array(IgnoreEntry).describe("Directories judged irrelevant to this app's tests, so later steps skip them.")
831
+ });
832
+ PROJECT_MAP_FILE = "project-map.json";
833
+ }
834
+ });
835
+
495
836
  // src/core/display.ts
496
837
  function formatArgs(input, keys) {
497
838
  const parts = [];
@@ -766,8 +1107,8 @@ var init_agent = __esm({
766
1107
  });
767
1108
 
768
1109
  // src/core/gitignore.ts
769
- import { readFile as readFile5 } from "fs/promises";
770
- import { join as join10, relative as relative2 } from "path";
1110
+ import { readFile as readFile7 } from "fs/promises";
1111
+ import { join as join12, relative as relative2 } from "path";
771
1112
  import { glob as glob2 } from "glob";
772
1113
  async function loadGitignorePatterns(projectRoot) {
773
1114
  const patterns = [
@@ -787,10 +1128,10 @@ async function loadGitignorePatterns(projectRoot) {
787
1128
  ];
788
1129
  const matches = await glob2("**/.gitignore", { cwd: projectRoot, dot: true });
789
1130
  for (const match of matches) {
790
- const fullPath = join10(projectRoot, match);
1131
+ const fullPath = join12(projectRoot, match);
791
1132
  try {
792
- const content = await readFile5(fullPath, "utf-8");
793
- const prefix = relative2(projectRoot, join10(projectRoot, match, ".."));
1133
+ const content = await readFile7(fullPath, "utf-8");
1134
+ const prefix = relative2(projectRoot, join12(projectRoot, match, ".."));
794
1135
  const parsed = parseGitignore(content, prefix);
795
1136
  patterns.push(...parsed);
796
1137
  } catch (err) {
@@ -853,7 +1194,7 @@ var init_exec_error = __esm({
853
1194
  import { execFile as execFile3 } from "child_process";
854
1195
  import { promisify as promisify2 } from "util";
855
1196
  import { tool } from "ai";
856
- import { z as z2 } from "zod";
1197
+ import { z as z4 } from "zod";
857
1198
  function validateCommand(command, allowed) {
858
1199
  const trimmed = command.trim();
859
1200
  if (trimmed.length === 0) return "Empty command";
@@ -919,8 +1260,8 @@ var init_bash = __esm({
919
1260
  DEFAULT_ALLOWED = /* @__PURE__ */ new Set(["git", "wc", "sort", "head", "tail", "cat", "ls", "find", "diff", "echo"]);
920
1261
  TIMEOUT_MS = 3e4;
921
1262
  MAX_OUTPUT_BYTES = 1024 * 512;
922
- inputSchema = z2.object({
923
- command: z2.string().describe("Shell command to execute")
1263
+ inputSchema = z4.object({
1264
+ command: z4.string().describe("Shell command to execute")
924
1265
  });
925
1266
  }
926
1267
  });
@@ -928,7 +1269,7 @@ var init_bash = __esm({
928
1269
  // src/tools/glob.ts
929
1270
  import { tool as tool2 } from "ai";
930
1271
  import { glob as glob3 } from "glob";
931
- import { z as z3 } from "zod";
1272
+ import { z as z5 } from "zod";
932
1273
  async function executeGlob(pattern, cwd, ignorePatterns = DEFAULT_IGNORE) {
933
1274
  try {
934
1275
  const matches = await glob3(pattern, {
@@ -955,9 +1296,9 @@ var init_glob = __esm({
955
1296
  "src/tools/glob.ts"() {
956
1297
  "use strict";
957
1298
  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.")
1299
+ inputSchema2 = z5.object({
1300
+ pattern: z5.string().describe("Glob pattern to match files (e.g. '**/*.ts', 'src/**/*.py')"),
1301
+ cwd: z5.string().optional().describe("Directory to search in. Defaults to working directory.")
961
1302
  });
962
1303
  DEFAULT_IGNORE = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
963
1304
  }
@@ -967,7 +1308,7 @@ var init_glob = __esm({
967
1308
  import { execFile as execFile4 } from "child_process";
968
1309
  import { promisify as promisify3 } from "util";
969
1310
  import { tool as tool3 } from "ai";
970
- import { z as z4 } from "zod";
1311
+ import { z as z6 } from "zod";
971
1312
  function buildGrepTool(workingDirectory) {
972
1313
  return tool3({
973
1314
  description: "Search file contents with ripgrep. Returns matching lines with file paths and line numbers.",
@@ -1011,10 +1352,10 @@ var init_grep = __esm({
1011
1352
  init_esm_shims();
1012
1353
  init_exec_error();
1013
1354
  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")
1355
+ inputSchema3 = z6.object({
1356
+ pattern: z6.string().describe("Regex pattern to search for in file contents"),
1357
+ glob: z6.string().optional().describe("Glob to filter files (e.g. '*.ts')"),
1358
+ path: z6.string().optional().describe("File or directory to search in")
1018
1359
  });
1019
1360
  }
1020
1361
  });
@@ -1022,10 +1363,10 @@ var init_grep = __esm({
1022
1363
  // src/tools/list-directory.ts
1023
1364
  import { readdir } from "fs/promises";
1024
1365
  import { stat } from "fs/promises";
1025
- import { join as join11, relative as relative3 } from "path";
1366
+ import { join as join13, relative as relative3 } from "path";
1026
1367
  import { tool as tool4 } from "ai";
1027
1368
  import { minimatch } from "minimatch";
1028
- import { z as z5 } from "zod";
1369
+ import { z as z7 } from "zod";
1029
1370
  function buildMatcher(patterns) {
1030
1371
  const positive = patterns.filter((p10) => !p10.startsWith("!"));
1031
1372
  const negative = patterns.filter((p10) => p10.startsWith("!")).map((p10) => p10.slice(1));
@@ -1047,7 +1388,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
1047
1388
  const withTypes = [];
1048
1389
  for (const name of rawEntries) {
1049
1390
  try {
1050
- const s = await stat(join11(dirPath, name));
1391
+ const s = await stat(join13(dirPath, name));
1051
1392
  withTypes.push({ name, isDir: s.isDirectory() });
1052
1393
  } catch {
1053
1394
  withTypes.push({ name, isDir: false });
@@ -1067,7 +1408,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
1067
1408
  }
1068
1409
  if (entry.isDir) {
1069
1410
  const children = await buildTree(
1070
- join11(dirPath, entry.name),
1411
+ join13(dirPath, entry.name),
1071
1412
  maxDepth,
1072
1413
  currentDepth + 1,
1073
1414
  isIgnored,
@@ -1105,10 +1446,10 @@ async function buildListDirectoryTool(workingDirectory) {
1105
1446
  const isIgnored = buildMatcher(patterns);
1106
1447
  return tool4({
1107
1448
  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(
1449
+ inputSchema: z7.object({
1450
+ path: z7.string().default(".").describe("Directory path relative to project root. Defaults to root."),
1451
+ depth: z7.number().min(1).max(15).default(10).describe("Max depth to traverse (1-15). Default 10."),
1452
+ gitignore: z7.boolean().describe(
1112
1453
  "Whether to respect the gitignore or to ignore it. true will respect it. false will ignore it. Default true"
1113
1454
  ).default(true)
1114
1455
  }),
@@ -1120,7 +1461,7 @@ async function buildListDirectoryTool(workingDirectory) {
1120
1461
  };
1121
1462
  }
1122
1463
  seen.add(cacheKey);
1123
- const targetDir = input.path === "." ? workingDirectory : join11(workingDirectory, input.path);
1464
+ const targetDir = input.path === "." ? workingDirectory : join13(workingDirectory, input.path);
1124
1465
  try {
1125
1466
  const s = await stat(targetDir);
1126
1467
  if (!s.isDirectory()) {
@@ -1152,10 +1493,10 @@ var init_list_directory = __esm({
1152
1493
  });
1153
1494
 
1154
1495
  // src/tools/read-file.ts
1155
- import { readFile as readFile6 } from "fs/promises";
1496
+ import { readFile as readFile8 } from "fs/promises";
1156
1497
  import { relative as relative4, resolve as resolve2 } from "path";
1157
1498
  import { tool as tool5 } from "ai";
1158
- import { z as z6 } from "zod";
1499
+ import { z as z8 } from "zod";
1159
1500
  function resolveSandboxedPath(workingDirectory, filePath) {
1160
1501
  const absolutePath = resolve2(workingDirectory, filePath);
1161
1502
  const relativePath = relative4(workingDirectory, absolutePath);
@@ -1180,7 +1521,7 @@ async function executeReadFile(workingDirectory, filePath, offset, limit) {
1180
1521
  const resolved = resolveSandboxedPath(workingDirectory, filePath);
1181
1522
  if ("error" in resolved) return resolved;
1182
1523
  try {
1183
- const content = await readFile6(resolved.absolutePath, "utf-8");
1524
+ const content = await readFile8(resolved.absolutePath, "utf-8");
1184
1525
  const sliced = sliceLines(content, offset ?? 0, limit ?? MAX_LINES);
1185
1526
  return {
1186
1527
  path: resolved.relativePath,
@@ -1208,10 +1549,10 @@ var init_read_file = __esm({
1208
1549
  "use strict";
1209
1550
  init_esm_shims();
1210
1551
  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")
1552
+ inputSchema4 = z8.object({
1553
+ filePath: z8.string().describe("Path to the file (absolute or relative to working directory)"),
1554
+ offset: z8.number().int().min(0).optional().describe("Line number to start reading from (0-based)"),
1555
+ limit: z8.number().int().min(1).optional().describe("Maximum number of lines to read")
1215
1556
  });
1216
1557
  }
1217
1558
  });
@@ -1234,10 +1575,10 @@ var init_pick_string = __esm({
1234
1575
 
1235
1576
  // src/tools/subagent.ts
1236
1577
  import { ToolLoopAgent as ToolLoopAgent2, hasToolCall as hasToolCall2, stepCountIs as stepCountIs2, tool as tool6 } from "ai";
1237
- import { z as z7 } from "zod";
1578
+ import { z as z9 } from "zod";
1238
1579
  function buildSubagentTools(workingDirectory, onFileRead) {
1239
1580
  const baseReadFile = buildReadFileTool(workingDirectory);
1240
- const readFile23 = onFileRead ? tool6({
1581
+ const readFile24 = onFileRead ? tool6({
1241
1582
  description: baseReadFile.description,
1242
1583
  inputSchema: baseReadFile.inputSchema,
1243
1584
  execute: async (input, options) => {
@@ -1250,7 +1591,7 @@ function buildSubagentTools(workingDirectory, onFileRead) {
1250
1591
  bash: buildBashTool(workingDirectory),
1251
1592
  glob: buildGlobTool(workingDirectory),
1252
1593
  grep: buildGrepTool(workingDirectory),
1253
- read_file: readFile23
1594
+ read_file: readFile24
1254
1595
  };
1255
1596
  }
1256
1597
  function buildSubagentTool(model, workingDirectory, onHeartbeat, onFileRead) {
@@ -1301,11 +1642,11 @@ var init_subagent = __esm({
1301
1642
  init_glob();
1302
1643
  init_grep();
1303
1644
  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.")
1645
+ inputSchema5 = z9.object({
1646
+ instruction: z9.string().describe("Focused task for the subagent. Be specific about files and patterns to investigate.")
1306
1647
  });
1307
- resultSchema = z7.object({
1308
- findings: z7.string().describe("Summary of what was found")
1648
+ resultSchema = z9.object({
1649
+ findings: z9.string().describe("Summary of what was found")
1309
1650
  });
1310
1651
  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
1652
 
@@ -1316,10 +1657,10 @@ Be thorough but focused - only investigate what's relevant to your instruction.`
1316
1657
  });
1317
1658
 
1318
1659
  // src/tools/write-file.ts
1319
- import { writeFile as writeFile4, mkdir as mkdir2 } from "fs/promises";
1660
+ import { writeFile as writeFile6, mkdir as mkdir2 } from "fs/promises";
1320
1661
  import { dirname as dirname2, relative as relative5, resolve as resolve3 } from "path";
1321
1662
  import { tool as tool7 } from "ai";
1322
- import { z as z8 } from "zod";
1663
+ import { z as z10 } from "zod";
1323
1664
  async function executeWriteFile(outputDirectory, filePath, content) {
1324
1665
  const cleaned = filePath.replace(/^autonoma\//, "");
1325
1666
  const absolutePath = resolve3(outputDirectory, cleaned);
@@ -1329,7 +1670,7 @@ async function executeWriteFile(outputDirectory, filePath, content) {
1329
1670
  }
1330
1671
  try {
1331
1672
  await mkdir2(dirname2(absolutePath), { recursive: true });
1332
- await writeFile4(absolutePath, content, "utf-8");
1673
+ await writeFile6(absolutePath, content, "utf-8");
1333
1674
  return { path: relativePath, bytesWritten: content.length };
1334
1675
  } catch (err) {
1335
1676
  const message = err instanceof Error ? err.message : String(err);
@@ -1348,28 +1689,33 @@ var init_write_file = __esm({
1348
1689
  "src/tools/write-file.ts"() {
1349
1690
  "use strict";
1350
1691
  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")
1692
+ inputSchema6 = z10.object({
1693
+ filePath: z10.string().describe("Path to write (absolute or relative to output directory)"),
1694
+ content: z10.string().describe("File content to write")
1354
1695
  });
1355
1696
  }
1356
1697
  });
1357
1698
 
1358
1699
  // src/tools/ask-user.ts
1359
- import * as p2 from "@clack/prompts";
1700
+ import * as p3 from "@clack/prompts";
1360
1701
  import { tool as tool8 } from "ai";
1361
- import { z as z9 } from "zod";
1702
+ import { z as z11 } from "zod";
1362
1703
  function buildAskUserTool() {
1363
1704
  return tool8({
1364
1705
  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(
1706
+ inputSchema: z11.object({
1707
+ question: z11.string().describe(
1367
1708
  "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
1709
  )
1369
1710
  }),
1370
1711
  execute: async (input) => {
1371
- const answer = await p2.text({ message: input.question });
1372
- if (p2.isCancel(answer)) return { answer: "User skipped this question" };
1712
+ if (!process.stdin.isTTY) {
1713
+ return {
1714
+ 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."
1715
+ };
1716
+ }
1717
+ const answer = await p3.text({ message: input.question });
1718
+ if (p3.isCancel(answer)) return { answer: "User skipped this question" };
1373
1719
  return { answer };
1374
1720
  }
1375
1721
  });
@@ -1411,6 +1757,58 @@ var init_tools = __esm({
1411
1757
  }
1412
1758
  });
1413
1759
 
1760
+ // src/agents/00-project-mapper/index.ts
1761
+ var project_mapper_exports = {};
1762
+ __export(project_mapper_exports, {
1763
+ runProjectMapper: () => runProjectMapper
1764
+ });
1765
+ import { tool as tool9 } from "ai";
1766
+ async function runProjectMapper(input) {
1767
+ const model = getModel(input.modelId);
1768
+ const { logger, onStepFinish } = buildDefaultStepLogger("project-map", MAX_STEPS);
1769
+ let captured;
1770
+ const prompt = `Map the codebase rooted at ${input.projectRoot}.
1771
+
1772
+ 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.`;
1773
+ const agentConfig = {
1774
+ id: "project-mapper",
1775
+ systemPrompt: SYSTEM_PROMPT2,
1776
+ model,
1777
+ maxSteps: MAX_STEPS,
1778
+ tools: async (heartbeat) => {
1779
+ const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
1780
+ return {
1781
+ ...tools,
1782
+ set_project_map: tool9({
1783
+ 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.",
1784
+ inputSchema: ProjectMapSchema,
1785
+ execute: (map) => {
1786
+ captured = map;
1787
+ return `Recorded: ${map.frontends.length} frontend(s), ${map.backends.length} backend(s), ${map.ignore.length} ignored. Now call finish.`;
1788
+ }
1789
+ })
1790
+ };
1791
+ },
1792
+ onStepFinish
1793
+ };
1794
+ await runAgent(agentConfig, prompt, () => captured);
1795
+ logger.summary();
1796
+ return captured;
1797
+ }
1798
+ var MAX_STEPS, SYSTEM_PROMPT2;
1799
+ var init_project_mapper = __esm({
1800
+ "src/agents/00-project-mapper/index.ts"() {
1801
+ "use strict";
1802
+ init_esm_shims();
1803
+ init_agent();
1804
+ init_model();
1805
+ init_project_map();
1806
+ init_tools();
1807
+ MAX_STEPS = 60;
1808
+ 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.";
1809
+ }
1810
+ });
1811
+
1414
1812
  // src/agents/00-pages-finder/index.ts
1415
1813
  var pages_finder_exports = {};
1416
1814
  __export(pages_finder_exports, {
@@ -1418,8 +1816,8 @@ __export(pages_finder_exports, {
1418
1816
  });
1419
1817
  import { existsSync } from "fs";
1420
1818
  import * as path2 from "path";
1421
- import { tool as tool9 } from "ai";
1422
- import { z as z10 } from "zod";
1819
+ import { tool as tool10 } from "ai";
1820
+ import { z as z12 } from "zod";
1423
1821
  async function runPageFinder(input) {
1424
1822
  const model = getModel(input.modelId);
1425
1823
  const pageCollector = new PageCollector();
@@ -1438,7 +1836,7 @@ ${input.extraMessage}`;
1438
1836
  const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
1439
1837
  return {
1440
1838
  ...tools,
1441
- add_page: tool9({
1839
+ add_page: tool10({
1442
1840
  description: "use this tool to add a page that you found",
1443
1841
  inputSchema: Page,
1444
1842
  execute: (input2) => {
@@ -1450,9 +1848,9 @@ ${input.extraMessage}`;
1450
1848
  return `page ${JSON.stringify(input2)} added`;
1451
1849
  }
1452
1850
  }),
1453
- view_pages: tool9({
1851
+ view_pages: tool10({
1454
1852
  description: "use this tool to view all the pages that you already added",
1455
- inputSchema: z10.object(),
1853
+ inputSchema: z12.object(),
1456
1854
  execute: () => pageCollector.viewPages()
1457
1855
  })
1458
1856
  };
@@ -1471,10 +1869,10 @@ var init_pages_finder = __esm({
1471
1869
  init_agent();
1472
1870
  init_model();
1473
1871
  init_tools();
1474
- Page = z10.object({
1475
- route: z10.string().min(1),
1476
- path: z10.string().min(1),
1477
- description: z10.string().min(10)
1872
+ Page = z12.object({
1873
+ route: z12.string().min(1),
1874
+ path: z12.string().min(1),
1875
+ description: z12.string().min(10)
1478
1876
  });
1479
1877
  PageCollector = class {
1480
1878
  // the key is the path
@@ -1504,13 +1902,13 @@ var init_pages_finder = __esm({
1504
1902
 
1505
1903
  // src/core/review.ts
1506
1904
  import { access } from "fs/promises";
1507
- import { join as join12, isAbsolute } from "path";
1508
- import * as p3 from "@clack/prompts";
1905
+ import { join as join14, isAbsolute } from "path";
1906
+ import * as p4 from "@clack/prompts";
1509
1907
  import spawn from "cross-spawn";
1510
1908
  import which from "which";
1511
1909
  function resolvePath(artifact, outputDir) {
1512
1910
  if (isAbsolute(artifact)) return artifact;
1513
- return join12(outputDir, artifact);
1911
+ return join14(outputDir, artifact);
1514
1912
  }
1515
1913
  async function detectEditors() {
1516
1914
  if (cachedEditors) return cachedEditors;
@@ -1525,16 +1923,16 @@ async function detectEditors() {
1525
1923
  async function launchEditor(editor, files) {
1526
1924
  const args = editor.args(files);
1527
1925
  const isTerminalEditor = TERMINAL_EDITORS.has(editor.command);
1528
- await new Promise((resolve5) => {
1926
+ await new Promise((resolve6) => {
1529
1927
  let settled = false;
1530
1928
  const settle = () => {
1531
1929
  if (settled) return;
1532
1930
  settled = true;
1533
- resolve5();
1931
+ resolve6();
1534
1932
  };
1535
1933
  const proc = spawn(editor.command, args, { stdio: "inherit" });
1536
1934
  proc.on("error", (err) => {
1537
- p3.log.warn(`Couldn't open ${editor.label} (${err.message}). Review the files manually:`);
1935
+ p4.log.warn(`Couldn't open ${editor.label} (${err.message}). Review the files manually:`);
1538
1936
  for (const f of files) console.log(` ${CYAN}${f}${RESET3}`);
1539
1937
  settle();
1540
1938
  });
@@ -1548,17 +1946,17 @@ async function launchEditor(editor, files) {
1548
1946
  async function openInEditor(files) {
1549
1947
  const editors = await detectEditors();
1550
1948
  if (editors.length === 0) {
1551
- p3.log.warn("No editors found. Review the files manually:");
1949
+ p4.log.warn("No editors found. Review the files manually:");
1552
1950
  for (const f of files) console.log(` ${CYAN}${f}${RESET3}`);
1553
1951
  return;
1554
1952
  }
1555
1953
  if (preferredEditor) {
1556
1954
  const editor2 = editors.find((e) => e.command === preferredEditor);
1557
1955
  if (editor2) {
1558
- const open = await p3.confirm({
1956
+ const open = await p4.confirm({
1559
1957
  message: `Open in ${editor2.label}?`
1560
1958
  });
1561
- if (!p3.isCancel(open) && open) {
1959
+ if (!p4.isCancel(open) && open) {
1562
1960
  await launchEditor(editor2, files);
1563
1961
  }
1564
1962
  return;
@@ -1568,20 +1966,20 @@ async function openInEditor(files) {
1568
1966
  value: e.command,
1569
1967
  label: e.label
1570
1968
  }));
1571
- const selected = await p3.select({
1969
+ const selected = await p4.select({
1572
1970
  message: "Open output files for review?",
1573
1971
  options: [...options, { value: "skip", label: "No, skip - I'll review later" }]
1574
1972
  });
1575
- if (p3.isCancel(selected) || selected === "skip") return;
1973
+ if (p4.isCancel(selected) || selected === "skip") return;
1576
1974
  const editor = editors.find((e) => e.command === selected);
1577
- const remember = await p3.select({
1975
+ const remember = await p4.select({
1578
1976
  message: `Use ${editor.label} for all future reviews?`,
1579
1977
  options: [
1580
1978
  { value: "always", label: `Yes, always use ${editor.label}` },
1581
1979
  { value: "ask", label: "No, ask me each time" }
1582
1980
  ]
1583
1981
  });
1584
- if (!p3.isCancel(remember) && remember === "always") {
1982
+ if (!p4.isCancel(remember) && remember === "always") {
1585
1983
  preferredEditor = editor.command;
1586
1984
  }
1587
1985
  await launchEditor(editor, files);
@@ -1592,7 +1990,7 @@ async function showResults(result, options) {
1592
1990
  if (result.artifacts.length === 0) {
1593
1991
  const knownFiles = ["AUTONOMA.md", "entity-audit.md", "scenarios.md"];
1594
1992
  for (const f of knownFiles) {
1595
- const fullPath = join12(options.outputDir, f);
1993
+ const fullPath = join14(options.outputDir, f);
1596
1994
  try {
1597
1995
  await access(fullPath);
1598
1996
  result.artifacts.push(f);
@@ -1622,7 +2020,7 @@ async function showResults(result, options) {
1622
2020
  }
1623
2021
  }
1624
2022
  if (options.reviewGuidance) {
1625
- p3.note(options.reviewGuidance, "What to check");
2023
+ p4.note(options.reviewGuidance, "What to check");
1626
2024
  }
1627
2025
  const showPreview = options.showPreview !== false;
1628
2026
  if (showPreview && resolvedPaths.length > 0 && !options.nonInteractive) {
@@ -1635,21 +2033,21 @@ async function reviewLoop(result, options) {
1635
2033
  await showResults(result, options);
1636
2034
  if (options.nonInteractive) return result;
1637
2035
  while (true) {
1638
- const input = await p3.text({
2036
+ const input = await p4.text({
1639
2037
  message: "Review the output. Press Enter to approve, or type feedback for the agent.",
1640
2038
  placeholder: "Looks good (Enter to approve)",
1641
2039
  defaultValue: ""
1642
2040
  });
1643
- if (p3.isCancel(input)) {
1644
- p3.log.warn("Cancelled.");
2041
+ if (p4.isCancel(input)) {
2042
+ p4.log.warn("Cancelled.");
1645
2043
  return result;
1646
2044
  }
1647
2045
  const feedback = input.trim();
1648
2046
  if (feedback === "") {
1649
- p3.log.success("Approved - moving on.");
2047
+ p4.log.success("Approved - moving on.");
1650
2048
  return result;
1651
2049
  }
1652
- p3.log.info(`Sending feedback to ${options.agentId}...`);
2050
+ p4.log.info(`Sending feedback to ${options.agentId}...`);
1653
2051
  console.log("");
1654
2052
  const revised = await options.onFeedback(feedback);
1655
2053
  if (revised) {
@@ -1681,13 +2079,13 @@ var init_review = __esm({
1681
2079
  });
1682
2080
 
1683
2081
  // src/agents/01-kb-generator/flows.ts
1684
- import { readFile as readFile7 } from "fs/promises";
1685
- import { join as join13 } from "path";
2082
+ import { readFile as readFile9 } from "fs/promises";
2083
+ import { join as join15 } from "path";
1686
2084
  import matter from "gray-matter";
1687
2085
  async function parseCoreFlows(outputDir) {
1688
2086
  let raw;
1689
2087
  try {
1690
- raw = await readFile7(join13(outputDir, "AUTONOMA.md"), "utf-8");
2088
+ raw = await readFile9(join15(outputDir, "AUTONOMA.md"), "utf-8");
1691
2089
  } catch {
1692
2090
  return [];
1693
2091
  }
@@ -1752,12 +2150,12 @@ var init_flows = __esm({
1752
2150
  });
1753
2151
 
1754
2152
  // src/agents/01-kb-generator/prompt.ts
1755
- var SYSTEM_PROMPT2;
2153
+ var SYSTEM_PROMPT3;
1756
2154
  var init_prompt = __esm({
1757
2155
  "src/agents/01-kb-generator/prompt.ts"() {
1758
2156
  "use strict";
1759
2157
  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.
2158
+ 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
2159
 
1762
2160
  ## Your output
1763
2161
 
@@ -1900,15 +2298,15 @@ var kb_generator_exports = {};
1900
2298
  __export(kb_generator_exports, {
1901
2299
  runKBGenerator: () => runKBGenerator
1902
2300
  });
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";
2301
+ import { readFile as readFile10 } from "fs/promises";
2302
+ import { join as join16, resolve as resolve5 } from "path";
2303
+ import { tool as tool11 } from "ai";
2304
+ import { z as z13 } from "zod";
1907
2305
  function buildRegisterPagesTool(tracker) {
1908
- return tool10({
2306
+ return tool11({
1909
2307
  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")
2308
+ inputSchema: z13.object({
2309
+ pages: z13.array(z13.string()).describe("All page file paths found by glob")
1912
2310
  }),
1913
2311
  execute: async (input) => {
1914
2312
  tracker.register(input.pages);
@@ -1920,25 +2318,33 @@ function buildRegisterPagesTool(tracker) {
1920
2318
  });
1921
2319
  }
1922
2320
  function buildPageCoverageTool(tracker) {
1923
- return tool10({
2321
+ return tool11({
1924
2322
  description: "Check how many registered pages you've read vs how many remain.",
1925
- inputSchema: z11.object({}),
2323
+ inputSchema: z13.object({}),
1926
2324
  execute: async () => tracker.coverage()
1927
2325
  });
1928
2326
  }
2327
+ function requiredReads(total) {
2328
+ if (total <= FULL_COVERAGE_MAX_ROUTES) return total;
2329
+ return Math.ceil(total * LARGE_APP_COVERAGE_FLOOR);
2330
+ }
1929
2331
  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")
2332
+ return tool11({
2333
+ 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.",
2334
+ inputSchema: z13.object({
2335
+ summary: z13.string().describe("Summary of what was generated"),
2336
+ artifacts: z13.array(z13.string()).describe("List of files written")
1935
2337
  }),
1936
2338
  execute: async (input) => {
1937
2339
  const cov = tracker.coverage();
1938
- if (cov.unread.length > 0) {
2340
+ const required = requiredReads(cov.total);
2341
+ if (cov.read < required) {
2342
+ const preview = cov.unread.slice(0, 40).join("\n");
2343
+ const more = cov.unread.length > 40 ? `
2344
+ ...and ${cov.unread.length - 40} more` : "";
1939
2345
  return {
1940
- error: `Cannot finish: ${cov.unread.length}/${cov.total} pages not yet read. Read these files first:
1941
- ${cov.unread.join("\n")}`
2346
+ error: `Cannot finish: only ${cov.read}/${cov.total} routes read - read at least ${required - cov.read} more (target ${required} of ${cov.total}). Start with:
2347
+ ${preview}${more}`
1942
2348
  };
1943
2349
  }
1944
2350
  onFinish({
@@ -1951,7 +2357,7 @@ ${cov.unread.join("\n")}`
1951
2357
  });
1952
2358
  }
1953
2359
  function buildTrackedReadTool(tracker, baseTool) {
1954
- return tool10({
2360
+ return tool11({
1955
2361
  description: baseTool.description,
1956
2362
  inputSchema: baseTool.inputSchema,
1957
2363
  execute: async (input, options) => {
@@ -1961,13 +2367,36 @@ function buildTrackedReadTool(tracker, baseTool) {
1961
2367
  }
1962
2368
  });
1963
2369
  }
2370
+ function buildKbAgentConfig(tracker, model, input, onStepFinish, setResult) {
2371
+ return {
2372
+ id: "kb-generator",
2373
+ systemPrompt: SYSTEM_PROMPT3,
2374
+ model,
2375
+ maxSteps: 150,
2376
+ tools: async (heartbeat) => {
2377
+ const onFileRead = (path3) => tracker.markRead(path3);
2378
+ const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat, onFileRead);
2379
+ return {
2380
+ ...tools,
2381
+ read_file: buildTrackedReadTool(tracker, tools.read_file),
2382
+ register_pages: buildRegisterPagesTool(tracker),
2383
+ page_coverage: buildPageCoverageTool(tracker),
2384
+ finish: buildFinishTool(tracker, setResult)
2385
+ };
2386
+ },
2387
+ onStepFinish
2388
+ };
2389
+ }
1964
2390
  async function runKBGenerator(input) {
1965
2391
  const model = getModel(input.modelId);
1966
2392
  let result;
1967
- const tracker = new PageTracker();
2393
+ const setResult = (r) => {
2394
+ result = r;
2395
+ };
1968
2396
  const { logger, onStepFinish } = buildDefaultStepLogger("kb", 150);
1969
2397
  const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + formatRetryGuidance(input.retryGuidance);
1970
2398
  const pages = input.projectContext?.pages;
2399
+ const tracker = new PageTracker(input.projectRoot);
1971
2400
  if (pages?.length) {
1972
2401
  tracker.register(pages.map((p10) => p10.path));
1973
2402
  }
@@ -1979,7 +2408,7 @@ Pages have already been discovered (${pages.length} routes pre-registered). You
1979
2408
  2. Read EVERY registered page file with read_file - the system tracks this
1980
2409
  3. Write AUTONOMA.md progressively as you go (update it after each major area)
1981
2410
  4. Call page_coverage to verify you've read all pages
1982
- 5. Call finish - it will REJECT if pages are unread
2411
+ 5. Call finish - it will REJECT if you have not read enough of the registered routes
1983
2412
 
1984
2413
  Output files:
1985
2414
  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 +2420,15 @@ MANDATORY PROCESS:
1991
2420
  4. Read EVERY registered page file with read_file - the system tracks this
1992
2421
  5. Write AUTONOMA.md progressively as you go (update it after each major area)
1993
2422
  6. Call page_coverage to verify you've read all pages
1994
- 7. Call finish - it will REJECT if pages are unread
2423
+ 7. Call finish - it will REJECT if you have not read enough of the registered routes
1995
2424
 
1996
2425
  Output files:
1997
2426
  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
- };
2427
+ const agentConfig = buildKbAgentConfig(tracker, model, input, onStepFinish, setResult);
2018
2428
  await runAgent(agentConfig, prompt, () => result);
2019
2429
  logger.summary();
2020
- const autonomaPath = join14(input.outputDir, "AUTONOMA.md");
2021
- const autonomaExists = await readFile8(autonomaPath, "utf-8").then(() => true).catch((err) => {
2430
+ const autonomaPath = join16(input.outputDir, "AUTONOMA.md");
2431
+ const autonomaExists = await readFile10(autonomaPath, "utf-8").then(() => true).catch((err) => {
2022
2432
  debugLog("AUTONOMA.md not found while checking step completion", { err });
2023
2433
  return false;
2024
2434
  });
@@ -2029,6 +2439,13 @@ Output files:
2029
2439
  summary: "Knowledge base generated."
2030
2440
  };
2031
2441
  }
2442
+ const finalTracker = new PageTracker(input.projectRoot);
2443
+ if (pages?.length) {
2444
+ const paths = pages.map((p10) => p10.path);
2445
+ finalTracker.register(paths);
2446
+ for (const path3 of paths) finalTracker.markRead(path3);
2447
+ }
2448
+ const finalConfig = buildKbAgentConfig(finalTracker, model, input, onStepFinish, setResult);
2032
2449
  const declaredCriticalFlows = input.projectContext?.criticalFlows?.trim();
2033
2450
  if (result?.success && declaredCriticalFlows) {
2034
2451
  const beforeSelfReview = result;
@@ -2045,7 +2462,7 @@ Read your AUTONOMA.md output. For EACH critical flow the user named:
2045
2462
  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
2463
 
2047
2464
  When AUTONOMA.md correctly reflects every declared critical flow, call finish.`;
2048
- await runAgent(agentConfig, selfReviewPrompt, () => result);
2465
+ await runAgent(finalConfig, selfReviewPrompt, () => result);
2049
2466
  if (!result) result = beforeSelfReview;
2050
2467
  }
2051
2468
  const reviewed = await reviewLoop(result, {
@@ -2066,7 +2483,7 @@ When AUTONOMA.md correctly reflects every declared critical flow, call finish.`;
2066
2483
  Read your previous output file (AUTONOMA.md) from the output directory to see what you produced.
2067
2484
  Adjust based on the feedback. You can read source files again if needed.
2068
2485
  Call page_coverage to see current state. When done with changes, call finish again.`;
2069
- await runAgent(agentConfig, feedbackPrompt, () => result);
2486
+ await runAgent(finalConfig, feedbackPrompt, () => result);
2070
2487
  return result;
2071
2488
  }
2072
2489
  });
@@ -2076,7 +2493,7 @@ Call page_coverage to see current state. When done with changes, call finish aga
2076
2493
  summary: "KB generator agent stopped without producing AUTONOMA.md"
2077
2494
  };
2078
2495
  }
2079
- var PageTracker;
2496
+ var PageTracker, FULL_COVERAGE_MAX_ROUTES, LARGE_APP_COVERAGE_FLOOR;
2080
2497
  var init_kb_generator = __esm({
2081
2498
  "src/agents/01-kb-generator/index.ts"() {
2082
2499
  "use strict";
@@ -2091,14 +2508,26 @@ var init_kb_generator = __esm({
2091
2508
  init_flows();
2092
2509
  init_prompt();
2093
2510
  PageTracker = class {
2511
+ // Registered pages (from pages.json) are absolute paths, but the agent reads them
2512
+ // with paths relative to the working directory. Canonicalize both to an absolute
2513
+ // path against projectRoot so coverage matches regardless of how each side spelled
2514
+ // it - otherwise the finish gate never clears and the agent nudges out re-reading.
2515
+ constructor(projectRoot) {
2516
+ this.projectRoot = projectRoot;
2517
+ }
2518
+ projectRoot;
2094
2519
  registered = /* @__PURE__ */ new Set();
2095
2520
  read = /* @__PURE__ */ new Set();
2521
+ normalize(filePath) {
2522
+ return resolve5(this.projectRoot, filePath);
2523
+ }
2096
2524
  register(pages) {
2097
- for (const p10 of pages) this.registered.add(p10);
2525
+ for (const p10 of pages) this.registered.add(this.normalize(p10));
2098
2526
  }
2099
2527
  markRead(filePath) {
2100
- if (this.registered.has(filePath)) {
2101
- this.read.add(filePath);
2528
+ const normalized = this.normalize(filePath);
2529
+ if (this.registered.has(normalized)) {
2530
+ this.read.add(normalized);
2102
2531
  }
2103
2532
  }
2104
2533
  unread() {
@@ -2112,16 +2541,18 @@ var init_kb_generator = __esm({
2112
2541
  };
2113
2542
  }
2114
2543
  };
2544
+ FULL_COVERAGE_MAX_ROUTES = 40;
2545
+ LARGE_APP_COVERAGE_FLOOR = 0.5;
2115
2546
  }
2116
2547
  });
2117
2548
 
2118
2549
  // src/agents/04-recipe-builder/entity-order.ts
2119
- import { readFile as readFile9 } from "fs/promises";
2120
- import { join as join15 } from "path";
2550
+ import { readFile as readFile11 } from "fs/promises";
2551
+ import { join as join17 } from "path";
2121
2552
  import matter2 from "gray-matter";
2122
- import { z as z12 } from "zod";
2553
+ import { z as z14 } from "zod";
2123
2554
  async function parseEntityAudit(outputDir) {
2124
- const raw = await readFile9(join15(outputDir, "entity-audit.md"), "utf-8");
2555
+ const raw = await readFile11(join17(outputDir, "entity-audit.md"), "utf-8");
2125
2556
  try {
2126
2557
  const parsed = frontmatterSchema.safeParse(matter2(raw).data);
2127
2558
  if (parsed.success && parsed.data.models.length > 0) {
@@ -2282,22 +2713,22 @@ var init_entity_order = __esm({
2282
2713
  "src/agents/04-recipe-builder/entity-order.ts"() {
2283
2714
  "use strict";
2284
2715
  init_esm_shims();
2285
- createdBySchema = z12.object({
2286
- owner: z12.string(),
2287
- via: z12.string().optional(),
2288
- why: z12.string().optional()
2716
+ createdBySchema = z14.object({
2717
+ owner: z14.string(),
2718
+ via: z14.string().optional(),
2719
+ why: z14.string().optional()
2289
2720
  });
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(),
2721
+ auditedModelSchema = z14.object({
2722
+ name: z14.string(),
2723
+ independently_created: z14.coerce.boolean().default(false),
2724
+ creation_file: z14.string().optional(),
2725
+ creation_function: z14.string().optional(),
2726
+ side_effects: z14.array(z14.string()).optional(),
2296
2727
  // Tolerate a stray `created_by:` with no entries (parsed as null by YAML).
2297
- created_by: z12.array(createdBySchema).nullish().transform((v) => v ?? [])
2728
+ created_by: z14.array(createdBySchema).nullish().transform((v) => v ?? [])
2298
2729
  });
2299
- frontmatterSchema = z12.object({
2300
- models: z12.array(auditedModelSchema).nullish().transform((v) => v ?? [])
2730
+ frontmatterSchema = z14.object({
2731
+ models: z14.array(auditedModelSchema).nullish().transform((v) => v ?? [])
2301
2732
  });
2302
2733
  }
2303
2734
  });
@@ -2364,12 +2795,12 @@ var init_audit_table = __esm({
2364
2795
  });
2365
2796
 
2366
2797
  // src/agents/02-entity-audit/prompt.ts
2367
- var SYSTEM_PROMPT3;
2798
+ var SYSTEM_PROMPT4;
2368
2799
  var init_prompt2 = __esm({
2369
2800
  "src/agents/02-entity-audit/prompt.ts"() {
2370
2801
  "use strict";
2371
2802
  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.
2803
+ 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
2804
 
2374
2805
  ## The two orthogonal questions
2375
2806
 
@@ -2507,17 +2938,17 @@ var entity_audit_exports = {};
2507
2938
  __export(entity_audit_exports, {
2508
2939
  runEntityAudit: () => runEntityAudit
2509
2940
  });
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";
2941
+ import { readFile as readFile12, writeFile as writeFile7 } from "fs/promises";
2942
+ import { join as join18 } from "path";
2943
+ import { tool as tool12 } from "ai";
2513
2944
  import { glob as glob4 } from "glob";
2514
- import { z as z13 } from "zod";
2945
+ import { z as z15 } from "zod";
2515
2946
  function buildRegisterModelsTool(tracker) {
2516
- return tool11({
2947
+ return tool12({
2517
2948
  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')")
2949
+ inputSchema: z15.object({
2950
+ models: z15.array(z15.string()).describe("All model/table names found by grep"),
2951
+ framework: z15.string().describe("Database framework detected (e.g. 'sqlalchemy', 'prisma', 'drizzle')")
2521
2952
  }),
2522
2953
  execute: async (input) => {
2523
2954
  tracker.register(input.models);
@@ -2531,9 +2962,9 @@ function buildRegisterModelsTool(tracker) {
2531
2962
  });
2532
2963
  }
2533
2964
  function buildNextModelTool(tracker) {
2534
- return tool11({
2965
+ return tool12({
2535
2966
  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({}),
2967
+ inputSchema: z15.object({}),
2537
2968
  execute: async () => {
2538
2969
  const next = tracker.nextModel();
2539
2970
  if (!next) {
@@ -2551,19 +2982,19 @@ function buildNextModelTool(tracker) {
2551
2982
  });
2552
2983
  }
2553
2984
  function buildMarkModelAuditedTool(tracker) {
2554
- return tool11({
2985
+ return tool12({
2555
2986
  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")
2987
+ inputSchema: z15.object({
2988
+ model: z15.string().describe("Model name"),
2989
+ independently_created: z15.boolean(),
2990
+ creation_file: z15.string().optional().describe("File containing the creation function"),
2991
+ creation_function: z15.string().optional().describe("Function/method name (e.g. 'UserService.create' or 'create_user')"),
2992
+ side_effects: z15.array(z15.string()).optional().describe("Side effects of creation (e.g. 'creates default Settings row', 'hashes password')"),
2993
+ created_by: z15.array(
2994
+ z15.object({
2995
+ owner: z15.string().describe("Owner model name"),
2996
+ via: z15.string().optional().describe("Function that creates this model (e.g. 'OrganizationService.create')"),
2997
+ why: z15.string().optional().describe("One sentence explaining why this model is created as a side effect")
2567
2998
  })
2568
2999
  ).describe("List of owner models that create this as a side effect, empty array if none")
2569
3000
  }),
@@ -2600,18 +3031,18 @@ function buildMarkModelAuditedTool(tracker) {
2600
3031
  });
2601
3032
  }
2602
3033
  function buildModelCoverageTool(tracker) {
2603
- return tool11({
3034
+ return tool12({
2604
3035
  description: "Check how many registered models you've audited vs how many remain.",
2605
- inputSchema: z13.object({}),
3036
+ inputSchema: z15.object({}),
2606
3037
  execute: async () => tracker.coverage()
2607
3038
  });
2608
3039
  }
2609
3040
  function buildFinishTool2(tracker, onFinish) {
2610
- return tool11({
3041
+ return tool12({
2611
3042
  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")
3043
+ inputSchema: z15.object({
3044
+ summary: z15.string().describe("Summary of the audit"),
3045
+ artifacts: z15.array(z15.string()).describe("Files written")
2615
3046
  }),
2616
3047
  execute: async (input) => {
2617
3048
  const cov = tracker.coverage();
@@ -2639,7 +3070,7 @@ async function findPrismaSchema(projectRoot) {
2639
3070
  return candidates[0] ?? void 0;
2640
3071
  }
2641
3072
  async function extractPrismaModels(schemaPath) {
2642
- const content = await readFile10(schemaPath, "utf-8");
3073
+ const content = await readFile12(schemaPath, "utf-8");
2643
3074
  return content.split("\n").filter((line) => line.startsWith("model ")).map((line) => line.split(/\s+/)[1]).filter((name) => name != null);
2644
3075
  }
2645
3076
  async function detectFrameworkAndModels(projectRoot) {
@@ -2662,8 +3093,11 @@ async function runEntityAudit(input) {
2662
3093
  tracker.initQueue();
2663
3094
  preRegisteredCount = detection.models.length;
2664
3095
  }
2665
- const { logger, onStepFinish } = buildDefaultStepLogger("entity-audit", 200);
2666
- const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + formatRetryGuidance(input.retryGuidance);
3096
+ const { logger, onStepFinish } = buildDefaultStepLogger("entity-audit", MAX_STEPS2);
3097
+ const scopeBlock = input.scopeHint != null ? `
3098
+ ${input.scopeHint}
3099
+ ` : "";
3100
+ const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + scopeBlock + formatRetryGuidance(input.retryGuidance);
2667
3101
  const preRegBlock = preRegisteredCount > 0 ? `
2668
3102
  ## Pre-registered models (${preRegisteredCount} found via ${detection.framework} schema at ${detection.schemaFile})
2669
3103
 
@@ -2693,9 +3127,9 @@ After every 10 mark_model_audited calls, use write_file to update entity-audit.m
2693
3127
  write_file already targets the output directory - use just the filename.`;
2694
3128
  const agentConfig = {
2695
3129
  id: "entity-audit",
2696
- systemPrompt: SYSTEM_PROMPT3,
3130
+ systemPrompt: SYSTEM_PROMPT4,
2697
3131
  model,
2698
- maxSteps: 200,
3132
+ maxSteps: MAX_STEPS2,
2699
3133
  tools: async (heartbeat) => {
2700
3134
  const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
2701
3135
  return {
@@ -2723,8 +3157,8 @@ ${formatException(err)}`);
2723
3157
  logger.summary();
2724
3158
  const writeCanonicalAudit = async () => {
2725
3159
  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");
3160
+ const auditPath = join18(input.outputDir, "entity-audit.md");
3161
+ await writeFile7(auditPath, tracker.generateAuditMarkdown(), "utf-8");
2728
3162
  return auditPath;
2729
3163
  };
2730
3164
  const canonicalPath = await writeCanonicalAudit();
@@ -2761,9 +3195,9 @@ When done with changes, call finish again.`;
2761
3195
  }
2762
3196
  });
2763
3197
  if (!reviewed) {
2764
- const auditPath = join16(input.outputDir, "entity-audit.md");
3198
+ const auditPath = join18(input.outputDir, "entity-audit.md");
2765
3199
  try {
2766
- await readFile10(auditPath, "utf-8");
3200
+ await readFile12(auditPath, "utf-8");
2767
3201
  return {
2768
3202
  success: true,
2769
3203
  artifacts: ["entity-audit.md"],
@@ -2779,7 +3213,7 @@ When done with changes, call finish again.`;
2779
3213
  summary: agentError ? `Entity audit failed: ${agentError}` : "Entity audit agent stopped without producing entity-audit.md"
2780
3214
  };
2781
3215
  }
2782
- var ModelTracker;
3216
+ var MAX_STEPS2, ModelTracker;
2783
3217
  var init_entity_audit = __esm({
2784
3218
  "src/agents/02-entity-audit/index.ts"() {
2785
3219
  "use strict";
@@ -2794,6 +3228,7 @@ var init_entity_audit = __esm({
2794
3228
  init_ask_user();
2795
3229
  init_audit_table();
2796
3230
  init_prompt2();
3231
+ MAX_STEPS2 = 400;
2797
3232
  ModelTracker = class {
2798
3233
  registered = /* @__PURE__ */ new Set();
2799
3234
  auditedModels = /* @__PURE__ */ new Map();
@@ -2904,11 +3339,11 @@ ${duals.length > 0 ? duals.map((m) => `- **${m.name}** - standalone: ${m.creatio
2904
3339
  });
2905
3340
 
2906
3341
  // src/core/parse-entity-audit.ts
2907
- import { readFile as readFile11 } from "fs/promises";
2908
- import { join as join17 } from "path";
3342
+ import { readFile as readFile13 } from "fs/promises";
3343
+ import { join as join19 } from "path";
2909
3344
  async function parseEntityNames(outputDir) {
2910
3345
  try {
2911
- const content = await readFile11(join17(outputDir, "entity-audit.md"), "utf-8");
3346
+ const content = await readFile13(join19(outputDir, "entity-audit.md"), "utf-8");
2912
3347
  const names = [];
2913
3348
  for (const line of content.split("\n")) {
2914
3349
  const match = line.match(/^\s+-\s+name:\s+(.+)$/);
@@ -2981,13 +3416,13 @@ values; the recipe builder generates the exact records from what you write.`;
2981
3416
  });
2982
3417
 
2983
3418
  // src/agents/03-scenario-recipe/scenario-table.ts
2984
- import { readFile as readFile12 } from "fs/promises";
2985
- import { join as join18 } from "path";
3419
+ import { readFile as readFile14 } from "fs/promises";
3420
+ import { join as join20 } from "path";
2986
3421
  import matter3 from "gray-matter";
2987
3422
  async function parseScenario(outputDir) {
2988
3423
  let raw;
2989
3424
  try {
2990
- raw = await readFile12(join18(outputDir, "scenarios.md"), "utf-8");
3425
+ raw = await readFile14(join20(outputDir, "scenarios.md"), "utf-8");
2991
3426
  } catch {
2992
3427
  return { scenarioNames: [], entityTypes: [] };
2993
3428
  }
@@ -3068,22 +3503,22 @@ __export(scenario_recipe_exports, {
3068
3503
  feedbackToScenario: () => feedbackToScenario,
3069
3504
  runScenarioRecipe: () => runScenarioRecipe
3070
3505
  });
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";
3506
+ import { readFile as readFile15 } from "fs/promises";
3507
+ import { join as join21 } from "path";
3508
+ import { tool as tool13 } from "ai";
3509
+ import { z as z16 } from "zod";
3075
3510
  function buildFinishTool3(requiredEntities, outputDir, onFinish) {
3076
- return tool12({
3511
+ return tool13({
3077
3512
  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")
3513
+ inputSchema: z16.object({
3514
+ summary: z16.string().describe("Summary of the scenario"),
3515
+ entityCount: z16.number().describe("Number of entity types in the scenario"),
3516
+ artifacts: z16.array(z16.string()).describe("Files written")
3082
3517
  }),
3083
3518
  execute: async (input) => {
3084
3519
  let content;
3085
3520
  try {
3086
- content = await readFile13(join19(outputDir, "scenarios.md"), "utf-8");
3521
+ content = await readFile15(join21(outputDir, "scenarios.md"), "utf-8");
3087
3522
  } catch {
3088
3523
  return { error: "Cannot finish: scenarios.md not found. Write it first." };
3089
3524
  }
@@ -3118,7 +3553,10 @@ async function runScenarioRecipe(input) {
3118
3553
  const model = getModel(input.modelId);
3119
3554
  let result;
3120
3555
  const { logger, onStepFinish } = buildDefaultStepLogger("scenario", 40);
3121
- const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + formatRetryGuidance(input.retryGuidance);
3556
+ const scopeBlock = input.scopeHint != null ? `
3557
+ ${input.scopeHint}
3558
+ ` : "";
3559
+ const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + scopeBlock + formatRetryGuidance(input.retryGuidance);
3122
3560
  const requiredEntities = await parseEntityNames(input.outputDir);
3123
3561
  const entityListBlock = requiredEntities.length > 0 ? `
3124
3562
  ## Required entities (${requiredEntities.length} total - ALL must appear in the scenario)
@@ -3180,9 +3618,9 @@ When done with changes, call finish again.`;
3180
3618
  }
3181
3619
  });
3182
3620
  if (!reviewed) {
3183
- const scenariosPath = join19(input.outputDir, "scenarios.md");
3621
+ const scenariosPath = join21(input.outputDir, "scenarios.md");
3184
3622
  try {
3185
- await readFile13(scenariosPath, "utf-8");
3623
+ await readFile15(scenariosPath, "utf-8");
3186
3624
  return {
3187
3625
  success: true,
3188
3626
  artifacts: ["scenarios.md"],
@@ -3243,7 +3681,7 @@ var init_scenario_recipe = __esm({
3243
3681
 
3244
3682
  // src/agents/04-recipe-builder/entity-relevance.ts
3245
3683
  import { Output, generateText } from "ai";
3246
- import { z as z15 } from "zod";
3684
+ import { z as z17 } from "zod";
3247
3685
  async function callRanker(model, prompt) {
3248
3686
  const result = await generateText({
3249
3687
  model,
@@ -3331,19 +3769,19 @@ var init_entity_relevance = __esm({
3331
3769
  init_esm_shims();
3332
3770
  init_errors();
3333
3771
  init_model();
3334
- rankedSchema = z15.object({
3335
- ranked: z15.array(z15.string()).describe("Every entity name, ordered most-important first.")
3772
+ rankedSchema = z17.object({
3773
+ ranked: z17.array(z17.string()).describe("Every entity name, ordered most-important first.")
3336
3774
  });
3337
3775
  }
3338
3776
  });
3339
3777
 
3340
3778
  // src/core/detect-pkg-manager.ts
3341
3779
  import { existsSync as existsSync2 } from "fs";
3342
- import { join as join20 } from "path";
3780
+ import { join as join22 } from "path";
3343
3781
  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";
3782
+ if (existsSync2(join22(projectRoot, "bun.lock")) || existsSync2(join22(projectRoot, "bun.lockb"))) return "bun";
3783
+ if (existsSync2(join22(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
3784
+ if (existsSync2(join22(projectRoot, "yarn.lock"))) return "yarn";
3347
3785
  return "npm";
3348
3786
  }
3349
3787
  function installCommand(pm, ...packages) {
@@ -3404,33 +3842,17 @@ var init_highlight = __esm({
3404
3842
  title: "\x1B[36m",
3405
3843
  literal: "\x1B[35m",
3406
3844
  attr: "",
3407
- params: "",
3408
- function: "\x1B[36m",
3409
- property: "",
3410
- punctuation: "\x1B[2m",
3411
- operator: "",
3412
- variable: "",
3413
- subst: "\x1B[36m",
3414
- "template-variable": "\x1B[36m",
3415
- meta: "\x1B[2m",
3416
- regexp: "\x1B[31m"
3417
- };
3418
- }
3419
- });
3420
-
3421
- // src/core/to-record.ts
3422
- function toRecord(value) {
3423
- if (typeof value !== "object" || value === null) return {};
3424
- const record = {};
3425
- for (const [key, entry] of Object.entries(value)) {
3426
- record[key] = entry;
3427
- }
3428
- return record;
3429
- }
3430
- var init_to_record = __esm({
3431
- "src/core/to-record.ts"() {
3432
- "use strict";
3433
- init_esm_shims();
3845
+ params: "",
3846
+ function: "\x1B[36m",
3847
+ property: "",
3848
+ punctuation: "\x1B[2m",
3849
+ operator: "",
3850
+ variable: "",
3851
+ subst: "\x1B[36m",
3852
+ "template-variable": "\x1B[36m",
3853
+ meta: "\x1B[2m",
3854
+ regexp: "\x1B[31m"
3855
+ };
3434
3856
  }
3435
3857
  });
3436
3858
 
@@ -3569,7 +3991,7 @@ function validateRecipeAgainstSchema(recipe, schema) {
3569
3991
  }
3570
3992
  }
3571
3993
  const refs = /* @__PURE__ */ new Set();
3572
- collectRefs(record, refs);
3994
+ collectRefs2(record, refs);
3573
3995
  for (const alias of refs) {
3574
3996
  if (!declaredAliases.has(alias)) {
3575
3997
  problems.push({
@@ -3583,16 +4005,16 @@ function validateRecipeAgainstSchema(recipe, schema) {
3583
4005
  }
3584
4006
  return problems;
3585
4007
  }
3586
- function collectRefs(value, out) {
4008
+ function collectRefs2(value, out) {
3587
4009
  if (Array.isArray(value)) {
3588
- for (const v of value) collectRefs(v, out);
4010
+ for (const v of value) collectRefs2(v, out);
3589
4011
  } else if (value !== null && typeof value === "object") {
3590
4012
  const obj = toRecord(value);
3591
4013
  if (typeof obj._ref === "string") {
3592
4014
  out.add(obj._ref);
3593
4015
  return;
3594
4016
  }
3595
- for (const v of Object.values(obj)) collectRefs(v, out);
4017
+ for (const v of Object.values(obj)) collectRefs2(v, out);
3596
4018
  }
3597
4019
  }
3598
4020
  function formatValidationProblems(problems) {
@@ -3613,96 +4035,9 @@ var init_discover_schema = __esm({
3613
4035
  }
3614
4036
  });
3615
4037
 
3616
- // 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";
3619
- function collectRefs2(value, out) {
3620
- if (Array.isArray(value)) {
3621
- for (const v of value) collectRefs2(v, out);
3622
- } else if (value !== null && typeof value === "object") {
3623
- const obj = toRecord(value);
3624
- if (typeof obj._ref === "string") out.add(obj._ref);
3625
- for (const v of Object.values(obj)) collectRefs2(v, out);
3626
- }
3627
- }
3628
- function buildSingleEntityRecipe(entityName, models, entityOrder, allEntities) {
3629
- const modelMap = new Map(models.map((m) => [m.name, m]));
3630
- const aliasOwner = /* @__PURE__ */ new Map();
3631
- for (const [name, entity] of Object.entries(allEntities)) {
3632
- for (const rec of entity?.recipeData ?? []) {
3633
- if (typeof rec._alias === "string") aliasOwner.set(rec._alias, name);
3634
- }
3635
- }
3636
- const recipe = {};
3637
- const done = /* @__PURE__ */ new Set();
3638
- const onStack = /* @__PURE__ */ new Set();
3639
- function include(name) {
3640
- if (done.has(name) || onStack.has(name)) return;
3641
- onStack.add(name);
3642
- const records = allEntities[name]?.recipeData ?? [];
3643
- for (const dep of modelMap.get(name)?.created_by ?? []) {
3644
- if (entityOrder.includes(dep.owner)) include(dep.owner);
3645
- }
3646
- const refs = /* @__PURE__ */ new Set();
3647
- collectRefs2(records, refs);
3648
- for (const alias of refs) {
3649
- const owner = aliasOwner.get(alias);
3650
- if (owner && owner !== name) include(owner);
3651
- }
3652
- onStack.delete(name);
3653
- done.add(name);
3654
- if (records.length > 0) recipe[name] = records;
3655
- }
3656
- include(entityName);
3657
- return recipe;
3658
- }
3659
- function buildFullRecipe(entityOrder, allEntities) {
3660
- const recipe = {};
3661
- for (const name of entityOrder) {
3662
- const entity = allEntities[name];
3663
- if (entity?.recipeData && entity.recipeData.length > 0) {
3664
- recipe[name] = entity.recipeData;
3665
- }
3666
- }
3667
- return recipe;
3668
- }
3669
- function buildSubmittableRecipe(create, description) {
3670
- return {
3671
- version: 1,
3672
- source: {
3673
- discoverPath: "discover.json",
3674
- scenariosPath: "scenarios.md"
3675
- },
3676
- validationMode: "endpoint-lifecycle",
3677
- recipes: [
3678
- {
3679
- name: "standard",
3680
- description,
3681
- create,
3682
- validation: {
3683
- status: "validated",
3684
- method: "endpoint-up-down"
3685
- }
3686
- }
3687
- ]
3688
- };
3689
- }
3690
- async function saveRecipe(outputDir, recipe) {
3691
- await writeFile6(join21(outputDir, RECIPE_FILE), JSON.stringify(recipe, null, 2), "utf-8");
3692
- }
3693
- var RECIPE_FILE;
3694
- var init_recipe = __esm({
3695
- "src/agents/04-recipe-builder/recipe.ts"() {
3696
- "use strict";
3697
- init_esm_shims();
3698
- init_to_record();
3699
- RECIPE_FILE = "recipe.json";
3700
- }
3701
- });
3702
-
3703
4038
  // 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";
4039
+ import { readFile as readFile16, writeFile as writeFile8 } from "fs/promises";
4040
+ import { join as join23 } from "path";
3706
4041
  function adapterKey(a) {
3707
4042
  return `${a.language}:${a.framework}`;
3708
4043
  }
@@ -3724,7 +4059,7 @@ function initialRecipeState() {
3724
4059
  }
3725
4060
  async function loadRecipeState(outputDir) {
3726
4061
  try {
3727
- const raw = await readFile15(join22(outputDir, STATE_FILE2), "utf-8");
4062
+ const raw = await readFile16(join23(outputDir, STATE_FILE2), "utf-8");
3728
4063
  const parsed = JSON.parse(raw);
3729
4064
  return parsed;
3730
4065
  } catch {
@@ -3732,7 +4067,7 @@ async function loadRecipeState(outputDir) {
3732
4067
  }
3733
4068
  }
3734
4069
  async function saveRecipeState(outputDir, state) {
3735
- await writeFile7(join22(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
4070
+ await writeFile8(join23(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
3736
4071
  }
3737
4072
  var ALL_ADAPTERS, ADAPTER_HINTS, STATE_FILE2;
3738
4073
  var init_state = __esm({
@@ -3804,7 +4139,7 @@ var init_state = __esm({
3804
4139
 
3805
4140
  // src/agents/04-recipe-builder/phases/failure-classifier.ts
3806
4141
  import { Output as Output2, generateText as generateText2 } from "ai";
3807
- import { z as z16 } from "zod";
4142
+ import { z as z18 } from "zod";
3808
4143
  function buildClassifierPrompt(args) {
3809
4144
  const errorText = typeof args.error === "string" ? args.error : JSON.stringify(args.error, null, 2);
3810
4145
  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 +4199,11 @@ var init_failure_classifier = __esm({
3864
4199
  init_esm_shims();
3865
4200
  init_errors();
3866
4201
  init_model();
3867
- classificationSchema = z16.object({
3868
- side: z16.enum(["recipe", "implementation", "unclear"]).describe(
4202
+ classificationSchema = z18.object({
4203
+ side: z18.enum(["recipe", "implementation", "unclear"]).describe(
3869
4204
  "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
4205
  ),
3871
- reason: z16.string().describe("One short, plain-language sentence explaining the verdict for the user. No code, no jargon dumps.")
4206
+ reason: z18.string().describe("One short, plain-language sentence explaining the verdict for the user. No code, no jargon dumps.")
3872
4207
  });
3873
4208
  PRIMER = `## Background - what you are looking at
3874
4209
 
@@ -3883,13 +4218,13 @@ So a failure has exactly two possible origins, and your only job is to tell them
3883
4218
  });
3884
4219
 
3885
4220
  // src/agents/04-recipe-builder/phases/entity-loop.ts
3886
- import { writeFile as writeFile8, readFile as readFile16 } from "fs/promises";
4221
+ import { writeFile as writeFile9, readFile as readFile17 } from "fs/promises";
3887
4222
  import { tmpdir } from "os";
3888
- import { join as join23 } from "path";
3889
- import * as p4 from "@clack/prompts";
3890
- import { tool as tool13 } from "ai";
4223
+ import { join as join24 } from "path";
4224
+ import * as p5 from "@clack/prompts";
4225
+ import { tool as tool14 } from "ai";
3891
4226
  import spawn2 from "cross-spawn";
3892
- import { z as z17 } from "zod";
4227
+ import { z as z19 } from "zod";
3893
4228
  function summarizeCompletedAliases(completedEntities, excludeName) {
3894
4229
  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
4230
  }
@@ -3904,10 +4239,10 @@ function summarizeEntityAudit(model) {
3904
4239
  async function proposeRecipeData(entityName, entityIndex, totalEntities, model, outputDir, _projectRoot, completedEntities, schemaSpec) {
3905
4240
  let result;
3906
4241
  const { logger, onStepFinish } = buildDefaultStepLogger(`propose:${entityName}`, 20);
3907
- const finishTool = tool13({
4242
+ const finishTool = tool14({
3908
4243
  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")
4244
+ inputSchema: z19.object({
4245
+ records: z19.array(z19.record(z19.string(), z19.unknown())).describe("Array of record objects for this entity")
3911
4246
  }),
3912
4247
  execute: async (input) => {
3913
4248
  result = input.records;
@@ -3950,10 +4285,10 @@ Call finish with the JSON array of records.`;
3950
4285
  }
3951
4286
  async function reviseRecipeData(entityName, entityIndex, totalEntities, current, feedback, model, outputDir, completedEntities, schemaSpec) {
3952
4287
  let revised;
3953
- const finishTool = tool13({
4288
+ const finishTool = tool14({
3954
4289
  description: "Submit the fixed recipe data.",
3955
- inputSchema: z17.object({
3956
- records: z17.array(z17.record(z17.string(), z17.unknown()))
4290
+ inputSchema: z19.object({
4291
+ records: z19.array(z19.record(z19.string(), z19.unknown()))
3957
4292
  }),
3958
4293
  execute: async (input) => {
3959
4294
  revised = input.records;
@@ -4002,19 +4337,19 @@ Read scenarios.md and entity-audit.md to understand the correct aliases and sche
4002
4337
  );
4003
4338
  logger.summary();
4004
4339
  if (revised) {
4005
- p4.note(JSON.stringify(revised, null, 2), `Fixed data for ${entityName}`, { format: codeNoteFormat });
4340
+ p5.note(JSON.stringify(revised, null, 2), `Fixed data for ${entityName}`, { format: codeNoteFormat });
4006
4341
  return revised;
4007
4342
  }
4008
- p4.log.warn("Could not auto-fix. Returning original data.");
4343
+ p5.log.warn("Could not auto-fix. Returning original data.");
4009
4344
  return current;
4010
4345
  }
4011
4346
  async function generateInstructions(entityName, entityIndex, totalEntities, isFirst, techStack, auditModel, recipeData, model, projectRoot, outputDir) {
4012
4347
  let result;
4013
4348
  const { logger, onStepFinish } = buildDefaultStepLogger(`instructions:${entityName}`, 15);
4014
- const finishTool = tool13({
4349
+ const finishTool = tool14({
4015
4350
  description: "Submit the implementation instructions.",
4016
- inputSchema: z17.object({
4017
- instructions: z17.string().describe("Complete, copy-pasteable implementation instructions")
4351
+ inputSchema: z19.object({
4352
+ instructions: z19.string().describe("Complete, copy-pasteable implementation instructions")
4018
4353
  }),
4019
4354
  execute: async (input) => {
4020
4355
  result = input.instructions;
@@ -4072,20 +4407,20 @@ Read the creation file from the project to understand the existing service/funct
4072
4407
  return result ?? "No instructions generated. Check the entity audit for creation_file and creation_function.";
4073
4408
  }
4074
4409
  async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed, model, outputDir, completedEntities, schemaSpec) {
4075
- p4.log.info(
4410
+ p5.log.info(
4076
4411
  `Legend for recipe fields:
4077
4412
  _alias - Internal ID used to reference this record from other entities (e.g., { "_ref": "org_1" })
4078
4413
  _ref - Reference to a record created by a parent entity's _alias
4079
4414
  All other fields are the actual data that will be inserted into your database.`
4080
4415
  );
4081
- p4.note(JSON.stringify(proposed, null, 2), `Proposed data for ${entityName} (${proposed.length} records)`, {
4416
+ p5.note(JSON.stringify(proposed, null, 2), `Proposed data for ${entityName} (${proposed.length} records)`, {
4082
4417
  format: codeNoteFormat
4083
4418
  });
4084
- p4.log.info(
4419
+ p5.log.info(
4085
4420
  "Review checklist:\n - Do field values match your real data patterns?\n - Are _ref references pointing to correct parent aliases?\n - Are enum fields varied across records (not all the same value)?\n - Are there enough records for your test scenarios?"
4086
4421
  );
4087
4422
  while (true) {
4088
- const action = await p4.select({
4423
+ const action = await p5.select({
4089
4424
  message: `[${entityIndex + 1}/${totalEntities}] ${entityName} - does this data look right?`,
4090
4425
  options: [
4091
4426
  { value: "keep", label: "Yes, keep" },
@@ -4093,40 +4428,40 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
4093
4428
  { value: "edit", label: "No, edit manually" }
4094
4429
  ]
4095
4430
  });
4096
- if (p4.isCancel(action)) throw new Error("Recipe review cancelled");
4431
+ if (p5.isCancel(action)) throw new Error("Recipe review cancelled");
4097
4432
  if (action === "keep") return proposed;
4098
4433
  if (action === "edit") {
4099
- const tmpPath = join23(tmpdir(), `autonoma-recipe-${entityName}.json`);
4100
- await writeFile8(tmpPath, JSON.stringify(proposed, null, 2), "utf-8");
4434
+ const tmpPath = join24(tmpdir(), `autonoma-recipe-${entityName}.json`);
4435
+ await writeFile9(tmpPath, JSON.stringify(proposed, null, 2), "utf-8");
4101
4436
  const env = readEnv();
4102
4437
  const editor = env.EDITOR ?? env.VISUAL ?? "vi";
4103
- p4.log.info(`Opening ${editor}... Save and close when done.`);
4104
- const launched = await new Promise((resolve5) => {
4438
+ p5.log.info(`Opening ${editor}... Save and close when done.`);
4439
+ const launched = await new Promise((resolve6) => {
4105
4440
  const proc = spawn2(editor, [tmpPath], { stdio: "inherit" });
4106
- proc.on("close", () => resolve5(true));
4441
+ proc.on("close", () => resolve6(true));
4107
4442
  proc.on("error", (err) => {
4108
- p4.log.error(
4443
+ p5.log.error(
4109
4444
  `Couldn't open ${editor} (${err.message}). Edit this file manually, then choose "edit" again: ${tmpPath}`
4110
4445
  );
4111
- resolve5(false);
4446
+ resolve6(false);
4112
4447
  });
4113
4448
  });
4114
4449
  if (!launched) continue;
4115
- const edited = await readFile16(tmpPath, "utf-8");
4450
+ const edited = await readFile17(tmpPath, "utf-8");
4116
4451
  try {
4117
4452
  proposed = JSON.parse(edited);
4118
- p4.note(JSON.stringify(proposed, null, 2), `Updated data for ${entityName}`, { format: codeNoteFormat });
4453
+ p5.note(JSON.stringify(proposed, null, 2), `Updated data for ${entityName}`, { format: codeNoteFormat });
4119
4454
  } catch (err) {
4120
- p4.log.error(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}. Try again.`);
4455
+ p5.log.error(`Invalid JSON: ${err instanceof Error ? err.message : String(err)}. Try again.`);
4121
4456
  }
4122
4457
  continue;
4123
4458
  }
4124
4459
  if (action === "chat") {
4125
- const feedback = await p4.text({
4460
+ const feedback = await p5.text({
4126
4461
  message: "What should be changed?",
4127
4462
  placeholder: "e.g., add more records, change field values, fix references..."
4128
4463
  });
4129
- if (p4.isCancel(feedback) || !feedback.trim()) continue;
4464
+ if (p5.isCancel(feedback) || !feedback.trim()) continue;
4130
4465
  proposed = await reviseRecipeData(
4131
4466
  entityName,
4132
4467
  entityIndex,
@@ -4166,14 +4501,14 @@ async function promptOnFailure(entityName, errorBody, ctx, phase, httpStatus) {
4166
4501
  });
4167
4502
  if (ctx.budget.attempts < MAX_AUTOFIX_ATTEMPTS) {
4168
4503
  ctx.budget.attempts++;
4169
- p4.log.info(`Triage: ${reason}`);
4170
- p4.log.info(
4504
+ p5.log.info(`Triage: ${reason}`);
4505
+ p5.log.info(
4171
4506
  `Handing the failure to the agent to fix from the error (attempt ${ctx.budget.attempts}/${MAX_AUTOFIX_ATTEMPTS})...`
4172
4507
  );
4173
4508
  return seedFeedbackFromError(errorContext, reason);
4174
4509
  }
4175
- p4.log.warn(`The agent tried ${MAX_AUTOFIX_ATTEMPTS}\xD7 without resolving it. Latest triage: ${reason}`);
4176
- const action = await p4.select({
4510
+ p5.log.warn(`The agent tried ${MAX_AUTOFIX_ATTEMPTS}\xD7 without resolving it. Latest triage: ${reason}`);
4511
+ const action = await p5.select({
4177
4512
  message: "What would you like to do?",
4178
4513
  options: [
4179
4514
  { value: "retry", label: "Yes, retry - I fixed my handler code", hint: "Send the same request again" },
@@ -4190,22 +4525,22 @@ async function promptOnFailure(entityName, errorBody, ctx, phase, httpStatus) {
4190
4525
  { value: "skip", label: "No, skip this entity", hint: "Move on to the next entity" }
4191
4526
  ]
4192
4527
  });
4193
- if (p4.isCancel(action)) throw new Error("Entity loop cancelled");
4528
+ if (p5.isCancel(action)) throw new Error("Entity loop cancelled");
4194
4529
  if (action === "skip") return "skip";
4195
4530
  if (action === "retry") return "retry";
4196
4531
  if (action === "autofix") {
4197
4532
  ctx.budget.attempts++;
4198
4533
  return seedFeedbackFromError(errorContext, reason);
4199
4534
  }
4200
- const fb = await p4.text({
4535
+ const fb = await p5.text({
4201
4536
  message: "What's wrong with the recipe data?",
4202
4537
  placeholder: "e.g. Transaction references acc_1 but Account uses account_1 as its alias"
4203
4538
  });
4204
- if (p4.isCancel(fb)) throw new Error("Entity loop cancelled");
4539
+ if (p5.isCancel(fb)) throw new Error("Entity loop cancelled");
4205
4540
  return { feedback: `${fb.trim()}${errorContext}` };
4206
4541
  }
4207
4542
  async function testUpDown(entityName, entityIndex, totalEntities, sdkConfig, recipe, grounding, discoverSchema) {
4208
- p4.log.info(
4543
+ p5.log.info(
4209
4544
  `Let's verify this factory works. We'll send a test request to create ${entityName}, then check the database.`
4210
4545
  );
4211
4546
  const failureCtx = { ...grounding, recipe };
@@ -4215,7 +4550,7 @@ async function testUpDown(entityName, entityIndex, totalEntities, sdkConfig, rec
4215
4550
  if (problems.length > 0) {
4216
4551
  const errorBody = `Recipe failed local schema validation against /discover (not sent to the server):
4217
4552
  ${formatValidationProblems(problems)}`;
4218
- p4.log.error(errorBody);
4553
+ p5.log.error(errorBody);
4219
4554
  const action = await promptOnFailure(entityName, errorBody, failureCtx, "create");
4220
4555
  if (action === "skip") return "skip";
4221
4556
  if (action === "retry") continue;
@@ -4223,12 +4558,12 @@ ${formatValidationProblems(problems)}`;
4223
4558
  }
4224
4559
  }
4225
4560
  const testRunId = `test-${Date.now()}`;
4226
- p4.log.step(`[${entityIndex + 1}/${totalEntities}] Sending UP request...`);
4561
+ p5.log.step(`[${entityIndex + 1}/${totalEntities}] Sending UP request...`);
4227
4562
  let upResult;
4228
4563
  try {
4229
4564
  upResult = await up(sdkConfig, recipe, testRunId);
4230
4565
  } catch (err) {
4231
- p4.log.error(`UP request failed:
4566
+ p5.log.error(`UP request failed:
4232
4567
  ${formatException(err)}`);
4233
4568
  const action = await promptOnFailure(entityName, formatException(err), failureCtx, "create");
4234
4569
  if (action === "skip") return "skip";
@@ -4236,29 +4571,29 @@ ${formatException(err)}`);
4236
4571
  return action;
4237
4572
  }
4238
4573
  if (!upResult.ok) {
4239
- p4.log.error(`UP failed (HTTP ${upResult.status}):`);
4574
+ p5.log.error(`UP failed (HTTP ${upResult.status}):`);
4240
4575
  console.log(JSON.stringify(upResult.body, null, 2));
4241
4576
  const action = await promptOnFailure(entityName, upResult.body, failureCtx, "create", upResult.status);
4242
4577
  if (action === "skip") return "skip";
4243
4578
  if (action === "retry") continue;
4244
4579
  return action;
4245
4580
  }
4246
- p4.log.success(`UP succeeded!`);
4581
+ p5.log.success(`UP succeeded!`);
4247
4582
  console.log(JSON.stringify(upResult.body, null, 2));
4248
4583
  const refsTokenValue = toRecord(upResult.body).refsToken;
4249
4584
  const refsToken = typeof refsTokenValue === "string" ? refsTokenValue : void 0;
4250
4585
  if (!refsToken) {
4251
- p4.log.error("No refsToken in UP response - cannot test DOWN.");
4586
+ p5.log.error("No refsToken in UP response - cannot test DOWN.");
4252
4587
  return "skip";
4253
4588
  }
4254
- p4.log.info("Now let's verify teardown works - leftover test data would pollute your database.");
4589
+ p5.log.info("Now let's verify teardown works - leftover test data would pollute your database.");
4255
4590
  while (true) {
4256
- p4.log.step(`[${entityIndex + 1}/${totalEntities}] Sending DOWN request...`);
4591
+ p5.log.step(`[${entityIndex + 1}/${totalEntities}] Sending DOWN request...`);
4257
4592
  let downResult;
4258
4593
  try {
4259
4594
  downResult = await down(sdkConfig, refsToken);
4260
4595
  } catch (err) {
4261
- p4.log.error(`DOWN request failed:
4596
+ p5.log.error(`DOWN request failed:
4262
4597
  ${formatException(err)}`);
4263
4598
  const action = await promptOnFailure(entityName, formatException(err), failureCtx, "teardown");
4264
4599
  if (action === "skip") return "skip";
@@ -4266,7 +4601,7 @@ ${formatException(err)}`);
4266
4601
  return action;
4267
4602
  }
4268
4603
  if (!downResult.ok) {
4269
- p4.log.error(`DOWN failed (HTTP ${downResult.status}):`);
4604
+ p5.log.error(`DOWN failed (HTTP ${downResult.status}):`);
4270
4605
  console.log(JSON.stringify(downResult.body, null, 2));
4271
4606
  const action = await promptOnFailure(
4272
4607
  entityName,
@@ -4279,7 +4614,7 @@ ${formatException(err)}`);
4279
4614
  if (action === "retry") continue;
4280
4615
  return action;
4281
4616
  }
4282
- p4.log.success("DOWN succeeded!");
4617
+ p5.log.success("DOWN succeeded!");
4283
4618
  return "success";
4284
4619
  }
4285
4620
  }
@@ -4296,7 +4631,7 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
4296
4631
  if (!schema) return {};
4297
4632
  return { schema, spec: renderModelSchema(schema, name) ?? void 0 };
4298
4633
  }
4299
- p4.log.info(
4634
+ p5.log.info(
4300
4635
  `We're going to set up your test data factories one entity at a time. Each factory teaches the Autonoma SDK how to create and tear down a specific type of record in YOUR database, using YOUR existing service functions.
4301
4636
 
4302
4637
  We'll test each one live before moving on - this way if something breaks, you'll know exactly which entity caused it. Let's start with the root entities (no dependencies), then work through the dependents.`
@@ -4305,7 +4640,7 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
4305
4640
  const entityName = state.entityOrder[i];
4306
4641
  const auditModel = modelMap.get(entityName);
4307
4642
  if (!auditModel) {
4308
- p4.log.warn(`[${i + 1}/${total}] ${entityName} - not found in entity audit, skipping`);
4643
+ p5.log.warn(`[${i + 1}/${total}] ${entityName} - not found in entity audit, skipping`);
4309
4644
  state.entities[entityName] = {
4310
4645
  entityName,
4311
4646
  status: "skipped",
@@ -4317,13 +4652,13 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
4317
4652
  }
4318
4653
  const existing = state.entities[entityName];
4319
4654
  if (existing?.status === "tested-down") {
4320
- p4.log.info(`[${i + 1}/${total}] ${entityName} - already done, skipping`);
4655
+ p5.log.info(`[${i + 1}/${total}] ${entityName} - already done, skipping`);
4321
4656
  continue;
4322
4657
  }
4323
4658
  const isRoot = auditModel.created_by.length === 0;
4324
4659
  const depInfo = isRoot ? "This is a root entity - no dependencies." : `This depends on: ${auditModel.created_by.map((d) => d.owner).join(", ")}`;
4325
- p4.log.step(`[${i + 1}/${total}] ${entityName}`);
4326
- p4.log.info(depInfo);
4660
+ p5.log.step(`[${i + 1}/${total}] ${entityName}`);
4661
+ p5.log.info(depInfo);
4327
4662
  const { spec: recipeSchemaSpec } = await loadLiveSchema(entityName);
4328
4663
  let recipeData = existing?.recipeData;
4329
4664
  if (!recipeData || existing?.status === "pending") {
@@ -4372,28 +4707,28 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
4372
4707
  outputDir
4373
4708
  );
4374
4709
  const DOCS_BASE2 = "https://docs.autonoma.app";
4375
- p4.log.info(
4710
+ p5.log.info(
4376
4711
  `Next: implement the ${entityName} factory. The block below is a copy-paste guide -
4377
4712
  paste it into Claude Code (or your AI assistant) and it will write the factory in your codebase.
4378
4713
  A factory teaches the Autonoma SDK how to create and tear down ${entityName} records using your app's own code.
4379
4714
  Keep it local for now: implement it, run your app on localhost, and we'll test it live here. You deploy later.`
4380
4715
  );
4381
- p4.note(instructions, `Implementation guide for ${entityName} (paste into your AI assistant)`, {
4716
+ p5.note(instructions, `Implementation guide for ${entityName} (paste into your AI assistant)`, {
4382
4717
  format: codeNoteFormat
4383
4718
  });
4384
- p4.log.info(`Autonoma SDK docs: ${DOCS_BASE2}/sdk/environment-factory`);
4719
+ p5.log.info(`Autonoma SDK docs: ${DOCS_BASE2}/sdk/environment-factory`);
4385
4720
  if (i === 0) {
4386
- p4.log.info(
4721
+ p5.log.info(
4387
4722
  "This is your first factory - the guide includes one-time SDK setup. Later entities only need the factory function."
4388
4723
  );
4389
4724
  }
4390
4725
  notify("Autonoma", `${entityName} - implementation ready, waiting for you`);
4391
- const ready = await p4.confirm({
4726
+ const ready = await p5.confirm({
4392
4727
  message: `[${i + 1}/${total}] Is your app running locally with the ${entityName} factory wired up?`
4393
4728
  });
4394
- if (p4.isCancel(ready)) throw new Error("Entity loop cancelled");
4729
+ if (p5.isCancel(ready)) throw new Error("Entity loop cancelled");
4395
4730
  if (!ready) {
4396
- p4.log.info("Take your time implementing. Run again with --resume to continue from here.");
4731
+ p5.log.info("Take your time implementing. Run again with --resume to continue from here.");
4397
4732
  return;
4398
4733
  }
4399
4734
  }
@@ -4404,41 +4739,41 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
4404
4739
  const secret = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
4405
4740
  state.sharedSecret = secret;
4406
4741
  await saveRecipeState(outputDir, state);
4407
- await writeFile8(
4408
- join23(outputDir, "autonoma-config.json"),
4742
+ await writeFile9(
4743
+ join24(outputDir, "autonoma-config.json"),
4409
4744
  JSON.stringify({ sharedSecret: secret, endpointUrl: state.sdkEndpointUrl }, null, 2),
4410
4745
  "utf-8"
4411
4746
  );
4412
- p4.note(
4747
+ p5.note(
4413
4748
  `AUTONOMA_SHARED_SECRET=${secret}
4414
4749
 
4415
4750
  Add this to your server's .env file and restart it.
4416
4751
  This is a 64-character hex key used for HMAC-SHA256 request signing.
4417
4752
  The same value must be set in both your server and the Autonoma dashboard.
4418
4753
 
4419
- Saved to: ${join23(outputDir, "autonoma-config.json")}`,
4754
+ Saved to: ${join24(outputDir, "autonoma-config.json")}`,
4420
4755
  "Shared secret generated"
4421
4756
  );
4422
- const secretReady = await p4.confirm({
4757
+ const secretReady = await p5.confirm({
4423
4758
  message: "Did you add the secret to your .env and restart the server?"
4424
4759
  });
4425
- if (p4.isCancel(secretReady)) throw new Error("Entity loop cancelled");
4760
+ if (p5.isCancel(secretReady)) throw new Error("Entity loop cancelled");
4426
4761
  if (!secretReady) {
4427
- p4.log.info("Add the secret and run again with --resume to continue.");
4762
+ p5.log.info("Add the secret and run again with --resume to continue.");
4428
4763
  return;
4429
4764
  }
4430
4765
  }
4431
4766
  if (!state.sdkEndpointUrl) {
4432
- const url = await p4.text({
4767
+ const url = await p5.text({
4433
4768
  message: "What's your SDK endpoint URL?",
4434
4769
  placeholder: "http://localhost:3000/api/autonoma",
4435
4770
  defaultValue: "http://localhost:3000/api/autonoma"
4436
4771
  });
4437
- if (p4.isCancel(url)) throw new Error("Entity loop cancelled");
4772
+ if (p5.isCancel(url)) throw new Error("Entity loop cancelled");
4438
4773
  state.sdkEndpointUrl = url.trim() || "http://localhost:3000/api/autonoma";
4439
4774
  await saveRecipeState(outputDir, state);
4440
- await writeFile8(
4441
- join23(outputDir, "autonoma-config.json"),
4775
+ await writeFile9(
4776
+ join24(outputDir, "autonoma-config.json"),
4442
4777
  JSON.stringify({ sharedSecret: state.sharedSecret, endpointUrl: state.sdkEndpointUrl }, null, 2),
4443
4778
  "utf-8"
4444
4779
  );
@@ -4470,15 +4805,15 @@ Saved to: ${join23(outputDir, "autonoma-config.json")}`,
4470
4805
  );
4471
4806
  if (testResult === "success") {
4472
4807
  state.entities[entityName].status = "tested-down";
4473
- p4.log.success(`[${i + 1}/${total}] ${entityName} - factory verified`);
4808
+ p5.log.success(`[${i + 1}/${total}] ${entityName} - factory verified`);
4474
4809
  testDone = true;
4475
4810
  } else if (testResult === "skip") {
4476
4811
  state.entities[entityName].status = "skipped";
4477
4812
  state.entities[entityName].errorLog.push("UP/DOWN test skipped by user");
4478
- p4.log.warn(`[${i + 1}/${total}] ${entityName} - skipped, continuing to next entity`);
4813
+ p5.log.warn(`[${i + 1}/${total}] ${entityName} - skipped, continuing to next entity`);
4479
4814
  testDone = true;
4480
4815
  } else {
4481
- p4.log.info(`Re-generating recipe data for ${entityName} based on your feedback...`);
4816
+ p5.log.info(`Re-generating recipe data for ${entityName} based on your feedback...`);
4482
4817
  const revised = await reviseRecipeData(
4483
4818
  entityName,
4484
4819
  i,
@@ -4553,15 +4888,15 @@ When done, call finish with the instructions text.`;
4553
4888
  });
4554
4889
 
4555
4890
  // src/agents/04-recipe-builder/phases/full-validation.ts
4556
- import * as p5 from "@clack/prompts";
4557
- import { tool as tool14 } from "ai";
4558
- import { z as z18 } from "zod";
4891
+ import * as p6 from "@clack/prompts";
4892
+ import { tool as tool15 } from "ai";
4893
+ import { z as z20 } from "zod";
4559
4894
  async function reviseFullRecipe(current, feedback, model, outputDir, entityOrder, schemaSpec) {
4560
4895
  let revised;
4561
- const finishTool = tool14({
4896
+ const finishTool = tool15({
4562
4897
  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())))
4898
+ inputSchema: z20.object({
4899
+ recipe: z20.record(z20.string(), z20.array(z20.record(z20.string(), z20.unknown())))
4565
4900
  }),
4566
4901
  execute: async (input) => {
4567
4902
  revised = input.recipe;
@@ -4612,35 +4947,35 @@ Revise the recipe to address the feedback, then call finish with the complete up
4612
4947
  }
4613
4948
  async function teardown(sdkConfig, refsToken, successMessage) {
4614
4949
  if (!refsToken) return true;
4615
- p5.log.step("[Full validation] Tearing down all entities...");
4950
+ p6.log.step("[Full validation] Tearing down all entities...");
4616
4951
  let downResult;
4617
4952
  try {
4618
4953
  downResult = await down(sdkConfig, refsToken);
4619
4954
  } catch (err) {
4620
- p5.log.error(`Full DOWN request failed:
4955
+ p6.log.error(`Full DOWN request failed:
4621
4956
  ${formatException(err)}`);
4622
4957
  return false;
4623
4958
  }
4624
4959
  if (!downResult.ok) {
4625
- p5.log.error(`Full DOWN failed (HTTP ${downResult.status}):`);
4960
+ p6.log.error(`Full DOWN failed (HTTP ${downResult.status}):`);
4626
4961
  console.log(JSON.stringify(downResult.body, null, 2));
4627
4962
  return false;
4628
4963
  }
4629
- p5.log.success(successMessage);
4964
+ p6.log.success(successMessage);
4630
4965
  return true;
4631
4966
  }
4632
4967
  async function runFullValidation(state, _models, outputDir, model) {
4633
4968
  const total = state.entityOrder.length;
4634
- p5.log.info(
4969
+ p6.log.info(
4635
4970
  `All individual factories work. Now let's create EVERYTHING together and verify the app looks right with a full dataset. This is the recipe that will run before every test execution.`
4636
4971
  );
4637
4972
  if (!state.sdkEndpointUrl) {
4638
- const url = await p5.text({
4973
+ const url = await p6.text({
4639
4974
  message: "What's your SDK endpoint URL?",
4640
4975
  placeholder: "http://localhost:3000/api/autonoma",
4641
4976
  defaultValue: "http://localhost:3000/api/autonoma"
4642
4977
  });
4643
- if (p5.isCancel(url)) throw new Error("Cancelled");
4978
+ if (p6.isCancel(url)) throw new Error("Cancelled");
4644
4979
  state.sdkEndpointUrl = url.trim() || "http://localhost:3000/api/autonoma";
4645
4980
  await saveRecipeState(outputDir, state);
4646
4981
  }
@@ -4655,69 +4990,69 @@ async function runFullValidation(state, _models, outputDir, model) {
4655
4990
  if (discoverSchema) {
4656
4991
  const problems = validateRecipeAgainstSchema(fullRecipe, discoverSchema);
4657
4992
  if (problems.length > 0) {
4658
- p5.log.warn(
4993
+ p6.log.warn(
4659
4994
  `Heads up - the recipe has likely schema problems (from /discover); the full UP may fail:
4660
4995
  ${formatValidationProblems(problems)}`
4661
4996
  );
4662
4997
  }
4663
4998
  }
4664
4999
  const testRunId = `full-${Date.now()}`;
4665
- p5.log.step(`[Full validation] Creating all ${total} entities...`);
5000
+ p6.log.step(`[Full validation] Creating all ${total} entities...`);
4666
5001
  let upResult;
4667
5002
  try {
4668
5003
  upResult = await up(sdkConfig, fullRecipe, testRunId);
4669
5004
  } catch (err) {
4670
- p5.log.error(`Full UP request failed:
5005
+ p6.log.error(`Full UP request failed:
4671
5006
  ${formatException(err)}`);
4672
5007
  notify("Autonoma", "Full validation UP failed, action needed");
4673
- const action = await p5.select({
5008
+ const action = await p6.select({
4674
5009
  message: "What would you like to do?",
4675
5010
  options: [
4676
5011
  { value: "retry", label: "Yes, retry - I fixed it", hint: "Send the request again" },
4677
5012
  { value: "skip", label: "No, skip full validation", hint: "Continue to test generation" }
4678
5013
  ]
4679
5014
  });
4680
- if (p5.isCancel(action)) throw new Error("Cancelled");
5015
+ if (p6.isCancel(action)) throw new Error("Cancelled");
4681
5016
  if (action === "skip") return false;
4682
5017
  continue;
4683
5018
  }
4684
5019
  if (!upResult.ok) {
4685
- p5.log.error(`Full UP failed (HTTP ${upResult.status}):`);
5020
+ p6.log.error(`Full UP failed (HTTP ${upResult.status}):`);
4686
5021
  console.log(JSON.stringify(upResult.body, null, 2));
4687
5022
  notify("Autonoma", "Full validation UP failed, action needed");
4688
- const action = await p5.select({
5023
+ const action = await p6.select({
4689
5024
  message: "What would you like to do?",
4690
5025
  options: [
4691
5026
  { value: "retry", label: "Yes, retry - I fixed it", hint: "Send the request again" },
4692
5027
  { value: "skip", label: "No, skip full validation", hint: "Continue to test generation" }
4693
5028
  ]
4694
5029
  });
4695
- if (p5.isCancel(action)) throw new Error("Cancelled");
5030
+ if (p6.isCancel(action)) throw new Error("Cancelled");
4696
5031
  if (action === "skip") return false;
4697
5032
  continue;
4698
5033
  }
4699
- p5.log.success("Full UP succeeded!");
5034
+ p6.log.success("Full UP succeeded!");
4700
5035
  const body = toRecord(upResult.body);
4701
5036
  const refsToken = typeof body.refsToken === "string" ? body.refsToken : void 0;
4702
5037
  const auth = body.auth != null && typeof body.auth === "object" ? toRecord(body.auth) : void 0;
4703
5038
  if (auth && Object.keys(auth).length > 0) {
4704
5039
  const authJson = JSON.stringify(auth, null, 2);
4705
5040
  const looksPlaceholder = authJson.includes("test-token") || authJson.includes("placeholder") || authJson.includes("todo");
4706
- p5.note(
5041
+ p6.note(
4707
5042
  authJson + "\n\nThese are the credentials your auth callback returns.\nThe test runner will use them to authenticate as the test user when executing tests." + (looksPlaceholder ? "\n\n\u26A0 This looks like a placeholder. Update your auth callback to return real credentials\n(a valid JWT, session cookie, or email/password) so the test runner can actually log in." : ""),
4708
5043
  "Auth credentials"
4709
5044
  );
4710
5045
  } else {
4711
- p5.log.warn(
5046
+ p6.log.warn(
4712
5047
  "No auth credentials returned. Your createHandler's auth callback must return credentials the test runner can use to log in (cookies, headers, or email/password). Without it, tests can't authenticate."
4713
5048
  );
4714
5049
  }
4715
- p5.log.info("Browse the app and check if the test data looks right.");
5050
+ p6.log.info("Browse the app and check if the test data looks right.");
4716
5051
  notify("Autonoma", "Full validation succeeded - review the app");
4717
- const looksGood = await p5.confirm({
5052
+ const looksGood = await p6.confirm({
4718
5053
  message: "Does the app look right with the test data?"
4719
5054
  });
4720
- if (p5.isCancel(looksGood)) throw new Error("Cancelled");
5055
+ if (p6.isCancel(looksGood)) throw new Error("Cancelled");
4721
5056
  const torndown = await teardown(
4722
5057
  sdkConfig,
4723
5058
  refsToken,
@@ -4725,15 +5060,15 @@ ${formatException(err)}`);
4725
5060
  );
4726
5061
  if (!torndown) return false;
4727
5062
  if (looksGood) return true;
4728
- const feedback = await p5.text({
5063
+ const feedback = await p6.text({
4729
5064
  message: "What's wrong with the test data? Describe what to change.",
4730
5065
  placeholder: "e.g. accounts need realistic balances, transactions should reference the right account..."
4731
5066
  });
4732
- if (p5.isCancel(feedback) || !feedback.trim()) {
4733
- p5.log.info("No feedback given. You can edit recipe.json manually and re-run with --resume.");
5067
+ if (p6.isCancel(feedback) || !feedback.trim()) {
5068
+ p6.log.info("No feedback given. You can edit recipe.json manually and re-run with --resume.");
4734
5069
  return false;
4735
5070
  }
4736
- p5.log.info("Revising the full recipe based on your feedback...");
5071
+ p6.log.info("Revising the full recipe based on your feedback...");
4737
5072
  const revised = await reviseFullRecipe(
4738
5073
  fullRecipe,
4739
5074
  feedback.trim(),
@@ -4743,7 +5078,7 @@ ${formatException(err)}`);
4743
5078
  fullSchemaSpec
4744
5079
  );
4745
5080
  if (!revised) {
4746
- p5.log.warn("Couldn't revise automatically. Edit recipe.json manually and re-run with --resume.");
5081
+ p6.log.warn("Couldn't revise automatically. Edit recipe.json manually and re-run with --resume.");
4747
5082
  return false;
4748
5083
  }
4749
5084
  for (const [name, records] of Object.entries(revised)) {
@@ -4753,7 +5088,7 @@ ${formatException(err)}`);
4753
5088
  }
4754
5089
  await saveRecipeState(outputDir, state);
4755
5090
  fullRecipe = buildFullRecipe(state.entityOrder, state.entities);
4756
- p5.note(JSON.stringify(fullRecipe, null, 2), "Revised recipe - re-running full validation", {
5091
+ p6.note(JSON.stringify(fullRecipe, null, 2), "Revised recipe - re-running full validation", {
4757
5092
  format: codeNoteFormat
4758
5093
  });
4759
5094
  }
@@ -4775,59 +5110,20 @@ var init_full_validation = __esm({
4775
5110
  }
4776
5111
  });
4777
5112
 
4778
- // src/agents/04-recipe-builder/phases/submit.ts
4779
- import * as p6 from "@clack/prompts";
4780
- async function runSubmit(state, outputDir, autonomaApiUrl, autonomaApiToken, autonomaGenerationId) {
4781
- const fullCreate = buildFullRecipe(state.entityOrder, state.entities);
4782
- const recipe = buildSubmittableRecipe(fullCreate, "Standard test scenario with realistic data");
4783
- await saveRecipe(outputDir, recipe);
4784
- p6.log.success("Recipe saved to recipe.json");
4785
- if (!autonomaApiUrl || !autonomaApiToken || !autonomaGenerationId) {
4786
- p6.log.info(
4787
- "Autonoma API credentials not configured - recipe saved locally. Submit manually or configure AUTONOMA_API_URL, AUTONOMA_API_TOKEN, AUTONOMA_GENERATION_ID."
4788
- );
4789
- return "recipe.json";
4790
- }
4791
- const url = `${autonomaApiUrl}/v1/setup/setups/${autonomaGenerationId}/scenario-recipe-versions`;
4792
- p6.log.step("Submitting recipe to Autonoma...");
4793
- const res = await fetch(url, {
4794
- method: "POST",
4795
- headers: {
4796
- "Content-Type": "application/json",
4797
- Authorization: `Bearer ${autonomaApiToken}`
4798
- },
4799
- body: JSON.stringify(recipe)
4800
- });
4801
- if (res.ok) {
4802
- p6.log.success(`Recipe submitted successfully (HTTP ${res.status})`);
4803
- } else {
4804
- const text6 = await res.text();
4805
- p6.log.error(`Recipe submission failed (HTTP ${res.status}): ${text6}`);
4806
- }
4807
- return "recipe.json";
4808
- }
4809
- var init_submit = __esm({
4810
- "src/agents/04-recipe-builder/phases/submit.ts"() {
4811
- "use strict";
4812
- init_esm_shims();
4813
- init_recipe();
4814
- }
4815
- });
4816
-
4817
5113
  // src/agents/04-recipe-builder/phases/tech-detect.ts
4818
5114
  import * as p7 from "@clack/prompts";
4819
- import { tool as tool15 } from "ai";
4820
- import { z as z19 } from "zod";
5115
+ import { tool as tool16 } from "ai";
5116
+ import { z as z21 } from "zod";
4821
5117
  async function detectTechStack(projectRoot, modelId, nonInteractive) {
4822
5118
  const model = getModel(modelId);
4823
5119
  const ignorePatterns = await loadGitignorePatterns(projectRoot);
4824
5120
  let detected;
4825
5121
  const { logger, onStepFinish } = buildDefaultStepLogger("tech-detect", 10);
4826
- const finishTool = tool15({
5122
+ const finishTool = tool16({
4827
5123
  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(
5124
+ inputSchema: z21.object({
5125
+ language: z21.string().describe("Programming language: typescript, python, go, ruby, java, php, rust, elixir"),
5126
+ framework: z21.string().describe(
4831
5127
  "Web framework: express, node, hono, web, flask, fastapi, django, gin, rails, rack, spring, laravel, axum, actix, plug"
4832
5128
  )
4833
5129
  }),
@@ -4838,7 +5134,7 @@ async function detectTechStack(projectRoot, modelId, nonInteractive) {
4838
5134
  });
4839
5135
  const agentConfig = {
4840
5136
  id: "tech-detect",
4841
- systemPrompt: SYSTEM_PROMPT4,
5137
+ systemPrompt: SYSTEM_PROMPT5,
4842
5138
  model,
4843
5139
  maxSteps: 10,
4844
5140
  tools: (_heartbeat) => ({
@@ -4894,7 +5190,7 @@ async function detectTechStack(projectRoot, modelId, nonInteractive) {
4894
5190
  p7.log.success(`Using ${adapterLabel(adapter)} - SDK: ${adapter.sdkPackage}, Adapter: ${adapter.adapterPackage}`);
4895
5191
  return adapter;
4896
5192
  }
4897
- var DOCS_BASE, SYSTEM_PROMPT4;
5193
+ var DOCS_BASE, SYSTEM_PROMPT5;
4898
5194
  var init_tech_detect = __esm({
4899
5195
  "src/agents/04-recipe-builder/phases/tech-detect.ts"() {
4900
5196
  "use strict";
@@ -4905,7 +5201,7 @@ var init_tech_detect = __esm({
4905
5201
  init_tools();
4906
5202
  init_state();
4907
5203
  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.
5204
+ 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
5205
 
4910
5206
  Explore the project files to detect:
4911
5207
  1. The programming language (check package.json, requirements.txt, go.mod, Gemfile, pom.xml, composer.json, Cargo.toml, mix.exs)
@@ -4920,8 +5216,8 @@ var recipe_builder_exports = {};
4920
5216
  __export(recipe_builder_exports, {
4921
5217
  runRecipeBuilder: () => runRecipeBuilder
4922
5218
  });
4923
- import { readFile as readFile17 } from "fs/promises";
4924
- import { join as join24 } from "path";
5219
+ import { readFile as readFile18 } from "fs/promises";
5220
+ import { join as join25 } from "path";
4925
5221
  import * as p8 from "@clack/prompts";
4926
5222
  async function runRecipeBuilder(input) {
4927
5223
  const model = getModel(input.modelId);
@@ -4935,7 +5231,7 @@ async function runRecipeBuilder(input) {
4935
5231
  state.techStack = await detectTechStack(input.projectRoot, input.modelId, input.nonInteractive);
4936
5232
  let importanceRank;
4937
5233
  try {
4938
- const auditMarkdown = await readFile17(join24(input.outputDir, "entity-audit.md"), "utf-8");
5234
+ const auditMarkdown = await readFile18(join25(input.outputDir, "entity-audit.md"), "utf-8");
4939
5235
  importanceRank = await rankEntitiesByImportance(models, auditMarkdown, model);
4940
5236
  } catch {
4941
5237
  importanceRank = void 0;
@@ -4993,13 +5289,21 @@ async function runRecipeBuilder(input) {
4993
5289
  }
4994
5290
  if (state.phase === "submit") {
4995
5291
  const env = readEnv();
4996
- const recipePath = await runSubmit(
5292
+ const { recipePath, uploaded } = await runSubmit(
4997
5293
  state,
4998
5294
  input.outputDir,
4999
5295
  env.AUTONOMA_API_URL,
5000
5296
  env.AUTONOMA_API_TOKEN,
5001
5297
  env.AUTONOMA_GENERATION_ID
5002
5298
  );
5299
+ const uploadCredentialsPresent = env.AUTONOMA_API_URL != null && env.AUTONOMA_API_TOKEN != null && env.AUTONOMA_GENERATION_ID != null;
5300
+ if (uploadCredentialsPresent && !uploaded) {
5301
+ return {
5302
+ success: false,
5303
+ artifacts: [recipePath],
5304
+ summary: `Recipe was generated but not accepted by Autonoma. The recipe JSON was printed above - re-upload with \`npx @autonoma-ai/planner@latest upload\` (or run again with --resume).`
5305
+ };
5306
+ }
5003
5307
  state.phase = "done";
5004
5308
  await saveRecipeState(input.outputDir, state);
5005
5309
  return {
@@ -5031,21 +5335,21 @@ var init_recipe_builder = __esm({
5031
5335
  });
5032
5336
 
5033
5337
  // src/agents/05-test-generator/rubrics.ts
5034
- import { z as z20 } from "zod";
5338
+ import { z as z22 } from "zod";
5035
5339
  function reviewResultSchema(shape) {
5036
- return z20.object(shape);
5340
+ return z22.object(shape);
5037
5341
  }
5038
5342
  var dimensionResultSchema, reviewResultRecordSchema, structuralIntentRubric, flowCompletenessRubric, uiTextRubric, dataAccuracyRubric, ALL_RUBRICS;
5039
5343
  var init_rubrics = __esm({
5040
5344
  "src/agents/05-test-generator/rubrics.ts"() {
5041
5345
  "use strict";
5042
5346
  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")
5347
+ dimensionResultSchema = z22.object({
5348
+ pass: z22.boolean(),
5349
+ evidence: z22.string().describe("What you checked and found - cite file paths, line content, or specific strings"),
5350
+ suggestion: z22.string().optional().describe("What the planner agent should fix, if failing")
5047
5351
  });
5048
- reviewResultRecordSchema = z20.record(z20.string(), dimensionResultSchema);
5352
+ reviewResultRecordSchema = z22.record(z22.string(), dimensionResultSchema);
5049
5353
  structuralIntentRubric = {
5050
5354
  name: "structural-intent",
5051
5355
  maxSteps: 8,
@@ -5219,12 +5523,12 @@ When done reviewing, call finish with your structured evaluation.`
5219
5523
  // src/agents/05-test-generator/review-pass.ts
5220
5524
  import { basename as basename2 } from "path";
5221
5525
  import "ai";
5222
- import { tool as tool16 } from "ai";
5526
+ import { tool as tool17 } from "ai";
5223
5527
  async function runReviewPass(testContent, testPath, rubric, projectRoot, model, scenarioData) {
5224
5528
  let result;
5225
5529
  const agentLabel = `review:${rubric.name}:${basename2(testPath)}`;
5226
5530
  const { onStepFinish } = buildDefaultStepLogger(agentLabel, rubric.maxSteps);
5227
- const finishTool = tool16({
5531
+ const finishTool = tool17({
5228
5532
  description: "Submit your structured review. Every dimension must have evidence from your investigation.",
5229
5533
  inputSchema: rubric.resultSchema,
5230
5534
  execute: async (input) => {
@@ -5282,8 +5586,8 @@ var init_review_pass = __esm({
5282
5586
  });
5283
5587
 
5284
5588
  // 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";
5589
+ import { readFile as readFile19 } from "fs/promises";
5590
+ import { join as join26, relative as relative6, basename as basename3 } from "path";
5287
5591
  import "ai";
5288
5592
  import { glob as glob5 } from "glob";
5289
5593
  async function reviewSingleTest(testContent, testPath, projectRoot, model, scenarioData) {
@@ -5312,19 +5616,19 @@ async function reviewSingleTest(testContent, testPath, projectRoot, model, scena
5312
5616
  return merged;
5313
5617
  }
5314
5618
  async function runConsolidatedReview(outputDir, projectRoot, model) {
5315
- const testsDir = join25(outputDir, "qa-tests");
5619
+ const testsDir = join26(outputDir, "qa-tests");
5316
5620
  const logger = createStepLogger("review", 5);
5317
5621
  let scenarioData;
5318
5622
  try {
5319
- scenarioData = await readFile18(join25(outputDir, "scenarios.md"), "utf-8");
5623
+ scenarioData = await readFile19(join26(outputDir, "scenarios.md"), "utf-8");
5320
5624
  } catch {
5321
5625
  }
5322
- const testFiles = await glob5(join25(testsDir, "**/*.md"));
5626
+ const testFiles = await glob5(join26(testsDir, "**/*.md"));
5323
5627
  const tests = [];
5324
5628
  for (const testPath of testFiles) {
5325
5629
  if (basename3(testPath) === "INDEX.md") continue;
5326
5630
  if (testPath.includes("/_invalid/")) continue;
5327
- const content = await readFile18(testPath, "utf-8");
5631
+ const content = await readFile19(testPath, "utf-8");
5328
5632
  const flowMatch = content.match(/^---\n[\s\S]*?flow:\s*["']?([^"'\n]+)["']?\s*\n[\s\S]*?---/m);
5329
5633
  tests.push({
5330
5634
  path: testPath,
@@ -5407,17 +5711,17 @@ var init_review2 = __esm({
5407
5711
  });
5408
5712
 
5409
5713
  // 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";
5714
+ import { readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
5715
+ import { join as join27 } from "path";
5716
+ import { tool as tool18 } from "ai";
5717
+ import { z as z23 } from "zod";
5414
5718
  async function saveFeatures(outputDir, features) {
5415
5719
  const obj = Object.fromEntries(features);
5416
- await writeFile9(join26(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
5720
+ await writeFile10(join27(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
5417
5721
  }
5418
5722
  async function loadFeatures(outputDir) {
5419
5723
  try {
5420
- const raw = await readFile19(join26(outputDir, FEATURES_FILE), "utf-8");
5724
+ const raw = await readFile20(join27(outputDir, FEATURES_FILE), "utf-8");
5421
5725
  const obj = JSON.parse(raw);
5422
5726
  return new Map(Object.entries(obj));
5423
5727
  } catch {
@@ -5441,17 +5745,17 @@ ${pagesDescription}
5441
5745
  Process every page. Call add_feature for each sub-feature you discover. When done, call finish.`;
5442
5746
  const agentConfig = {
5443
5747
  id: "feature-discovery",
5444
- systemPrompt: SYSTEM_PROMPT5,
5748
+ systemPrompt: SYSTEM_PROMPT6,
5445
5749
  model,
5446
5750
  maxSteps: 300,
5447
5751
  tools: async (heartbeat) => {
5448
5752
  const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
5449
5753
  return {
5450
5754
  ...tools,
5451
- add_feature: tool17({
5755
+ add_feature: tool18({
5452
5756
  description: "Add a discovered sub-feature",
5453
5757
  inputSchema: Feature.extend({
5454
- id: z21.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
5758
+ id: z23.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
5455
5759
  }),
5456
5760
  execute: (featureInput) => {
5457
5761
  const { id, ...rest } = featureInput;
@@ -5463,19 +5767,19 @@ Process every page. Call add_feature for each sub-feature you discover. When don
5463
5767
  return `Feature "${id}" added (${collector.features.size} total)`;
5464
5768
  }
5465
5769
  }),
5466
- view_features: tool17({
5770
+ view_features: tool18({
5467
5771
  description: "View all discovered features so far",
5468
- inputSchema: z21.object({}),
5772
+ inputSchema: z23.object({}),
5469
5773
  execute: () => collector.viewFeatures()
5470
5774
  }),
5471
- view_pages: tool17({
5775
+ view_pages: tool18({
5472
5776
  description: "View the pages list to know what to analyze",
5473
- inputSchema: z21.object({}),
5777
+ inputSchema: z23.object({}),
5474
5778
  execute: () => pagesDescription
5475
5779
  }),
5476
- finish: tool17({
5780
+ finish: tool18({
5477
5781
  description: "Signal that feature discovery is complete",
5478
- inputSchema: z21.object({ summary: z21.string() }),
5782
+ inputSchema: z23.object({ summary: z23.string() }),
5479
5783
  execute: async (finishInput) => {
5480
5784
  result = {
5481
5785
  success: true,
@@ -5497,7 +5801,7 @@ Process every page. Call add_feature for each sub-feature you discover. When don
5497
5801
  }
5498
5802
  return collector.features;
5499
5803
  }
5500
- var FEATURES_FILE, Feature, FeatureCollector, SYSTEM_PROMPT5;
5804
+ var FEATURES_FILE, Feature, FeatureCollector, SYSTEM_PROMPT6;
5501
5805
  var init_b_feature_discovery = __esm({
5502
5806
  "src/agents/00b-feature-discovery/index.ts"() {
5503
5807
  "use strict";
@@ -5506,13 +5810,13 @@ var init_b_feature_discovery = __esm({
5506
5810
  init_model();
5507
5811
  init_tools();
5508
5812
  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")
5813
+ Feature = z23.object({
5814
+ name: z23.string().min(1).describe("Human-readable name (e.g. 'Settings > Notifications Tab', 'Create Project Modal')"),
5815
+ type: z23.enum(["tab", "modal", "form", "table", "wizard", "nested-route", "complex-component"]),
5816
+ parentPagePath: z23.string().min(1).describe("The page path this feature belongs to (from the pages list)"),
5817
+ sourceFiles: z23.array(z23.string()).min(1).describe("Relative paths to the source files for this sub-feature"),
5818
+ interactiveElements: z23.number().int().min(0).describe("Count of interactive elements found (buttons, inputs, toggles, etc.)"),
5819
+ description: z23.string().min(10).describe("What this sub-feature does")
5516
5820
  });
5517
5821
  FeatureCollector = class {
5518
5822
  features = /* @__PURE__ */ new Map();
@@ -5543,7 +5847,7 @@ ${page}:`);
5543
5847
  return lines.join("\n");
5544
5848
  }
5545
5849
  };
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.
5850
+ 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
5851
 
5548
5852
  You will be given a list of pages. For each page, you must:
5549
5853
  1. Read the page's source file
@@ -5595,16 +5899,16 @@ Use kebab-case IDs that indicate the parent page and feature type:
5595
5899
  });
5596
5900
 
5597
5901
  // 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";
5902
+ import { readFile as readFile21, writeFile as writeFile11 } from "fs/promises";
5903
+ import { join as join28 } from "path";
5600
5904
  async function saveBfsState(outputDir, state) {
5601
- const path3 = join27(outputDir, STATE_FILE3);
5602
- await writeFile10(path3, JSON.stringify(state.serialize(), null, 2), "utf-8");
5905
+ const path3 = join28(outputDir, STATE_FILE3);
5906
+ await writeFile11(path3, JSON.stringify(state.serialize(), null, 2), "utf-8");
5603
5907
  }
5604
5908
  async function loadBfsState(outputDir) {
5605
- const path3 = join27(outputDir, STATE_FILE3);
5909
+ const path3 = join28(outputDir, STATE_FILE3);
5606
5910
  try {
5607
- const raw = await readFile20(path3, "utf-8");
5911
+ const raw = await readFile21(path3, "utf-8");
5608
5912
  return CoverageState.deserialize(JSON.parse(raw));
5609
5913
  } catch {
5610
5914
  return void 0;
@@ -5695,12 +5999,12 @@ var init_graph = __esm({
5695
5999
  });
5696
6000
 
5697
6001
  // src/agents/05-test-generator/prompt.ts
5698
- var SYSTEM_PROMPT6;
6002
+ var SYSTEM_PROMPT7;
5699
6003
  var init_prompt4 = __esm({
5700
6004
  "src/agents/05-test-generator/prompt.ts"() {
5701
6005
  "use strict";
5702
6006
  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.
6007
+ 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
6008
 
5705
6009
  ## Your process
5706
6010
 
@@ -6131,11 +6435,11 @@ var init_validation = __esm({
6131
6435
  });
6132
6436
 
6133
6437
  // 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";
6438
+ import { mkdir as mkdir3, writeFile as writeFile12 } from "fs/promises";
6439
+ import { dirname as dirname3, join as join29 } from "path";
6440
+ import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as tool19, ToolLoopAgent as ToolLoopAgent3 } from "ai";
6137
6441
  import matter5 from "gray-matter";
6138
- import { z as z22 } from "zod";
6442
+ import { z as z24 } from "zod";
6139
6443
  function findForbiddenPlaceholder(stepsSection) {
6140
6444
  const placeholderPatterns = [
6141
6445
  { pattern: /Dynamic:\s/gi, name: '"Dynamic:" placeholder' },
@@ -6153,13 +6457,13 @@ function findForbiddenPlaceholder(stepsSection) {
6153
6457
  return void 0;
6154
6458
  }
6155
6459
  function buildWriteTestTool(state, outputDir) {
6156
- return tool18({
6460
+ return tool19({
6157
6461
  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")
6462
+ inputSchema: z24.object({
6463
+ folder: z24.string().describe("Subfolder name under qa-tests/"),
6464
+ filename: z24.string().describe("File name (e.g. login-valid-credentials.md)"),
6465
+ content: z24.string().describe("Full file content including YAML frontmatter"),
6466
+ nodeId: z24.string().describe("The FeatureNode ID this test belongs to")
6163
6467
  }),
6164
6468
  execute: async (input) => {
6165
6469
  const frontmatter = extractFrontmatter(input.content);
@@ -6202,11 +6506,11 @@ function buildWriteTestTool(state, outputDir) {
6202
6506
  error: `Test steps contain ${placeholder.name}: "${placeholder.match}". Use EXACT values from scenarios.md - not placeholders or examples.`
6203
6507
  };
6204
6508
  }
6205
- const relPath = join28("qa-tests", input.folder, input.filename);
6206
- const absPath = join28(outputDir, relPath);
6509
+ const relPath = join29("qa-tests", input.folder, input.filename);
6510
+ const absPath = join29(outputDir, relPath);
6207
6511
  try {
6208
6512
  await mkdir3(dirname3(absPath), { recursive: true });
6209
- await writeFile11(absPath, input.content, "utf-8");
6513
+ await writeFile12(absPath, input.content, "utf-8");
6210
6514
  state.markTested(input.nodeId, [relPath]);
6211
6515
  await saveBfsState(outputDir, state);
6212
6516
  return { path: relPath, title: parsed.data.title };
@@ -6218,16 +6522,16 @@ function buildWriteTestTool(state, outputDir) {
6218
6522
  });
6219
6523
  }
6220
6524
  function buildCreateFolderTool(outputDir) {
6221
- return tool18({
6525
+ return tool19({
6222
6526
  description: "Create a folder under qa-tests/ for organizing tests.",
6223
- inputSchema: z22.object({
6224
- folder: z22.string().describe("Folder name (kebab-case)")
6527
+ inputSchema: z24.object({
6528
+ folder: z24.string().describe("Folder name (kebab-case)")
6225
6529
  }),
6226
6530
  execute: async (input) => {
6227
- const absPath = join28(outputDir, "qa-tests", input.folder);
6531
+ const absPath = join29(outputDir, "qa-tests", input.folder);
6228
6532
  try {
6229
6533
  await mkdir3(absPath, { recursive: true });
6230
- return { path: join28("qa-tests", input.folder) };
6534
+ return { path: join29("qa-tests", input.folder) };
6231
6535
  } catch (err) {
6232
6536
  const message = err instanceof Error ? err.message : String(err);
6233
6537
  return { error: `Failed to create folder: ${message}` };
@@ -6236,9 +6540,9 @@ function buildCreateFolderTool(outputDir) {
6236
6540
  });
6237
6541
  }
6238
6542
  function buildNextNodeTool(state, outputDir) {
6239
- return tool18({
6543
+ return tool19({
6240
6544
  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({}),
6545
+ inputSchema: z24.object({}),
6242
6546
  execute: async () => {
6243
6547
  const next = state.nextNode();
6244
6548
  await saveBfsState(outputDir, state);
@@ -6265,9 +6569,9 @@ function buildNextNodeTool(state, outputDir) {
6265
6569
  });
6266
6570
  }
6267
6571
  function buildGetProgressTool(state) {
6268
- return tool18({
6572
+ return tool19({
6269
6573
  description: "Check how many nodes have been tested vs how many remain.",
6270
- inputSchema: z22.object({}),
6574
+ inputSchema: z24.object({}),
6271
6575
  execute: async () => {
6272
6576
  const stats = state.summary();
6273
6577
  const nodes = [...state.nodes.values()].map((n) => ({
@@ -6281,14 +6585,14 @@ function buildGetProgressTool(state) {
6281
6585
  });
6282
6586
  }
6283
6587
  function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
6284
- return tool18({
6588
+ return tool19({
6285
6589
  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")
6590
+ inputSchema: z24.object({
6591
+ instruction: z24.string().describe("What to research - be specific about files and what to look for")
6288
6592
  }),
6289
6593
  execute: async (input) => {
6290
- const resultSchema2 = z22.object({
6291
- findings: z22.string().describe("Summary of what was found")
6594
+ const resultSchema2 = z24.object({
6595
+ findings: z24.string().describe("Summary of what was found")
6292
6596
  });
6293
6597
  let result;
6294
6598
  const subagent = new ToolLoopAgent3({
@@ -6300,7 +6604,7 @@ function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
6300
6604
  glob: buildGlobTool(workingDirectory),
6301
6605
  grep: buildGrepTool(workingDirectory),
6302
6606
  read_file: buildReadFileTool(workingDirectory),
6303
- finish: tool18({
6607
+ finish: tool19({
6304
6608
  description: "Report your findings.",
6305
6609
  inputSchema: resultSchema2,
6306
6610
  execute: async (output) => {
@@ -6342,14 +6646,14 @@ var init_tools2 = __esm({
6342
6646
  init_tools();
6343
6647
  init_graph();
6344
6648
  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(
6649
+ testFrontmatterSchema = z24.object({
6650
+ title: z24.string().min(1),
6651
+ description: z24.string().min(1),
6652
+ intent: z24.string().min(30, "Intent must be at least 30 characters - describe the BEHAVIOR being tested, not the steps"),
6653
+ criticality: z24.enum(["critical", "high", "mid", "low"]),
6654
+ scenario: z24.string().min(1),
6655
+ flow: z24.string().min(1),
6656
+ verification: z24.string().min(
6353
6657
  20,
6354
6658
  "Verification must describe WHERE to navigate and WHAT to assert at the source of truth - not UI acknowledgments like toasts"
6355
6659
  )
@@ -6362,10 +6666,10 @@ var test_generator_exports = {};
6362
6666
  __export(test_generator_exports, {
6363
6667
  runTestGenerator: () => runTestGenerator
6364
6668
  });
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";
6669
+ import { mkdir as mkdir4, readFile as readFile22, rmdir, unlink, writeFile as writeFile13 } from "fs/promises";
6670
+ import { basename as basename4, join as join30 } from "path";
6671
+ import { tool as tool20 } from "ai";
6672
+ import { z as z25 } from "zod";
6369
6673
  import { glob as glob6 } from "glob";
6370
6674
  async function preseedQueue(state, projectRoot, pages, features) {
6371
6675
  let seeded = 0;
@@ -6413,10 +6717,10 @@ async function runTestGenerator(input) {
6413
6717
  const existingState = await loadBfsState(input.outputDir);
6414
6718
  const state = existingState ?? new CoverageState();
6415
6719
  let result;
6416
- const finishTool = tool19({
6720
+ const finishTool = tool20({
6417
6721
  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")
6722
+ inputSchema: z25.object({
6723
+ summary: z25.string().describe("Coverage summary")
6420
6724
  }),
6421
6725
  execute: async (finishInput) => {
6422
6726
  const stats = state.summary();
@@ -6445,7 +6749,7 @@ async function runTestGenerator(input) {
6445
6749
  });
6446
6750
  let kbContext = "";
6447
6751
  try {
6448
- const autonomaMd = await readFile21(join29(input.outputDir, "AUTONOMA.md"), "utf-8");
6752
+ const autonomaMd = await readFile22(join30(input.outputDir, "AUTONOMA.md"), "utf-8");
6449
6753
  kbContext += `
6450
6754
  ## Knowledge Base (AUTONOMA.md)
6451
6755
 
@@ -6454,7 +6758,7 @@ ${autonomaMd}
6454
6758
  } catch {
6455
6759
  }
6456
6760
  try {
6457
- const scenariosMd = await readFile21(join29(input.outputDir, "scenarios.md"), "utf-8");
6761
+ const scenariosMd = await readFile22(join30(input.outputDir, "scenarios.md"), "utf-8");
6458
6762
  kbContext += `
6459
6763
  ## Scenarios
6460
6764
 
@@ -6508,7 +6812,7 @@ Do NOT try to finish early. Process EVERY node via next_node until it returns do
6508
6812
  const listDirectoryFn = await buildListDirectoryTool(input.projectRoot);
6509
6813
  const agentConfig = {
6510
6814
  id: "test-generator",
6511
- systemPrompt: SYSTEM_PROMPT6,
6815
+ systemPrompt: SYSTEM_PROMPT7,
6512
6816
  model,
6513
6817
  maxSteps: CHUNK_STEPS,
6514
6818
  temperature: 0.3,
@@ -6624,20 +6928,20 @@ IMPORTANT: Do NOT try to finish early. Process every node via next_node until it
6624
6928
  }
6625
6929
  console.log(` Fix pass complete`);
6626
6930
  }
6627
- const allTestFiles = await glob6(join29(input.outputDir, "qa-tests", "**/*.md"));
6931
+ const allTestFiles = await glob6(join30(input.outputDir, "qa-tests", "**/*.md"));
6628
6932
  let markedInvalid = 0;
6629
6933
  for (const testPath of allTestFiles) {
6630
6934
  if (basename4(testPath) === "INDEX.md") continue;
6631
6935
  if (testPath.includes("/_invalid/")) continue;
6632
- const content = await readFile21(testPath, "utf-8");
6936
+ const content = await readFile22(testPath, "utf-8");
6633
6937
  const validation = validateTestContent(content);
6634
6938
  if (!validation.valid) {
6635
- const invalidDir = join29(input.outputDir, "qa-tests", "_invalid");
6939
+ const invalidDir = join30(input.outputDir, "qa-tests", "_invalid");
6636
6940
  await mkdir4(invalidDir, { recursive: true });
6637
- const dest = join29(invalidDir, basename4(testPath));
6941
+ const dest = join30(invalidDir, basename4(testPath));
6638
6942
  const annotated = `<!-- VALIDATION ERRORS: ${validation.errors.join("; ")} -->
6639
6943
  ${content}`;
6640
- await writeFile12(dest, annotated, "utf-8");
6944
+ await writeFile13(dest, annotated, "utf-8");
6641
6945
  await unlink(testPath);
6642
6946
  markedInvalid++;
6643
6947
  }
@@ -6645,7 +6949,7 @@ ${content}`;
6645
6949
  if (markedInvalid > 0) {
6646
6950
  console.log(` ${markedInvalid} tests still invalid after review cycles - moved to _invalid/`);
6647
6951
  }
6648
- const dirs = await glob6(join29(input.outputDir, "qa-tests", "**/"), {
6952
+ const dirs = await glob6(join30(input.outputDir, "qa-tests", "**/"), {
6649
6953
  dot: false
6650
6954
  });
6651
6955
  for (const dir of dirs.sort((a, b) => b.length - a.length)) {
@@ -6737,7 +7041,7 @@ async function generateIndex(outputDir, state) {
6737
7041
  for (const paths of state.testsWritten.values()) {
6738
7042
  for (const p10 of paths) {
6739
7043
  try {
6740
- const content2 = await readFile21(join29(outputDir, p10), "utf-8");
7044
+ const content2 = await readFile22(join30(outputDir, p10), "utf-8");
6741
7045
  const critMatch = content2.match(/criticality:\s*(\w+)/);
6742
7046
  const critVal = critMatch?.[1] ?? "";
6743
7047
  if (critCounts.has(critVal)) critCounts.set(critVal, (critCounts.get(critVal) ?? 0) + 1);
@@ -6782,28 +7086,28 @@ ${folders.map((f) => `| ${f.name} | ${f.test_count} |`).join("\n")}
6782
7086
 
6783
7087
  ${[...testsByFolder.entries()].flatMap(([_folder, tests]) => tests.map((t) => `- \`${t}\``)).join("\n")}
6784
7088
  `;
6785
- await writeFile12(join29(outputDir, "qa-tests", "INDEX.md"), content, "utf-8");
7089
+ await writeFile13(join30(outputDir, "qa-tests", "INDEX.md"), content, "utf-8");
6786
7090
  }
6787
7091
  async function generateJourneyTests(outputDir, model, projectRoot) {
6788
7092
  const logger = createStepLogger("journeys", 50);
6789
7093
  let autonomaMd = "";
6790
7094
  let scenariosMd = "";
6791
7095
  try {
6792
- autonomaMd = await readFile21(join29(outputDir, "AUTONOMA.md"), "utf-8");
7096
+ autonomaMd = await readFile22(join30(outputDir, "AUTONOMA.md"), "utf-8");
6793
7097
  } catch (err) {
6794
7098
  debugLog("AUTONOMA.md not present for journey generation", { err });
6795
7099
  }
6796
7100
  try {
6797
- scenariosMd = await readFile21(join29(outputDir, "scenarios.md"), "utf-8");
7101
+ scenariosMd = await readFile22(join30(outputDir, "scenarios.md"), "utf-8");
6798
7102
  } catch (err) {
6799
7103
  debugLog("scenarios.md not present for journey generation", { err });
6800
7104
  }
6801
7105
  if (!autonomaMd) return 0;
6802
- const existingTests = await glob6(join29(outputDir, "qa-tests", "**/*.md"));
7106
+ const existingTests = await glob6(join30(outputDir, "qa-tests", "**/*.md"));
6803
7107
  const existingTitles = [];
6804
7108
  for (const t of existingTests) {
6805
7109
  if (basename4(t) === "INDEX.md") continue;
6806
- const content = await readFile21(t, "utf-8");
7110
+ const content = await readFile22(t, "utf-8");
6807
7111
  const titleMatch = content.match(/title:\s*"([^"]+)"/);
6808
7112
  if (titleMatch) existingTitles.push(titleMatch[1]);
6809
7113
  }
@@ -6846,9 +7150,9 @@ Write 5-8 journey tests using the write_test tool with folder "journeys". Then c
6846
7150
  status: "queued"
6847
7151
  });
6848
7152
  let journeyResult;
6849
- const journeyFinish = tool19({
7153
+ const journeyFinish = tool20({
6850
7154
  description: "Signal journey generation is complete.",
6851
- inputSchema: z23.object({ summary: z23.string() }),
7155
+ inputSchema: z25.object({ summary: z25.string() }),
6852
7156
  execute: async (finishInput) => {
6853
7157
  journeyResult = {
6854
7158
  success: true,
@@ -6860,7 +7164,7 @@ Write 5-8 journey tests using the write_test tool with folder "journeys". Then c
6860
7164
  });
6861
7165
  const config = {
6862
7166
  id: "journey-gen",
6863
- systemPrompt: SYSTEM_PROMPT6,
7167
+ systemPrompt: SYSTEM_PROMPT7,
6864
7168
  model,
6865
7169
  maxSteps: 50,
6866
7170
  temperature: 0.3,
@@ -6937,23 +7241,24 @@ function ensureSupportedNode() {
6937
7241
  ensureSupportedNode();
6938
7242
 
6939
7243
  // src/index.ts
6940
- import { readFile as readFile22, writeFile as writeFile13 } from "fs/promises";
6941
- import { join as join30 } from "path";
7244
+ init_submit();
7245
+ import { readFile as readFile23, writeFile as writeFile14 } from "fs/promises";
7246
+ import { join as join31 } from "path";
6942
7247
  import * as p9 from "@clack/prompts";
6943
7248
 
6944
7249
  // src/config.ts
6945
7250
  init_esm_shims();
6946
7251
  import { readFileSync as readFileSync2 } from "fs";
6947
- import { resolve, join as join2 } from "path";
7252
+ import { resolve, join as join3 } from "path";
6948
7253
 
6949
7254
  // src/core/global-env.ts
6950
7255
  init_esm_shims();
6951
7256
  init_env();
6952
7257
  import { readFileSync } from "fs";
6953
7258
  import { homedir } from "os";
6954
- import { join } from "path";
6955
- var AUTONOMA_HOME = join(homedir(), ".autonoma");
6956
- var GLOBAL_ENV_PATH = join(AUTONOMA_HOME, ".env");
7259
+ import { join as join2 } from "path";
7260
+ var AUTONOMA_HOME = join2(homedir(), ".autonoma");
7261
+ var GLOBAL_ENV_PATH = join2(AUTONOMA_HOME, ".env");
6957
7262
  function parseEnvContent(content) {
6958
7263
  const out = {};
6959
7264
  for (const line of content.split("\n")) {
@@ -6989,7 +7294,7 @@ init_env();
6989
7294
  function loadProjectEnv(projectRoot) {
6990
7295
  let content;
6991
7296
  try {
6992
- content = readFileSync2(join2(projectRoot, ".env"), "utf-8");
7297
+ content = readFileSync2(join3(projectRoot, ".env"), "utf-8");
6993
7298
  } catch {
6994
7299
  return;
6995
7300
  }
@@ -7018,6 +7323,8 @@ function loadConfig(args) {
7018
7323
  projectRoot,
7019
7324
  projectSlug,
7020
7325
  modelId: args.model ?? env.OPENROUTER_MODEL,
7326
+ frontend: args.frontend,
7327
+ backends: args.backends,
7021
7328
  databaseUrl: env.DATABASE_URL,
7022
7329
  sdkEndpointUrl: env.SDK_ENDPOINT_URL,
7023
7330
  sharedSecret: env.AUTONOMA_SHARED_SECRET,
@@ -7092,6 +7399,50 @@ function installInterruptHandler(opts) {
7092
7399
  return iface;
7093
7400
  });
7094
7401
  }
7402
+ function installTerminationDiagnostics() {
7403
+ const memSnapshot = () => {
7404
+ const m = process.memoryUsage();
7405
+ const mb = (n) => Math.round(n / 1024 / 1024);
7406
+ return `rss=${mb(m.rss)}MB heapUsed=${mb(m.heapUsed)}MB heapTotal=${mb(m.heapTotal)}MB`;
7407
+ };
7408
+ for (const signal of ["SIGTERM", "SIGHUP"]) {
7409
+ const forcedCode = signal === "SIGTERM" ? 143 : 129;
7410
+ process.on(signal, () => {
7411
+ process.stderr.write(`
7412
+ [diagnostics] received ${signal} - external termination (${memSnapshot()})
7413
+ `);
7414
+ terminateGracefully(forcedCode);
7415
+ });
7416
+ }
7417
+ process.on("uncaughtException", (err) => {
7418
+ const detail = err instanceof Error ? err.stack ?? err.message : String(err);
7419
+ process.stderr.write(`
7420
+ [diagnostics] uncaughtException (${memSnapshot()}): ${detail}
7421
+ `);
7422
+ restoreTerminal();
7423
+ process.exit(1);
7424
+ });
7425
+ process.on("unhandledRejection", (reason) => {
7426
+ const detail = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
7427
+ process.stderr.write(`
7428
+ [diagnostics] unhandledRejection (${memSnapshot()}): ${detail}
7429
+ `);
7430
+ restoreTerminal();
7431
+ process.exit(1);
7432
+ });
7433
+ }
7434
+ function terminateGracefully(forcedCode) {
7435
+ if (quitting || onExit == null) {
7436
+ restoreTerminal();
7437
+ process.exit(forcedCode);
7438
+ }
7439
+ quitting = true;
7440
+ setTimeout(() => {
7441
+ restoreTerminal();
7442
+ process.exit(forcedCode);
7443
+ }, FORCE_EXIT_MS).unref?.();
7444
+ onExit(forcedCode);
7445
+ }
7095
7446
  function restoreTerminal() {
7096
7447
  try {
7097
7448
  if (process.stdin.isTTY) process.stdin.setRawMode(false);
@@ -7108,10 +7459,10 @@ init_model();
7108
7459
  init_esm_shims();
7109
7460
  import { mkdir } from "fs/promises";
7110
7461
  import { homedir as homedir3 } from "os";
7111
- import { join as join6 } from "path";
7112
- var AUTONOMA_HOME3 = join6(homedir3(), ".autonoma");
7462
+ import { join as join7 } from "path";
7463
+ var AUTONOMA_HOME3 = join7(homedir3(), ".autonoma");
7113
7464
  function getOutputDir(projectSlug) {
7114
- return join6(AUTONOMA_HOME3, projectSlug);
7465
+ return join7(AUTONOMA_HOME3, projectSlug);
7115
7466
  }
7116
7467
  async function ensureOutputDir(projectSlug) {
7117
7468
  const dir = getOutputDir(projectSlug);
@@ -7125,8 +7476,8 @@ init_env();
7125
7476
  // src/core/git.ts
7126
7477
  init_esm_shims();
7127
7478
  import { execFile } from "child_process";
7128
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
7129
- import { join as join7 } from "path";
7479
+ import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
7480
+ import { join as join8 } from "path";
7130
7481
  import { promisify } from "util";
7131
7482
  var execFileAsync = promisify(execFile);
7132
7483
  var GIT_INFO_FILE = ".git-info.json";
@@ -7150,11 +7501,11 @@ async function readGitInfo(projectRoot) {
7150
7501
  };
7151
7502
  }
7152
7503
  async function saveGitInfo(outputDir, info) {
7153
- await writeFile2(join7(outputDir, GIT_INFO_FILE), JSON.stringify(info, null, 2), "utf-8");
7504
+ await writeFile3(join8(outputDir, GIT_INFO_FILE), JSON.stringify(info, null, 2), "utf-8");
7154
7505
  }
7155
7506
  async function loadGitInfo(outputDir) {
7156
7507
  try {
7157
- const raw = await readFile2(join7(outputDir, GIT_INFO_FILE), "utf-8");
7508
+ const raw = await readFile3(join8(outputDir, GIT_INFO_FILE), "utf-8");
7158
7509
  const parsed = JSON.parse(raw);
7159
7510
  if (typeof parsed === "object" && parsed != null && "sha" in parsed && typeof parsed.sha === "string") {
7160
7511
  const branch = "branch" in parsed && typeof parsed.branch === "string" ? parsed.branch : void 0;
@@ -7169,15 +7520,31 @@ async function loadGitInfo(outputDir) {
7169
7520
 
7170
7521
  // src/index.ts
7171
7522
  init_notify();
7523
+ init_project_map();
7172
7524
 
7173
7525
  // src/core/state.ts
7174
7526
  init_esm_shims();
7175
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
7176
- import { join as join8 } from "path";
7527
+ init_debug();
7528
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
7529
+ import { join as join10 } from "path";
7530
+ import { z as z3 } from "zod";
7531
+ var StepStatusSchema = z3.enum(["pending", "running", "done", "failed", "paused"]);
7532
+ var PipelineStateSchema = z3.object({
7533
+ steps: z3.object({
7534
+ projectMapper: StepStatusSchema.default("done"),
7535
+ pagesFinder: StepStatusSchema.default("done"),
7536
+ kb: StepStatusSchema.default("done"),
7537
+ entityAudit: StepStatusSchema.default("done"),
7538
+ scenarioRecipe: StepStatusSchema.default("done"),
7539
+ recipeBuilder: StepStatusSchema.default("done"),
7540
+ testGenerator: StepStatusSchema.default("done")
7541
+ })
7542
+ });
7177
7543
  var STATE_FILE = ".pipeline-state.json";
7178
7544
  function initialState() {
7179
7545
  return {
7180
7546
  steps: {
7547
+ projectMapper: "pending",
7181
7548
  pagesFinder: "pending",
7182
7549
  kb: "pending",
7183
7550
  entityAudit: "pending",
@@ -7188,18 +7555,19 @@ function initialState() {
7188
7555
  };
7189
7556
  }
7190
7557
  async function loadState(outputDir) {
7191
- const path3 = join8(outputDir, STATE_FILE);
7558
+ const path3 = join10(outputDir, STATE_FILE);
7192
7559
  try {
7193
- const raw = await readFile3(path3, "utf-8");
7194
- const parsed = JSON.parse(raw);
7195
- return parsed;
7196
- } catch {
7560
+ const raw = await readFile5(path3, "utf-8");
7561
+ return PipelineStateSchema.parse(JSON.parse(raw));
7562
+ } catch (err) {
7563
+ const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
7564
+ if (!isMissingFile) debugLog("Failed to load pipeline state, starting fresh", { path: path3, err });
7197
7565
  return initialState();
7198
7566
  }
7199
7567
  }
7200
7568
  async function saveState(outputDir, state) {
7201
- const path3 = join8(outputDir, STATE_FILE);
7202
- await writeFile3(path3, JSON.stringify(state, null, 2), "utf-8");
7569
+ const path3 = join10(outputDir, STATE_FILE);
7570
+ await writeFile5(path3, JSON.stringify(state, null, 2), "utf-8");
7203
7571
  }
7204
7572
  async function markStep(outputDir, state, step, status) {
7205
7573
  const updated = {
@@ -7210,23 +7578,31 @@ async function markStep(outputDir, state, step, status) {
7210
7578
  return updated;
7211
7579
  }
7212
7580
  function nextPendingStep(state) {
7213
- const order = ["pagesFinder", "kb", "entityAudit", "scenarioRecipe", "recipeBuilder", "testGenerator"];
7581
+ const order = [
7582
+ "projectMapper",
7583
+ "pagesFinder",
7584
+ "kb",
7585
+ "entityAudit",
7586
+ "scenarioRecipe",
7587
+ "recipeBuilder",
7588
+ "testGenerator"
7589
+ ];
7214
7590
  return order.find((s) => state.steps[s] !== "done") ?? void 0;
7215
7591
  }
7216
7592
 
7217
7593
  // src/core/upload.ts
7218
7594
  init_esm_shims();
7219
7595
  init_debug();
7220
- import { readFile as readFile4 } from "fs/promises";
7221
- import { basename, join as join9, relative } from "path";
7222
- import * as p from "@clack/prompts";
7596
+ import { readFile as readFile6 } from "fs/promises";
7597
+ import { basename, join as join11, relative } from "path";
7598
+ import * as p2 from "@clack/prompts";
7223
7599
  import { glob } from "glob";
7224
7600
  var ARTIFACT_FILES = ["AUTONOMA.md", "scenarios.md", "entity-audit.md"];
7225
7601
  async function readArtifacts(outputDir) {
7226
7602
  const files = [];
7227
7603
  for (const name of ARTIFACT_FILES) {
7228
7604
  try {
7229
- const content = await readFile4(join9(outputDir, name), "utf-8");
7605
+ const content = await readFile6(join11(outputDir, name), "utf-8");
7230
7606
  files.push({ name, content });
7231
7607
  } catch (err) {
7232
7608
  debugLog(`Artifact ${name} not on disk; skipping upload`, { err });
@@ -7235,13 +7611,13 @@ async function readArtifacts(outputDir) {
7235
7611
  return files;
7236
7612
  }
7237
7613
  async function readTestCases(outputDir) {
7238
- const testsDir = join9(outputDir, "qa-tests");
7614
+ const testsDir = join11(outputDir, "qa-tests");
7239
7615
  const matches = await glob("**/*.md", { cwd: testsDir, nodir: true });
7240
7616
  const files = [];
7241
7617
  for (const match of matches) {
7242
7618
  const name = basename(match);
7243
7619
  if (name === "INDEX.md") continue;
7244
- const content = await readFile4(join9(testsDir, match), "utf-8");
7620
+ const content = await readFile6(join11(testsDir, match), "utf-8");
7245
7621
  const folderPath = relative(".", match).split("/").slice(0, -1).join("/");
7246
7622
  files.push({ name, content, folder: folderPath.length > 0 ? folderPath : void 0 });
7247
7623
  }
@@ -7278,14 +7654,14 @@ async function patchJson(url, token, body) {
7278
7654
  async function uploadArtifacts(config, outputDir) {
7279
7655
  const { autonomaApiUrl, autonomaApiToken, autonomaGenerationId } = config;
7280
7656
  if (autonomaApiUrl == null || autonomaApiToken == null || autonomaGenerationId == null) {
7281
- p.log.info(
7657
+ p2.log.info(
7282
7658
  `Autonoma upload credentials not configured - artifacts saved locally only. They live in ${outputDir}.`
7283
7659
  );
7284
7660
  return;
7285
7661
  }
7286
7662
  const baseUrl = autonomaApiUrl.replace(/\/+$/, "");
7287
7663
  const setupUrl = `${baseUrl}/v1/setup/setups/${autonomaGenerationId}`;
7288
- p.log.step("Uploading artifacts to Autonoma...");
7664
+ p2.log.step("Uploading artifacts to Autonoma...");
7289
7665
  const [testCases, artifacts, gitInfo] = await Promise.all([
7290
7666
  readTestCases(outputDir),
7291
7667
  readArtifacts(outputDir),
@@ -7293,7 +7669,7 @@ async function uploadArtifacts(config, outputDir) {
7293
7669
  ]);
7294
7670
  await postJson(`${setupUrl}/artifacts`, autonomaApiToken, { testCases, artifacts, commitSha: gitInfo?.sha });
7295
7671
  await patchJson(setupUrl, autonomaApiToken, { status: "completed" });
7296
- p.log.success(
7672
+ p2.log.success(
7297
7673
  `Uploaded ${testCases.length} test case${testCases.length === 1 ? "" : "s"} and ${artifacts.length} artifact${artifacts.length === 1 ? "" : "s"}. Return to your browser to continue onboarding.`
7298
7674
  );
7299
7675
  }
@@ -7303,11 +7679,11 @@ process.setSourceMapsEnabled(true);
7303
7679
  var PAGES_FILE = "pages.json";
7304
7680
  async function savePages(outputDir, pages) {
7305
7681
  const obj = Object.fromEntries(pages);
7306
- await writeFile13(join30(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
7682
+ await writeFile14(join31(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
7307
7683
  }
7308
7684
  async function loadPages(outputDir) {
7309
7685
  try {
7310
- const raw = await readFile22(join30(outputDir, PAGES_FILE), "utf-8");
7686
+ const raw = await readFile23(join31(outputDir, PAGES_FILE), "utf-8");
7311
7687
  const obj = JSON.parse(raw);
7312
7688
  return new Map(Object.entries(obj));
7313
7689
  } catch {
@@ -7336,6 +7712,7 @@ function strArg(args, key) {
7336
7712
  return typeof value === "string" ? value : void 0;
7337
7713
  }
7338
7714
  var STEP_LABELS = {
7715
+ projectMapper: "Map your project structure",
7339
7716
  pagesFinder: "Find your pages",
7340
7717
  kb: "Build a knowledge base",
7341
7718
  entityAudit: "Map your data models",
@@ -7347,6 +7724,7 @@ function isStepName(value) {
7347
7724
  return value in STEP_LABELS;
7348
7725
  }
7349
7726
  var STEP_SUMMARIES = {
7727
+ projectMapper: "Identify your frontend(s), backend(s), and which folders to ignore.",
7350
7728
  pagesFinder: "Map every page and route in your app.",
7351
7729
  kb: "Learn your app's features, flows, and UI patterns.",
7352
7730
  entityAudit: "Find what your app stores (users, orgs, ...) and how each one is created.",
@@ -7355,6 +7733,7 @@ var STEP_SUMMARIES = {
7355
7733
  testGenerator: "Write the end-to-end tests, covering every page and feature."
7356
7734
  };
7357
7735
  var STEP_INTROS = {
7736
+ 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
7737
  pagesFinder: "Scanning your codebase to find every page and route, so we know the full surface area that needs test coverage.",
7359
7738
  kb: "Reading those pages to learn your app's features, flows, and UI patterns - the context everything after this builds on.",
7360
7739
  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 +7741,32 @@ var STEP_INTROS = {
7362
7741
  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
7742
  testGenerator: "Writing the actual end-to-end tests, covering every page and feature with depth proportional to its complexity."
7364
7743
  };
7744
+ async function resolveScopeSelection(map, config, nonInteractive) {
7745
+ if (config.frontend != null) {
7746
+ const requestedFrontend = resolveSelection(map, { frontend: config.frontend, backends: [] }).frontend;
7747
+ const backends = config.backends ?? defaultBackendsFor(map, requestedFrontend);
7748
+ return resolveSelection(map, { frontend: config.frontend, backends });
7749
+ }
7750
+ if (!nonInteractive) return promptScopeSelection(map);
7751
+ return pickDefaultSelection(map);
7752
+ }
7753
+ async function promptScopeSelection(map) {
7754
+ const frontend = await p9.select({
7755
+ message: "Which frontend do you want to plan tests for?",
7756
+ options: map.frontends.map((f) => ({ value: f.path, label: `${f.path} [${f.framework}]`, hint: f.why }))
7757
+ });
7758
+ if (p9.isCancel(frontend)) throw new Error("Cancelled");
7759
+ if (map.backends.length === 0) return { frontend, backends: [] };
7760
+ const needed = defaultBackendsFor(map, frontend);
7761
+ const backends = await p9.multiselect({
7762
+ message: "Which backends does it need? (pre-checked: the ones it depends on)",
7763
+ options: map.backends.map((b) => ({ value: b.path, label: `${b.path} [${b.framework}]`, hint: b.why })),
7764
+ initialValues: needed,
7765
+ required: false
7766
+ });
7767
+ if (p9.isCancel(backends)) throw new Error("Cancelled");
7768
+ return { frontend, backends };
7769
+ }
7365
7770
  async function runStep(step, outputDir, state, config, projectContext, nonInteractive, retryGuidance) {
7366
7771
  const label = STEP_LABELS[step];
7367
7772
  p9.note(STEP_INTROS[step], `Step: ${label}`);
@@ -7377,13 +7782,52 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
7377
7782
  try {
7378
7783
  let result;
7379
7784
  switch (step) {
7785
+ case "projectMapper": {
7786
+ const { runProjectMapper: runProjectMapper2 } = await Promise.resolve().then(() => (init_project_mapper(), project_mapper_exports));
7787
+ const map = await runProjectMapper2({
7788
+ projectRoot: config.projectRoot,
7789
+ outputDir,
7790
+ modelId: config.modelId,
7791
+ nonInteractive
7792
+ });
7793
+ if (map == null) {
7794
+ result = { success: false, artifacts: [], summary: "Project mapper did not produce a map." };
7795
+ break;
7796
+ }
7797
+ if (map.frontends.length === 0) {
7798
+ result = {
7799
+ success: false,
7800
+ artifacts: [],
7801
+ summary: "Project mapper found no frontend to test. Point --project at a codebase with a UI."
7802
+ };
7803
+ break;
7804
+ }
7805
+ const selection = await resolveScopeSelection(map, config, nonInteractive);
7806
+ if (selection == null) {
7807
+ await saveProjectMap(outputDir, map);
7808
+ p9.note(renderProjectMap(map), "Project map - candidates (pick one frontend + its backends)");
7809
+ result = {
7810
+ success: false,
7811
+ paused: true,
7812
+ artifacts: [],
7813
+ summary: `Found ${map.frontends.length} candidate frontends. Choose one and its backends, then resume with --frontend <path> --backends <path,path>.`
7814
+ };
7815
+ break;
7816
+ }
7817
+ const scoped = applySelection(map, selection);
7818
+ await saveProjectMap(outputDir, scoped);
7819
+ p9.note(renderProjectMap(scoped), "Project map");
7820
+ break;
7821
+ }
7380
7822
  case "pagesFinder": {
7381
7823
  const { runPageFinder: runPageFinder2 } = await Promise.resolve().then(() => (init_pages_finder(), pages_finder_exports));
7824
+ const projectMap = await loadProjectMap(outputDir);
7382
7825
  const pages = await runPageFinder2({
7383
7826
  projectRoot: config.projectRoot,
7384
7827
  outputDir,
7385
7828
  modelId: config.modelId,
7386
- nonInteractive
7829
+ nonInteractive,
7830
+ extraMessage: projectMap != null ? formatFrontendScope(projectMap) : void 0
7387
7831
  });
7388
7832
  await savePages(outputDir, pages);
7389
7833
  break;
@@ -7402,18 +7846,21 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
7402
7846
  }
7403
7847
  case "entityAudit": {
7404
7848
  const { runEntityAudit: runEntityAudit2 } = await Promise.resolve().then(() => (init_entity_audit(), entity_audit_exports));
7849
+ const auditMap = await loadProjectMap(outputDir);
7405
7850
  result = await runEntityAudit2({
7406
7851
  projectRoot: config.projectRoot,
7407
7852
  outputDir,
7408
7853
  modelId: config.modelId,
7409
7854
  projectContext,
7410
7855
  nonInteractive,
7411
- retryGuidance
7856
+ retryGuidance,
7857
+ scopeHint: auditMap != null ? formatBackendScope(auditMap) : void 0
7412
7858
  });
7413
7859
  break;
7414
7860
  }
7415
7861
  case "scenarioRecipe": {
7416
7862
  const { runScenarioRecipe: runScenarioRecipe2 } = await Promise.resolve().then(() => (init_scenario_recipe(), scenario_recipe_exports));
7863
+ const recipeMap = await loadProjectMap(outputDir);
7417
7864
  result = await runScenarioRecipe2({
7418
7865
  projectRoot: config.projectRoot,
7419
7866
  outputDir,
@@ -7421,7 +7868,8 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
7421
7868
  config,
7422
7869
  projectContext,
7423
7870
  nonInteractive,
7424
- retryGuidance
7871
+ retryGuidance,
7872
+ scopeHint: recipeMap != null ? formatBackendScope(recipeMap) : void 0
7425
7873
  });
7426
7874
  break;
7427
7875
  }
@@ -7574,6 +8022,7 @@ async function gatherProjectContext() {
7574
8022
  };
7575
8023
  }
7576
8024
  async function main() {
8025
+ installTerminationDiagnostics();
7577
8026
  const args = parseArgs(process.argv.slice(2));
7578
8027
  const command = process.argv[2];
7579
8028
  if (command === "status") {
@@ -7588,12 +8037,28 @@ async function main() {
7588
8037
  await showStatus(outputDir2);
7589
8038
  return;
7590
8039
  }
8040
+ if (command === "upload") {
8041
+ const config2 = loadConfig({
8042
+ project: strArg(args, "project"),
8043
+ slug: strArg(args, "slug")
8044
+ });
8045
+ const outputDir2 = await ensureOutputDir(config2.projectSlug);
8046
+ const recipeUploaded = await uploadRecipeFromDisk(outputDir2, {
8047
+ apiUrl: config2.autonomaApiUrl,
8048
+ apiToken: config2.autonomaApiToken,
8049
+ generationId: config2.autonomaGenerationId
8050
+ });
8051
+ await uploadArtifacts(config2, outputDir2);
8052
+ await flushAnalytics();
8053
+ process.exit(recipeUploaded ? 0 : 1);
8054
+ }
7591
8055
  if (command === "help" || args.help) {
7592
8056
  console.log("Usage:");
7593
8057
  console.log(
7594
- " test-planner [run] [--project <path>] [--model <id>] [--step <name>] [--resume] [--non-interactive]"
8058
+ " test-planner [run] [--project <path>] [--frontend <path>] [--backends <path,path>] [--model <id>] [--step <name>] [--resume] [--non-interactive]"
7595
8059
  );
7596
8060
  console.log(" test-planner status [--project <path>]");
8061
+ console.log(" test-planner upload [--project <path>] # re-upload already-generated recipe + artifacts");
7597
8062
  console.log("");
7598
8063
  console.log("`run` is the default command; it may be omitted.");
7599
8064
  return;
@@ -7602,19 +8067,25 @@ async function main() {
7602
8067
  p9.intro("Let's generate your test suite");
7603
8068
  const resumeCommand = `autonoma-planner --resume` + (args.project ? ` --project ${args.project}` : "");
7604
8069
  installInterruptHandler({
7605
- onExit: () => {
8070
+ // exitCode defaults to 0 for a user-initiated Ctrl+C (progress saved, a clean stop);
8071
+ // an external SIGTERM/SIGHUP passes the conventional 143/129 so the flushed exit still
8072
+ // carries the signal code a reaper/CI reads, not a 0 that looks like normal completion.
8073
+ onExit: (exitCode = 0) => {
7606
8074
  track("cli_run_exited");
7607
8075
  restoreTerminal();
7608
8076
  console.log("");
7609
8077
  p9.log.warn(`Your progress is saved. To resume, run:
7610
8078
  ${resumeCommand}`);
7611
- void flushAnalytics().finally(() => process.exit(0));
8079
+ void flushAnalytics().finally(() => process.exit(exitCode));
7612
8080
  }
7613
8081
  });
8082
+ const backendsArg = strArg(args, "backends");
7614
8083
  const config = loadConfig({
7615
8084
  project: strArg(args, "project"),
7616
8085
  model: strArg(args, "model"),
7617
- slug: strArg(args, "slug")
8086
+ slug: strArg(args, "slug"),
8087
+ frontend: strArg(args, "frontend"),
8088
+ backends: backendsArg != null ? backendsArg.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0
7618
8089
  });
7619
8090
  if (!ensureAutonomaAuth()) {
7620
8091
  return;
@@ -7705,12 +8176,20 @@ or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
7705
8176
  p9.outro("Done");
7706
8177
  return;
7707
8178
  }
7708
- const startStep = isResuming ? nextPendingStep(state) : "pagesFinder";
8179
+ const startStep = isResuming ? nextPendingStep(state) : "projectMapper";
7709
8180
  if (!startStep) {
7710
8181
  p9.log.success("All steps complete.");
7711
8182
  return;
7712
8183
  }
7713
- const steps = ["pagesFinder", "kb", "entityAudit", "scenarioRecipe", "recipeBuilder", "testGenerator"];
8184
+ const steps = [
8185
+ "projectMapper",
8186
+ "pagesFinder",
8187
+ "kb",
8188
+ "entityAudit",
8189
+ "scenarioRecipe",
8190
+ "recipeBuilder",
8191
+ "testGenerator"
8192
+ ];
7714
8193
  const startIdx = steps.indexOf(startStep);
7715
8194
  p9.note(steps.map((s, idx) => `${idx + 1}. ${STEP_LABELS[s]} - ${STEP_SUMMARIES[s]}`).join("\n"), "Here's the plan");
7716
8195
  try {