@autonoma-ai/planner 0.1.2 → 0.1.3-canary.44ffae5

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
@@ -20,13 +20,13 @@ var init_esm_shims = __esm({
20
20
 
21
21
  // src/core/context.ts
22
22
  import { readFile, writeFile } from "fs/promises";
23
- import { join as join2 } from "path";
23
+ import { join as join3 } from "path";
24
24
  async function saveContext(outputDir, ctx) {
25
- await writeFile(join2(outputDir, CONTEXT_FILE), JSON.stringify(ctx, null, 2), "utf-8");
25
+ await writeFile(join3(outputDir, CONTEXT_FILE), JSON.stringify(ctx, null, 2), "utf-8");
26
26
  }
27
27
  async function loadContext(outputDir) {
28
28
  try {
29
- const raw = await readFile(join2(outputDir, CONTEXT_FILE), "utf-8");
29
+ const raw = await readFile(join3(outputDir, CONTEXT_FILE), "utf-8");
30
30
  return JSON.parse(raw);
31
31
  } catch {
32
32
  return null;
@@ -39,9 +39,9 @@ function formatContext(ctx) {
39
39
 
40
40
  **Why they want testing:** ${ctx.testingGoal}
41
41
 
42
- **Critical flows to prioritize:** ${ctx.criticalFlows}
42
+ **Critical flows (user-declared \u2014 these MUST be covered):** ${ctx.criticalFlows}
43
43
 
44
- Use this context to prioritize your exploration. Start with the critical flows the user mentioned, then expand to cover the rest of the application.`;
44
+ These are flows the user explicitly said cannot break. Treat them as authoritative: every one of them must be represented faithfully in your output \u2014 never drop or downplay them. Start with these, then expand to cover the rest of the application.`;
45
45
  if (ctx.pages?.length) {
46
46
  output += `
47
47
 
@@ -150,7 +150,7 @@ function createStepLogger(agentId, maxSteps) {
150
150
  function writeSpinner(message) {
151
151
  const frame = SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length];
152
152
  frameIdx++;
153
- process.stderr.write(`${CLEAR_LINE} ${DIM}${frame} ${message}${RESET}`);
153
+ process.stderr.write(`${CLEAR_LINE} ${DIM2}${frame} ${message}${RESET2}`);
154
154
  lastSpinnerLine = true;
155
155
  }
156
156
  function writePermanent(message) {
@@ -182,52 +182,52 @@ function createStepLogger(agentId, maxSteps) {
182
182
  case "write_file": {
183
183
  stats.filesWritten++;
184
184
  const path3 = String(tc.input.path ?? tc.input.file_path ?? "");
185
- writePermanent(` ${GREEN}\u270E write ${path3}${RESET}`);
185
+ writePermanent(` ${GREEN}\u270E write ${path3}${RESET2}`);
186
186
  break;
187
187
  }
188
188
  case "write_test":
189
189
  stats.filesWritten++;
190
- writePermanent(` ${GREEN}\u270E test ${summary2}${RESET}`);
190
+ writePermanent(` ${GREEN}\u270E test ${summary2}${RESET2}`);
191
191
  break;
192
192
  case "finish":
193
- writePermanent(` ${GREEN}${BOLD}\u2713 finish${RESET}`);
193
+ writePermanent(` ${GREEN}${BOLD}\u2713 done:${RESET2} ${GREEN}${agentId}${RESET2}`);
194
194
  break;
195
195
  case "subagent":
196
196
  case "spawn_researcher":
197
- writePermanent(` ${CYAN}\u2295 subagent: ${summary2}${RESET}`);
197
+ writePermanent(` ${CYAN}\u2295 subagent: ${summary2}${RESET2}`);
198
198
  break;
199
199
  default:
200
200
  writeSpinner(`${stepPrefix} \u2014 ${tc.name}${summary2 ? " " + summary2 : ""}`);
201
201
  }
202
202
  }
203
203
  for (const te of info.toolErrors) {
204
- writePermanent(` ${RED}\u2717 ${te.name}: ${te.error}${RESET}`);
204
+ writePermanent(` ${RED}\u2717 ${te.name}: ${te.error}${RESET2}`);
205
205
  }
206
206
  for (const f of info.writtenFiles) {
207
- writePermanent(` ${GREEN}\u{1F4C4} wrote: ${f}${RESET}`);
207
+ writePermanent(` ${GREEN}\u{1F4C4} wrote: ${f}${RESET2}`);
208
208
  }
209
209
  }
210
210
  function checkpoint(message) {
211
- writePermanent(` ${YELLOW}\u25B8 ${message}${RESET}`);
211
+ writePermanent(` ${YELLOW}\u25B8 ${message}${RESET2}`);
212
212
  }
213
213
  function summary() {
214
214
  clearSpinner();
215
215
  if (stats.filesRead > 0 || stats.filesWritten > 0) {
216
- console.log(` ${DIM}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}`);
216
+ console.log(` ${DIM2}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET2}`);
217
217
  console.log(
218
- ` ${DIM}files read: ${stats.filesRead} | files written: ${stats.filesWritten}${RESET}`
218
+ ` ${DIM2}files read: ${stats.filesRead} | files written: ${stats.filesWritten}${RESET2}`
219
219
  );
220
220
  }
221
221
  }
222
222
  return { log: log8, checkpoint, summary, stats };
223
223
  }
224
- var DIM, RESET, CYAN, GREEN, RED, YELLOW, BOLD, SPINNER_FRAMES, CLEAR_LINE;
224
+ var DIM2, RESET2, CYAN, GREEN, RED, YELLOW, BOLD, SPINNER_FRAMES, CLEAR_LINE;
225
225
  var init_display = __esm({
226
226
  "src/core/display.ts"() {
227
227
  "use strict";
228
228
  init_esm_shims();
229
- DIM = "\x1B[2m";
230
- RESET = "\x1B[0m";
229
+ DIM2 = "\x1B[2m";
230
+ RESET2 = "\x1B[0m";
231
231
  CYAN = "\x1B[36m";
232
232
  GREEN = "\x1B[32m";
233
233
  RED = "\x1B[31m";
@@ -286,8 +286,8 @@ function buildStepHandler(config) {
286
286
  async function runAgent(config, prompt, extractResult) {
287
287
  const stepTimeout = config.stepTimeoutMs ?? STEP_TIMEOUT_MS;
288
288
  const modelsToTry = [config.model, ...FALLBACK_MODELS.map((id) => getModel(id))];
289
- const YELLOW2 = "\x1B[33m";
290
- const RESET4 = "\x1B[0m";
289
+ const YELLOW3 = "\x1B[33m";
290
+ const RESET6 = "\x1B[0m";
291
291
  for (let modelIdx = 0; modelIdx < modelsToTry.length; modelIdx++) {
292
292
  const model = modelsToTry[modelIdx];
293
293
  for (let retry = 0; retry < RETRIES_BEFORE_FALLBACK; retry++) {
@@ -312,17 +312,17 @@ async function runAgent(config, prompt, extractResult) {
312
312
  const msg = err instanceof Error ? err.message : String(err);
313
313
  const isTimeout = msg.includes("timed out") || msg.includes("timeout") || msg.includes("abort");
314
314
  if (!isTimeout) throw err;
315
- console.log(` ${YELLOW2}[${config.id}] step timed out after ${stepTimeout / 1e3}s${RESET4}`);
315
+ console.log(` ${YELLOW3}[${config.id}] step timed out after ${stepTimeout / 1e3}s${RESET6}`);
316
316
  if (retry < RETRIES_BEFORE_FALLBACK - 1) {
317
317
  console.log(
318
- ` ${YELLOW2}[${config.id}] retrying (${retry + 1}/${RETRIES_BEFORE_FALLBACK})...${RESET4}`
318
+ ` ${YELLOW3}[${config.id}] retrying (${retry + 1}/${RETRIES_BEFORE_FALLBACK})...${RESET6}`
319
319
  );
320
320
  continue;
321
321
  }
322
322
  if (modelIdx < modelsToTry.length - 1) {
323
323
  const nextModel = FALLBACK_MODELS[modelIdx];
324
324
  console.log(
325
- ` ${YELLOW2}[${config.id}] ${RETRIES_BEFORE_FALLBACK} timeouts, switching to ${nextModel}${RESET4}`
325
+ ` ${YELLOW3}[${config.id}] ${RETRIES_BEFORE_FALLBACK} timeouts, switching to ${nextModel}${RESET6}`
326
326
  );
327
327
  break;
328
328
  }
@@ -351,7 +351,7 @@ var init_agent = __esm({
351
351
 
352
352
  // src/core/gitignore.ts
353
353
  import { readFile as readFile3 } from "fs/promises";
354
- import { join as join5, relative } from "path";
354
+ import { join as join7, relative } from "path";
355
355
  import { glob } from "glob";
356
356
  async function loadGitignorePatterns(projectRoot) {
357
357
  const patterns = [
@@ -371,10 +371,10 @@ async function loadGitignorePatterns(projectRoot) {
371
371
  ];
372
372
  const matches = await glob("**/.gitignore", { cwd: projectRoot, dot: true });
373
373
  for (const match of matches) {
374
- const fullPath = join5(projectRoot, match);
374
+ const fullPath = join7(projectRoot, match);
375
375
  try {
376
376
  const content = await readFile3(fullPath, "utf-8");
377
- const prefix = relative(projectRoot, join5(projectRoot, match, ".."));
377
+ const prefix = relative(projectRoot, join7(projectRoot, match, ".."));
378
378
  const parsed = parseGitignore(content, prefix);
379
379
  patterns.push(...parsed);
380
380
  } catch (err) {
@@ -598,7 +598,7 @@ var init_grep = __esm({
598
598
  // src/tools/list-directory.ts
599
599
  import { readdir } from "fs/promises";
600
600
  import { stat } from "fs/promises";
601
- import { join as join6, relative as relative2 } from "path";
601
+ import { join as join8, relative as relative2 } from "path";
602
602
  import { tool as tool4 } from "ai";
603
603
  import { z as z4 } from "zod";
604
604
  import { minimatch } from "minimatch";
@@ -623,7 +623,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
623
623
  const withTypes = [];
624
624
  for (const name of rawEntries) {
625
625
  try {
626
- const s = await stat(join6(dirPath, name));
626
+ const s = await stat(join8(dirPath, name));
627
627
  withTypes.push({ name, isDir: s.isDirectory() });
628
628
  } catch {
629
629
  withTypes.push({ name, isDir: false });
@@ -643,7 +643,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
643
643
  }
644
644
  if (entry.isDir) {
645
645
  const children = await buildTree(
646
- join6(dirPath, entry.name),
646
+ join8(dirPath, entry.name),
647
647
  maxDepth,
648
648
  currentDepth + 1,
649
649
  isIgnored,
@@ -694,7 +694,7 @@ async function buildListDirectoryTool(workingDirectory) {
694
694
  };
695
695
  }
696
696
  seen.add(cacheKey);
697
- const targetDir = input.path === "." ? workingDirectory : join6(workingDirectory, input.path);
697
+ const targetDir = input.path === "." ? workingDirectory : join8(workingDirectory, input.path);
698
698
  try {
699
699
  const s = await stat(targetDir);
700
700
  if (!s.isDirectory()) {
@@ -800,7 +800,7 @@ import {
800
800
  import { z as z6 } from "zod";
801
801
  function buildSubagentTools(workingDirectory, onFileRead) {
802
802
  const baseReadFile = buildReadFileTool(workingDirectory);
803
- const readFile18 = onFileRead ? tool6({
803
+ const readFile19 = onFileRead ? tool6({
804
804
  description: baseReadFile.description,
805
805
  inputSchema: baseReadFile.inputSchema,
806
806
  execute: async (input, options) => {
@@ -813,7 +813,7 @@ function buildSubagentTools(workingDirectory, onFileRead) {
813
813
  bash: buildBashTool(workingDirectory),
814
814
  glob: buildGlobTool(workingDirectory),
815
815
  grep: buildGrepTool(workingDirectory),
816
- read_file: readFile18
816
+ read_file: readFile19
817
817
  };
818
818
  }
819
819
  function buildSubagentTool(model, workingDirectory, onHeartbeat, onFileRead) {
@@ -1091,12 +1091,12 @@ var init_notify = __esm({
1091
1091
  // src/core/review.ts
1092
1092
  import * as p2 from "@clack/prompts";
1093
1093
  import { access } from "fs/promises";
1094
- import { join as join7, isAbsolute } from "path";
1094
+ import { join as join9, isAbsolute } from "path";
1095
1095
  import { spawn } from "child_process";
1096
1096
  import which from "which";
1097
1097
  function resolvePath(artifact, outputDir) {
1098
1098
  if (isAbsolute(artifact)) return artifact;
1099
- return join7(outputDir, artifact);
1099
+ return join9(outputDir, artifact);
1100
1100
  }
1101
1101
  async function detectEditors() {
1102
1102
  if (cachedEditors) return cachedEditors;
@@ -1122,7 +1122,7 @@ async function openInEditor(files) {
1122
1122
  const editors = await detectEditors();
1123
1123
  if (editors.length === 0) {
1124
1124
  p2.log.warn("No editors found. Review the files manually:");
1125
- for (const f of files) console.log(` ${CYAN2}${f}${RESET2}`);
1125
+ for (const f of files) console.log(` ${CYAN2}${f}${RESET3}`);
1126
1126
  return;
1127
1127
  }
1128
1128
  if (preferredEditor) {
@@ -1164,11 +1164,11 @@ async function openInEditor(files) {
1164
1164
  }
1165
1165
  async function showResults(result, options) {
1166
1166
  console.log("");
1167
- console.log(` ${GREEN2}[${options.agentId}] Step complete.${RESET2}`);
1167
+ console.log(` ${GREEN2}[${options.agentId}] Step complete.${RESET3}`);
1168
1168
  if (result.artifacts.length === 0) {
1169
1169
  const knownFiles = ["AUTONOMA.md", "entity-audit.md", "scenarios.md"];
1170
1170
  for (const f of knownFiles) {
1171
- const fullPath = join7(options.outputDir, f);
1171
+ const fullPath = join9(options.outputDir, f);
1172
1172
  try {
1173
1173
  await access(fullPath);
1174
1174
  result.artifacts.push(f);
@@ -1178,17 +1178,24 @@ async function showResults(result, options) {
1178
1178
  }
1179
1179
  const resolvedPaths = [];
1180
1180
  if (result.artifacts.length > 0) {
1181
- console.log(` ${DIM2}Output files:${RESET2}`);
1181
+ console.log(` ${DIM3}Output files:${RESET3}`);
1182
1182
  for (const a of result.artifacts) {
1183
1183
  const fullPath = resolvePath(a, options.outputDir);
1184
1184
  resolvedPaths.push(fullPath);
1185
- console.log(` ${CYAN2}${fullPath}${RESET2}`);
1185
+ console.log(` ${CYAN2}${fullPath}${RESET3}`);
1186
1186
  }
1187
1187
  }
1188
1188
  if (result.summary) {
1189
1189
  console.log(` ${result.summary}`);
1190
1190
  }
1191
1191
  console.log("");
1192
+ if (options.renderSummary) {
1193
+ const rendered = await options.renderSummary();
1194
+ if (rendered) {
1195
+ console.log(rendered);
1196
+ console.log("");
1197
+ }
1198
+ }
1192
1199
  if (options.reviewGuidance) {
1193
1200
  p2.note(options.reviewGuidance, "What to check");
1194
1201
  }
@@ -1226,16 +1233,16 @@ async function reviewLoop(result, options) {
1226
1233
  await showResults(result, options);
1227
1234
  }
1228
1235
  }
1229
- var DIM2, CYAN2, GREEN2, RESET2, EDITORS, cachedEditors, preferredEditor;
1236
+ var DIM3, CYAN2, GREEN2, RESET3, EDITORS, cachedEditors, preferredEditor;
1230
1237
  var init_review = __esm({
1231
1238
  "src/core/review.ts"() {
1232
1239
  "use strict";
1233
1240
  init_esm_shims();
1234
1241
  init_notify();
1235
- DIM2 = "\x1B[2m";
1242
+ DIM3 = "\x1B[2m";
1236
1243
  CYAN2 = "\x1B[36m";
1237
1244
  GREEN2 = "\x1B[32m";
1238
- RESET2 = "\x1B[0m";
1245
+ RESET3 = "\x1B[0m";
1239
1246
  EDITORS = [
1240
1247
  { command: "cursor", label: "Cursor", args: (f) => f },
1241
1248
  { command: "code", label: "VS Code", args: (f) => f },
@@ -1339,6 +1346,7 @@ pages:
1339
1346
  BAD mission: "Shows analytics charts" (just restates the feature name)
1340
1347
  - coreReason (required when core: true): WHY breakage of this feature makes the product unusable.
1341
1348
  - At least one flow must have core: true
1349
+ - Any flow the user explicitly named as critical in the Project Context MUST appear as a feature in core_flows AND be marked core: true with a coreReason. Map the user's wording to the matching feature(s) \u2014 never drop a user-declared critical flow or leave it as core: false.
1342
1350
  - feature_count: total features identified (positive integer)
1343
1351
  - pages: a list of all pages discovered, with their path and brief description
1344
1352
 
@@ -1391,6 +1399,77 @@ After the frontmatter, include:
1391
1399
  }
1392
1400
  });
1393
1401
 
1402
+ // src/agents/01-kb-generator/flows.ts
1403
+ import { readFile as readFile5 } from "fs/promises";
1404
+ import { join as join10 } from "path";
1405
+ import matter from "gray-matter";
1406
+ async function parseCoreFlows(outputDir) {
1407
+ let raw;
1408
+ try {
1409
+ raw = await readFile5(join10(outputDir, "AUTONOMA.md"), "utf-8");
1410
+ } catch {
1411
+ return [];
1412
+ }
1413
+ try {
1414
+ const parsed = matter(raw);
1415
+ const flows = parsed.data.core_flows;
1416
+ if (!Array.isArray(flows)) return [];
1417
+ return flows.filter((f) => !!f && typeof f === "object").map((f) => ({
1418
+ feature: String(f.feature ?? "").trim(),
1419
+ description: f.description != null ? String(f.description) : void 0,
1420
+ mission: f.mission != null ? String(f.mission) : void 0,
1421
+ core: f.core === true,
1422
+ coreReason: f.coreReason != null ? String(f.coreReason) : void 0
1423
+ })).filter((f) => f.feature.length > 0);
1424
+ } catch {
1425
+ return [];
1426
+ }
1427
+ }
1428
+ function truncate(s, max) {
1429
+ if (s.length <= max) return s;
1430
+ return s.slice(0, max - 1).trimEnd() + "\u2026";
1431
+ }
1432
+ function pad(s, width) {
1433
+ return s + " ".repeat(Math.max(0, width - s.length));
1434
+ }
1435
+ function renderFlowsTable(flows) {
1436
+ if (flows.length === 0) return "";
1437
+ const DESC_MAX = 60;
1438
+ const NAME_MAX = 32;
1439
+ const rows = flows.map((f, i) => ({
1440
+ num: String(i + 1),
1441
+ name: truncate(f.feature, NAME_MAX),
1442
+ crit: f.core ? "core" : "normal",
1443
+ desc: truncate((f.description ?? "").replace(/\s+/g, " ").trim(), DESC_MAX)
1444
+ }));
1445
+ const numW = Math.max(1, ...rows.map((r) => r.num.length));
1446
+ const nameW = Math.max("Flow".length, ...rows.map((r) => r.name.length));
1447
+ const critW = Math.max("Criticality".length, ...rows.map((r) => r.crit.length));
1448
+ const coreCount = flows.filter((f) => f.core).length;
1449
+ const header = `${BOLD2}${pad("#", numW)} ${pad("Flow", nameW)} ${pad("Criticality", critW)} Description${RESET4}`;
1450
+ const sep = `${DIM4}${"\u2500".repeat(numW + nameW + critW + DESC_MAX + 6)}${RESET4}`;
1451
+ const body = rows.map((r) => {
1452
+ const line = `${pad(r.num, numW)} ${pad(r.name, nameW)} ${pad(r.crit, critW)} ${r.desc}`;
1453
+ return r.crit === "core" ? `${YELLOW2}${line}${RESET4}` : line;
1454
+ }).join("\n");
1455
+ const caption = `${DIM4}${flows.length} flows \xB7 ${coreCount} marked core${RESET4}`;
1456
+ return `${header}
1457
+ ${sep}
1458
+ ${body}
1459
+ ${caption}`;
1460
+ }
1461
+ var RESET4, DIM4, YELLOW2, BOLD2;
1462
+ var init_flows = __esm({
1463
+ "src/agents/01-kb-generator/flows.ts"() {
1464
+ "use strict";
1465
+ init_esm_shims();
1466
+ RESET4 = "\x1B[0m";
1467
+ DIM4 = "\x1B[2m";
1468
+ YELLOW2 = "\x1B[33m";
1469
+ BOLD2 = "\x1B[1m";
1470
+ }
1471
+ });
1472
+
1394
1473
  // src/agents/01-kb-generator/index.ts
1395
1474
  var kb_generator_exports = {};
1396
1475
  __export(kb_generator_exports, {
@@ -1398,8 +1477,8 @@ __export(kb_generator_exports, {
1398
1477
  });
1399
1478
  import { tool as tool10 } from "ai";
1400
1479
  import { z as z10 } from "zod";
1401
- import { readFile as readFile5 } from "fs/promises";
1402
- import { join as join8 } from "path";
1480
+ import { readFile as readFile6 } from "fs/promises";
1481
+ import { join as join11 } from "path";
1403
1482
  function buildRegisterPagesTool(tracker) {
1404
1483
  return tool10({
1405
1484
  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.",
@@ -1513,11 +1592,43 @@ Output files:
1513
1592
  };
1514
1593
  await runAgent(agentConfig, prompt, () => result);
1515
1594
  logger.summary();
1595
+ const autonomaPath = join11(input.outputDir, "AUTONOMA.md");
1596
+ const autonomaExists = await readFile6(autonomaPath, "utf-8").then(() => true).catch(() => false);
1597
+ if (!result?.success && autonomaExists) {
1598
+ result = {
1599
+ success: true,
1600
+ artifacts: ["AUTONOMA.md"],
1601
+ summary: "Knowledge base generated."
1602
+ };
1603
+ }
1604
+ const declaredCriticalFlows = input.projectContext?.criticalFlows?.trim();
1605
+ if (result?.success && declaredCriticalFlows) {
1606
+ const beforeSelfReview = result;
1607
+ result = void 0;
1608
+ const selfReviewPrompt = `Before this knowledge base is shown to the user, verify it honors the critical flows they explicitly declared.
1609
+
1610
+ The user said these flows are critical and cannot break:
1611
+ "${declaredCriticalFlows}"
1612
+
1613
+ Read your AUTONOMA.md output. For EACH critical flow the user named:
1614
+ - Confirm it appears as a feature in core_flows (map the user's wording to the matching feature).
1615
+ - Confirm that feature is marked core: true with a coreReason.
1616
+
1617
+ If any declared critical flow is missing, mismatched, or left core: false, FIX AUTONOMA.md now \u2014 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.
1618
+
1619
+ When AUTONOMA.md correctly reflects every declared critical flow, call finish.`;
1620
+ await runAgent(agentConfig, selfReviewPrompt, () => result);
1621
+ if (!result) result = beforeSelfReview;
1622
+ }
1516
1623
  const reviewed = await reviewLoop(result, {
1517
1624
  agentId: "kb-generator",
1518
1625
  outputDir: input.outputDir,
1519
1626
  nonInteractive: input.nonInteractive,
1520
- reviewGuidance: "Check that every page/route in your app appears in core_flows.\nVerify the mission for each feature describes the ONE thing it must do correctly.\nLook for missing features or incorrectly grouped pages.\nA complex app should have 20-40 features \u2014 if you see fewer than 15, features are probably grouped too aggressively.",
1627
+ renderSummary: async () => {
1628
+ const flows = await parseCoreFlows(input.outputDir);
1629
+ return flows.length ? renderFlowsTable(flows) : void 0;
1630
+ },
1631
+ reviewGuidance: "Check that every page/route in your app appears in core_flows.\nVerify that every flow the user named as critical in the Project Context appears in core_flows and is marked core: true with a coreReason.\nVerify the mission for each feature describes the ONE thing it must do correctly.\nLook for missing features or incorrectly grouped pages.\nA complex app should have 20-40 features \u2014 if you see fewer than 15, features are probably grouped too aggressively.",
1521
1632
  onFeedback: async (feedback) => {
1522
1633
  result = void 0;
1523
1634
  const feedbackPrompt = `The user reviewed your knowledge base output and has this feedback:
@@ -1531,18 +1642,6 @@ Call page_coverage to see current state. When done with changes, call finish aga
1531
1642
  return result;
1532
1643
  }
1533
1644
  });
1534
- if (!reviewed) {
1535
- const autonomaPath = join8(input.outputDir, "AUTONOMA.md");
1536
- try {
1537
- await readFile5(autonomaPath, "utf-8");
1538
- return {
1539
- success: true,
1540
- artifacts: ["AUTONOMA.md"],
1541
- summary: "Knowledge base generated (finish tool may not have captured the result, but AUTONOMA.md exists)."
1542
- };
1543
- } catch {
1544
- }
1545
- }
1546
1645
  return reviewed ?? {
1547
1646
  success: false,
1548
1647
  artifacts: [],
@@ -1560,6 +1659,7 @@ var init_kb_generator = __esm({
1560
1659
  init_review();
1561
1660
  init_tools();
1562
1661
  init_prompt();
1662
+ init_flows();
1563
1663
  PageTracker = class {
1564
1664
  registered = /* @__PURE__ */ new Set();
1565
1665
  read = /* @__PURE__ */ new Set();
@@ -1729,8 +1829,8 @@ var entity_audit_exports = {};
1729
1829
  __export(entity_audit_exports, {
1730
1830
  runEntityAudit: () => runEntityAudit
1731
1831
  });
1732
- import { readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
1733
- import { join as join9 } from "path";
1832
+ import { readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
1833
+ import { join as join12 } from "path";
1734
1834
  import { tool as tool11 } from "ai";
1735
1835
  import { z as z11 } from "zod";
1736
1836
  import { glob as glob3 } from "glob";
@@ -1859,7 +1959,7 @@ async function findPrismaSchema(projectRoot) {
1859
1959
  return candidates[0] ?? null;
1860
1960
  }
1861
1961
  async function extractPrismaModels(schemaPath) {
1862
- const content = await readFile6(schemaPath, "utf-8");
1962
+ const content = await readFile7(schemaPath, "utf-8");
1863
1963
  return content.split("\n").filter((line) => line.startsWith("model ")).map((line) => line.split(/\s+/)[1]).filter((name) => name != null);
1864
1964
  }
1865
1965
  async function detectFrameworkAndModels(projectRoot) {
@@ -1941,7 +2041,7 @@ write_file already targets the output directory \u2014 use just the filename.`;
1941
2041
  logger.summary();
1942
2042
  if (!result && tracker.auditedModels.size > 0) {
1943
2043
  const markdown = tracker.generateAuditMarkdown();
1944
- const auditPath = join9(input.outputDir, "entity-audit.md");
2044
+ const auditPath = join12(input.outputDir, "entity-audit.md");
1945
2045
  await writeFile4(auditPath, markdown, "utf-8");
1946
2046
  const cov = tracker.coverage();
1947
2047
  result = {
@@ -1970,9 +2070,9 @@ When done with changes, call finish again.`;
1970
2070
  }
1971
2071
  });
1972
2072
  if (!reviewed) {
1973
- const auditPath = join9(input.outputDir, "entity-audit.md");
2073
+ const auditPath = join12(input.outputDir, "entity-audit.md");
1974
2074
  try {
1975
- await readFile6(auditPath, "utf-8");
2075
+ await readFile7(auditPath, "utf-8");
1976
2076
  return {
1977
2077
  success: true,
1978
2078
  artifacts: ["entity-audit.md"],
@@ -2109,11 +2209,11 @@ ${duals.length > 0 ? duals.map((m) => `- **${m.name}** \u2014 standalone: ${m.cr
2109
2209
  });
2110
2210
 
2111
2211
  // src/core/parse-entity-audit.ts
2112
- import { readFile as readFile7 } from "fs/promises";
2113
- import { join as join10 } from "path";
2212
+ import { readFile as readFile8 } from "fs/promises";
2213
+ import { join as join13 } from "path";
2114
2214
  async function parseEntityNames(outputDir) {
2115
2215
  try {
2116
- const content = await readFile7(join10(outputDir, "entity-audit.md"), "utf-8");
2216
+ const content = await readFile8(join13(outputDir, "entity-audit.md"), "utf-8");
2117
2217
  const names = [];
2118
2218
  for (const line of content.split("\n")) {
2119
2219
  const match = line.match(/^\s+-\s+name:\s+(.+)$/);
@@ -2196,8 +2296,8 @@ __export(scenario_recipe_exports, {
2196
2296
  feedbackToScenario: () => feedbackToScenario,
2197
2297
  runScenarioRecipe: () => runScenarioRecipe
2198
2298
  });
2199
- import { readFile as readFile8 } from "fs/promises";
2200
- import { join as join11 } from "path";
2299
+ import { readFile as readFile9 } from "fs/promises";
2300
+ import { join as join14 } from "path";
2201
2301
  import { tool as tool12 } from "ai";
2202
2302
  import { z as z12 } from "zod";
2203
2303
  function buildFinishTool3(requiredEntities, outputDir, onFinish) {
@@ -2211,7 +2311,7 @@ function buildFinishTool3(requiredEntities, outputDir, onFinish) {
2211
2311
  execute: async (input) => {
2212
2312
  if (requiredEntities.length > 0) {
2213
2313
  try {
2214
- const content = await readFile8(join11(outputDir, "scenarios.md"), "utf-8");
2314
+ const content = await readFile9(join14(outputDir, "scenarios.md"), "utf-8");
2215
2315
  const missing = requiredEntities.filter(
2216
2316
  (e) => !content.includes(e)
2217
2317
  );
@@ -2294,9 +2394,9 @@ When done with changes, call finish again.`;
2294
2394
  }
2295
2395
  });
2296
2396
  if (!reviewed) {
2297
- const scenariosPath = join11(input.outputDir, "scenarios.md");
2397
+ const scenariosPath = join14(input.outputDir, "scenarios.md");
2298
2398
  try {
2299
- await readFile8(scenariosPath, "utf-8");
2399
+ await readFile9(scenariosPath, "utf-8");
2300
2400
  return {
2301
2401
  success: true,
2302
2402
  artifacts: ["scenarios.md"],
@@ -2349,8 +2449,8 @@ var init_scenario_recipe = __esm({
2349
2449
  });
2350
2450
 
2351
2451
  // src/agents/04-recipe-builder/state.ts
2352
- import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
2353
- import { join as join12 } from "path";
2452
+ import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
2453
+ import { join as join15 } from "path";
2354
2454
  function adapterKey(a) {
2355
2455
  return `${a.language}:${a.framework}`;
2356
2456
  }
@@ -2377,14 +2477,14 @@ function initialRecipeState() {
2377
2477
  }
2378
2478
  async function loadRecipeState(outputDir) {
2379
2479
  try {
2380
- const raw = await readFile9(join12(outputDir, STATE_FILE2), "utf-8");
2480
+ const raw = await readFile10(join15(outputDir, STATE_FILE2), "utf-8");
2381
2481
  return JSON.parse(raw);
2382
2482
  } catch {
2383
2483
  return null;
2384
2484
  }
2385
2485
  }
2386
2486
  async function saveRecipeState(outputDir, state) {
2387
- await writeFile5(join12(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
2487
+ await writeFile5(join15(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
2388
2488
  }
2389
2489
  var ALL_ADAPTERS, ADAPTER_HINTS, STATE_FILE2;
2390
2490
  var init_state = __esm({
@@ -2430,10 +2530,10 @@ var init_state = __esm({
2430
2530
  });
2431
2531
 
2432
2532
  // src/agents/04-recipe-builder/entity-order.ts
2433
- import { readFile as readFile10 } from "fs/promises";
2434
- import { join as join13 } from "path";
2533
+ import { readFile as readFile11 } from "fs/promises";
2534
+ import { join as join16 } from "path";
2435
2535
  async function parseEntityAudit(outputDir) {
2436
- const raw = await readFile10(join13(outputDir, "entity-audit.md"), "utf-8");
2536
+ const raw = await readFile11(join16(outputDir, "entity-audit.md"), "utf-8");
2437
2537
  const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
2438
2538
  if (!fmMatch) throw new Error("entity-audit.md has no YAML frontmatter");
2439
2539
  const yaml = fmMatch[1];
@@ -2690,11 +2790,11 @@ When done, call finish with your findings.`;
2690
2790
 
2691
2791
  // src/core/detect-pkg-manager.ts
2692
2792
  import { existsSync as existsSync2 } from "fs";
2693
- import { join as join14 } from "path";
2793
+ import { join as join17 } from "path";
2694
2794
  function detectPackageManager(projectRoot) {
2695
- if (existsSync2(join14(projectRoot, "bun.lock")) || existsSync2(join14(projectRoot, "bun.lockb"))) return "bun";
2696
- if (existsSync2(join14(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
2697
- if (existsSync2(join14(projectRoot, "yarn.lock"))) return "yarn";
2795
+ if (existsSync2(join17(projectRoot, "bun.lock")) || existsSync2(join17(projectRoot, "bun.lockb"))) return "bun";
2796
+ if (existsSync2(join17(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
2797
+ if (existsSync2(join17(projectRoot, "yarn.lock"))) return "yarn";
2698
2798
  return "npm";
2699
2799
  }
2700
2800
  function installCommand(pm, ...packages) {
@@ -2724,7 +2824,7 @@ function spanReplacer(_match, cls) {
2724
2824
  return ANSI[mainCls] ?? "";
2725
2825
  }
2726
2826
  function htmlToAnsi(html) {
2727
- return html.replace(/<span class="hljs-([^"]+)">/g, spanReplacer).replace(/<\/span>/g, RESET3).replace(/&#x27;/g, "'").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"');
2827
+ return html.replace(/<span class="hljs-([^"]+)">/g, spanReplacer).replace(/<\/span>/g, RESET5).replace(/&#x27;/g, "'").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"');
2728
2828
  }
2729
2829
  function highlightCode(code, language = "typescript") {
2730
2830
  try {
@@ -2738,14 +2838,14 @@ function codeNoteFormat(line) {
2738
2838
  if (line.includes("\x1B[")) return line;
2739
2839
  return highlightCode(line);
2740
2840
  }
2741
- var RESET3, ANSI;
2841
+ var RESET5, ANSI;
2742
2842
  var init_highlight = __esm({
2743
2843
  "src/core/highlight.ts"() {
2744
2844
  "use strict";
2745
2845
  init_esm_shims();
2746
- RESET3 = "\x1B[0m";
2846
+ RESET5 = "\x1B[0m";
2747
2847
  ANSI = {
2748
- reset: RESET3,
2848
+ reset: RESET5,
2749
2849
  keyword: "\x1B[35m",
2750
2850
  string: "\x1B[32m",
2751
2851
  number: "\x1B[33m",
@@ -2770,8 +2870,8 @@ var init_highlight = __esm({
2770
2870
  });
2771
2871
 
2772
2872
  // src/agents/04-recipe-builder/recipe.ts
2773
- import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
2774
- import { join as join15 } from "path";
2873
+ import { readFile as readFile12, writeFile as writeFile6 } from "fs/promises";
2874
+ import { join as join18 } from "path";
2775
2875
  function buildSingleEntityRecipe(entityName, models, entityOrder, allEntities) {
2776
2876
  const chain = getEntityDependencyChain(entityName, models, entityOrder);
2777
2877
  const recipe = {};
@@ -2815,7 +2915,7 @@ function buildSubmittableRecipe(create, description) {
2815
2915
  };
2816
2916
  }
2817
2917
  async function saveRecipe(outputDir, recipe) {
2818
- await writeFile6(join15(outputDir, RECIPE_FILE), JSON.stringify(recipe, null, 2), "utf-8");
2918
+ await writeFile6(join18(outputDir, RECIPE_FILE), JSON.stringify(recipe, null, 2), "utf-8");
2819
2919
  }
2820
2920
  var RECIPE_FILE;
2821
2921
  var init_recipe = __esm({
@@ -2875,12 +2975,15 @@ var init_http_client = __esm({
2875
2975
 
2876
2976
  // src/agents/04-recipe-builder/phases/entity-loop.ts
2877
2977
  import * as p4 from "@clack/prompts";
2878
- import { writeFile as writeFile7, readFile as readFile12 } from "fs/promises";
2879
- import { join as join16 } from "path";
2978
+ import { writeFile as writeFile7, readFile as readFile13 } from "fs/promises";
2979
+ import { join as join19 } from "path";
2880
2980
  import { tmpdir } from "os";
2881
2981
  import { spawn as spawn2 } from "child_process";
2882
2982
  import { tool as tool14 } from "ai";
2883
2983
  import { z as z14 } from "zod";
2984
+ function summarizeCompletedAliases(completedEntities, excludeName) {
2985
+ 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");
2986
+ }
2884
2987
  async function proposeRecipeData(entityName, entityIndex, totalEntities, model, outputDir, _projectRoot, completedEntities) {
2885
2988
  let result;
2886
2989
  const { logger, onStepFinish } = buildDefaultStepLogger(`propose:${entityName}`, 20);
@@ -2894,7 +2997,7 @@ async function proposeRecipeData(entityName, entityIndex, totalEntities, model,
2894
2997
  return { accepted: true };
2895
2998
  }
2896
2999
  });
2897
- const completedAliases = Object.entries(completedEntities).filter(([, e]) => e.recipeData && e.recipeData.length > 0).map(([name, e]) => `${name}: aliases ${e.recipeData.map((r) => r._alias ?? "?").join(", ")}`).join("\n");
3000
+ const completedAliases = summarizeCompletedAliases(completedEntities, entityName);
2898
3001
  const prompt = `[${entityIndex + 1}/${totalEntities}] Propose recipe data for entity "${entityName}".
2899
3002
 
2900
3003
  Read scenarios.md and entity-audit.md from the output directory. Design records that match the scenario data.
@@ -2924,7 +3027,7 @@ Call finish with the JSON array of records.`;
2924
3027
  logger.summary();
2925
3028
  return result ?? [];
2926
3029
  }
2927
- async function reviseRecipeData(entityName, entityIndex, totalEntities, current, feedback, model, outputDir) {
3030
+ async function reviseRecipeData(entityName, entityIndex, totalEntities, current, feedback, model, outputDir, completedEntities) {
2928
3031
  let revised;
2929
3032
  const finishTool = tool14({
2930
3033
  description: "Submit the fixed recipe data.",
@@ -2937,14 +3040,19 @@ async function reviseRecipeData(entityName, entityIndex, totalEntities, current,
2937
3040
  }
2938
3041
  });
2939
3042
  const { logger, onStepFinish } = buildDefaultStepLogger(`fix:${entityName}`, 15);
3043
+ const completedAliases = summarizeCompletedAliases(completedEntities, entityName);
3044
+ const aliasBlock = completedAliases ? `Aliases declared by already-created parent entities (these are the ONLY valid _ref targets):
3045
+ ${completedAliases}
3046
+ ` : `This is a root entity \u2014 it has no parent entities to _ref.
3047
+ `;
2940
3048
  await runAgent(
2941
3049
  {
2942
3050
  id: `fix-${entityName}`,
2943
- systemPrompt: `You are fixing recipe data that failed validation. Read the error, the current data, and the user's feedback. Read scenarios.md and entity-audit.md if needed. Fix the data and call finish.
3051
+ systemPrompt: `You are fixing recipe data based on user feedback (or a validation failure). Read the error, the current data, and the user's feedback. Read scenarios.md and entity-audit.md if needed. Fix the data and call finish.
2944
3052
 
2945
3053
  Rules:
2946
3054
  - _alias fields must be unique identifiers (e.g., "card_1", "transaction_1")
2947
- - _ref fields reference aliases from OTHER entities that were already created
3055
+ - _ref fields must reference an alias that ALREADY EXISTS on a parent entity \u2014 see the list of valid targets below. Never invent a _ref to an alias that isn't listed.
2948
3056
  - Read scenarios.md to verify you're using correct alias names from parent entities
2949
3057
  - Field names must match the entity's schema from entity-audit.md`,
2950
3058
  model,
@@ -2957,13 +3065,14 @@ Rules:
2957
3065
  },
2958
3066
  `[${entityIndex + 1}/${totalEntities}] Fix recipe data for "${entityName}".
2959
3067
 
2960
- Current data that failed:
3068
+ Current data:
2961
3069
  ${JSON.stringify(current, null, 2)}
2962
3070
 
2963
- Problem:
3071
+ What's wrong / what to change:
2964
3072
  ${feedback}
2965
3073
 
2966
- Read scenarios.md and entity-audit.md to understand the correct aliases and schema. Fix the data and call finish.`,
3074
+ ${aliasBlock}
3075
+ Read scenarios.md and entity-audit.md to understand the correct aliases and schema. Apply the change and call finish.`,
2967
3076
  () => revised
2968
3077
  );
2969
3078
  logger.summary();
@@ -3037,7 +3146,7 @@ Read the creation file from the project to understand the existing service/funct
3037
3146
  logger.summary();
3038
3147
  return result ?? "No instructions generated. Check the entity audit for creation_file and creation_function.";
3039
3148
  }
3040
- async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed, model, outputDir) {
3149
+ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed, model, outputDir, completedEntities) {
3041
3150
  p4.log.info(
3042
3151
  `Legend for recipe fields:
3043
3152
  _alias \u2014 Internal ID used to reference this record from other entities (e.g., { "_ref": "org_1" })
@@ -3060,7 +3169,7 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
3060
3169
  if (p4.isCancel(action)) throw new Error("Recipe review cancelled");
3061
3170
  if (action === "keep") return proposed;
3062
3171
  if (action === "edit") {
3063
- const tmpPath = join16(tmpdir(), `autonoma-recipe-${entityName}.json`);
3172
+ const tmpPath = join19(tmpdir(), `autonoma-recipe-${entityName}.json`);
3064
3173
  await writeFile7(tmpPath, JSON.stringify(proposed, null, 2), "utf-8");
3065
3174
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
3066
3175
  p4.log.info(`Opening ${editor}... Save and close when done.`);
@@ -3069,7 +3178,7 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
3069
3178
  proc.on("close", () => resolve5());
3070
3179
  proc.on("error", reject);
3071
3180
  });
3072
- const edited = await readFile12(tmpPath, "utf-8");
3181
+ const edited = await readFile13(tmpPath, "utf-8");
3073
3182
  try {
3074
3183
  proposed = JSON.parse(edited);
3075
3184
  p4.note(JSON.stringify(proposed, null, 2), `Updated data for ${entityName}`, { format: codeNoteFormat });
@@ -3084,43 +3193,16 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
3084
3193
  placeholder: "e.g., add more records, change field values, fix references..."
3085
3194
  });
3086
3195
  if (p4.isCancel(feedback) || !feedback.trim()) continue;
3087
- let revised;
3088
- const finishTool = tool14({
3089
- description: "Submit revised recipe data.",
3090
- inputSchema: z14.object({
3091
- records: z14.array(z14.record(z14.string(), z14.unknown()))
3092
- }),
3093
- execute: async (input) => {
3094
- revised = input.records;
3095
- return { done: true };
3096
- }
3097
- });
3098
- const { logger, onStepFinish } = buildDefaultStepLogger(`revise:${entityName}`, 10);
3099
- await runAgent(
3100
- {
3101
- id: `revise-${entityName}`,
3102
- systemPrompt: "You are revising recipe data based on user feedback. Read the current data, apply the feedback, and call finish with the updated records.",
3103
- model,
3104
- maxSteps: 10,
3105
- tools: (_heartbeat) => ({
3106
- read_output: buildReadFileTool(outputDir),
3107
- finish: finishTool
3108
- }),
3109
- onStepFinish
3110
- },
3111
- `Current data for ${entityName}:
3112
- ${JSON.stringify(proposed, null, 2)}
3113
-
3114
- User feedback: "${feedback}"
3115
-
3116
- Revise the data and call finish.`,
3117
- () => revised
3196
+ proposed = await reviseRecipeData(
3197
+ entityName,
3198
+ entityIndex,
3199
+ totalEntities,
3200
+ proposed,
3201
+ feedback.trim(),
3202
+ model,
3203
+ outputDir,
3204
+ completedEntities
3118
3205
  );
3119
- logger.summary();
3120
- if (revised) {
3121
- proposed = revised;
3122
- p4.note(JSON.stringify(proposed, null, 2), `Revised data for ${entityName}`, { format: codeNoteFormat });
3123
- }
3124
3206
  }
3125
3207
  }
3126
3208
  }
@@ -3247,7 +3329,7 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
3247
3329
  );
3248
3330
  }
3249
3331
  if (!nonInteractive) {
3250
- recipeData = await reviewRecipeData(entityName, i, total, recipeData, model, outputDir);
3332
+ recipeData = await reviewRecipeData(entityName, i, total, recipeData, model, outputDir, state.entities);
3251
3333
  }
3252
3334
  state.entities[entityName] = {
3253
3335
  entityName,
@@ -3270,20 +3352,21 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
3270
3352
  projectRoot,
3271
3353
  outputDir
3272
3354
  );
3273
- p4.note(instructions, `Implementation guide for ${entityName}`, { format: codeNoteFormat });
3274
3355
  const DOCS_BASE2 = "https://docs.agent.autonoma.app";
3275
3356
  p4.log.info(
3276
- `Copy the instructions above into Claude Code or your AI coding assistant.
3277
- They can implement the factory directly in your codebase.
3278
-
3279
- Autonoma SDK docs: ${DOCS_BASE2}/sdk/environment-factory`
3357
+ `Next: implement the ${entityName} factory. The block below is a copy-paste guide \u2014
3358
+ paste it into Claude Code (or your AI assistant) and it will write the factory in your codebase.
3359
+ A factory teaches the Autonoma SDK how to create and tear down ${entityName} records using your app's own code.
3360
+ Keep it local for now: implement it, run your app on localhost, and we'll test it live here. You deploy later.`
3280
3361
  );
3362
+ p4.note(instructions, `Implementation guide for ${entityName} (paste into your AI assistant)`, { format: codeNoteFormat });
3363
+ p4.log.info(`Autonoma SDK docs: ${DOCS_BASE2}/sdk/environment-factory`);
3281
3364
  if (i === 0) {
3282
- p4.log.info("This is your first factory \u2014 the instructions include one-time SDK setup. Subsequent entities will only need the factory function.");
3365
+ p4.log.info("This is your first factory \u2014 the guide includes one-time SDK setup. Later entities only need the factory function.");
3283
3366
  }
3284
3367
  notify("Autonoma", `${entityName} \u2014 implementation ready, waiting for you`);
3285
3368
  const ready = await p4.confirm({
3286
- message: `[${i + 1}/${total}] Is your server running with the ${entityName} factory?`
3369
+ message: `[${i + 1}/${total}] Is your app running locally with the ${entityName} factory wired up?`
3287
3370
  });
3288
3371
  if (p4.isCancel(ready)) throw new Error("Entity loop cancelled");
3289
3372
  if (!ready) {
@@ -3299,7 +3382,7 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
3299
3382
  state.sharedSecret = secret;
3300
3383
  await saveRecipeState(outputDir, state);
3301
3384
  await writeFile7(
3302
- join16(outputDir, "autonoma-config.json"),
3385
+ join19(outputDir, "autonoma-config.json"),
3303
3386
  JSON.stringify({ sharedSecret: secret, endpointUrl: state.sdkEndpointUrl }, null, 2),
3304
3387
  "utf-8"
3305
3388
  );
@@ -3310,7 +3393,7 @@ Add this to your server's .env file and restart it.
3310
3393
  This is a 64-character hex key used for HMAC-SHA256 request signing.
3311
3394
  The same value must be set in both your server and the Autonoma dashboard.
3312
3395
 
3313
- Saved to: ${join16(outputDir, "autonoma-config.json")}`,
3396
+ Saved to: ${join19(outputDir, "autonoma-config.json")}`,
3314
3397
  "Shared secret generated"
3315
3398
  );
3316
3399
  const secretReady = await p4.confirm({
@@ -3332,7 +3415,7 @@ Saved to: ${join16(outputDir, "autonoma-config.json")}`,
3332
3415
  state.sdkEndpointUrl = url.trim() || "http://localhost:3000/api/autonoma";
3333
3416
  await saveRecipeState(outputDir, state);
3334
3417
  await writeFile7(
3335
- join16(outputDir, "autonoma-config.json"),
3418
+ join19(outputDir, "autonoma-config.json"),
3336
3419
  JSON.stringify({ sharedSecret: state.sharedSecret, endpointUrl: state.sdkEndpointUrl }, null, 2),
3337
3420
  "utf-8"
3338
3421
  );
@@ -3363,7 +3446,8 @@ Saved to: ${join16(outputDir, "autonoma-config.json")}`,
3363
3446
  state.entities[entityName].recipeData,
3364
3447
  testResult.feedback,
3365
3448
  model,
3366
- outputDir
3449
+ outputDir,
3450
+ state.entities
3367
3451
  );
3368
3452
  state.entities[entityName].recipeData = revised;
3369
3453
  await saveRecipeState(outputDir, state);
@@ -3423,7 +3507,79 @@ When done, call finish with the instructions text.`;
3423
3507
 
3424
3508
  // src/agents/04-recipe-builder/phases/full-validation.ts
3425
3509
  import * as p5 from "@clack/prompts";
3426
- async function runFullValidation(state, _models, outputDir) {
3510
+ import { tool as tool15 } from "ai";
3511
+ import { z as z15 } from "zod";
3512
+ async function reviseFullRecipe(current, feedback, model, outputDir, entityOrder) {
3513
+ let revised;
3514
+ const finishTool = tool15({
3515
+ description: "Submit the revised full recipe: an object mapping each entity name to its array of records.",
3516
+ inputSchema: z15.object({
3517
+ recipe: z15.record(z15.string(), z15.array(z15.record(z15.string(), z15.unknown())))
3518
+ }),
3519
+ execute: async (input) => {
3520
+ revised = input.recipe;
3521
+ return { done: true };
3522
+ }
3523
+ });
3524
+ const { logger, onStepFinish } = buildDefaultStepLogger("revise:full-recipe", 20);
3525
+ await runAgent(
3526
+ {
3527
+ id: "revise-full-recipe",
3528
+ systemPrompt: `You are revising a full test-data recipe based on user feedback after they reviewed the app populated with this data.
3529
+
3530
+ The recipe is an object mapping entity names to arrays of records. Records use:
3531
+ - _alias: a unique id for a record so other records can point to it
3532
+ - _ref: { "_ref": "alias" } points to a parent record's _alias
3533
+
3534
+ Rules:
3535
+ - Apply the user's feedback across whatever entities it touches.
3536
+ - Keep _ref values pointing to aliases that actually exist in the recipe. Never invent a _ref to a missing alias.
3537
+ - Entities are created in this order (parents first): ${entityOrder.join(" \u2192 ")}. A record may only _ref an alias declared by an entity earlier in that order.
3538
+ - Field names/types must match the schema in entity-audit.md.
3539
+ - Read scenarios.md and entity-audit.md from the output directory as needed.
3540
+
3541
+ Return the COMPLETE revised recipe (all entities, not just the changed ones) via finish.`,
3542
+ model,
3543
+ maxSteps: 20,
3544
+ tools: (_heartbeat) => ({
3545
+ read_output: buildReadFileTool(outputDir),
3546
+ finish: finishTool
3547
+ }),
3548
+ onStepFinish
3549
+ },
3550
+ `The user reviewed the app with this test data and said it doesn't look right.
3551
+
3552
+ Current full recipe:
3553
+ ${JSON.stringify(current, null, 2)}
3554
+
3555
+ User feedback:
3556
+ "${feedback}"
3557
+
3558
+ Revise the recipe to address the feedback, then call finish with the complete updated recipe.`,
3559
+ () => revised
3560
+ );
3561
+ logger.summary();
3562
+ return revised;
3563
+ }
3564
+ async function teardown(sdkConfig, refsToken, successMessage) {
3565
+ if (!refsToken) return true;
3566
+ p5.log.step("[Full validation] Tearing down all entities...");
3567
+ let downResult;
3568
+ try {
3569
+ downResult = await down(sdkConfig, refsToken);
3570
+ } catch (err) {
3571
+ p5.log.error(`Full DOWN request failed: ${err instanceof Error ? err.message : String(err)}`);
3572
+ return false;
3573
+ }
3574
+ if (!downResult.ok) {
3575
+ p5.log.error(`Full DOWN failed (HTTP ${downResult.status}):`);
3576
+ console.log(JSON.stringify(downResult.body, null, 2));
3577
+ return false;
3578
+ }
3579
+ p5.log.success(successMessage);
3580
+ return true;
3581
+ }
3582
+ async function runFullValidation(state, _models, outputDir, model) {
3427
3583
  const total = state.entityOrder.length;
3428
3584
  p5.log.info(
3429
3585
  `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.`
@@ -3442,7 +3598,7 @@ async function runFullValidation(state, _models, outputDir) {
3442
3598
  endpointUrl: state.sdkEndpointUrl,
3443
3599
  sharedSecret: state.sharedSecret ?? ""
3444
3600
  };
3445
- const fullRecipe = buildFullRecipe(state.entityOrder, state.entities);
3601
+ let fullRecipe = buildFullRecipe(state.entityOrder, state.entities);
3446
3602
  while (true) {
3447
3603
  const testRunId = `full-${Date.now()}`;
3448
3604
  p5.log.step(`[Full validation] Creating all ${total} entities...`);
@@ -3500,26 +3656,35 @@ async function runFullValidation(state, _models, outputDir) {
3500
3656
  message: "Does the app look right with the test data?"
3501
3657
  });
3502
3658
  if (p5.isCancel(looksGood)) throw new Error("Cancelled");
3503
- if (!looksGood) {
3504
- p5.log.info("You can adjust the recipe by editing recipe.json or re-running individual entities with --resume.");
3659
+ const torndown = await teardown(
3660
+ sdkConfig,
3661
+ refsToken,
3662
+ looksGood ? "Full lifecycle works. All data was created and torn down cleanly." : "Tore down the test data so we can regenerate it."
3663
+ );
3664
+ if (!torndown) return false;
3665
+ if (looksGood) return true;
3666
+ const feedback = await p5.text({
3667
+ message: "What's wrong with the test data? Describe what to change.",
3668
+ placeholder: "e.g. accounts need realistic balances, transactions should reference the right account..."
3669
+ });
3670
+ if (p5.isCancel(feedback) || !feedback.trim()) {
3671
+ p5.log.info("No feedback given. You can edit recipe.json manually and re-run with --resume.");
3672
+ return false;
3505
3673
  }
3506
- if (refsToken) {
3507
- p5.log.step("[Full validation] Tearing down all entities...");
3508
- let downResult;
3509
- try {
3510
- downResult = await down(sdkConfig, refsToken);
3511
- } catch (err) {
3512
- p5.log.error(`Full DOWN request failed: ${err instanceof Error ? err.message : String(err)}`);
3513
- return false;
3514
- }
3515
- if (!downResult.ok) {
3516
- p5.log.error(`Full DOWN failed (HTTP ${downResult.status}):`);
3517
- console.log(JSON.stringify(downResult.body, null, 2));
3518
- return false;
3674
+ p5.log.info("Revising the full recipe based on your feedback...");
3675
+ const revised = await reviseFullRecipe(fullRecipe, feedback.trim(), model, outputDir, state.entityOrder);
3676
+ if (!revised) {
3677
+ p5.log.warn("Couldn't revise automatically. Edit recipe.json manually and re-run with --resume.");
3678
+ return false;
3679
+ }
3680
+ for (const [name, records] of Object.entries(revised)) {
3681
+ if (state.entities[name]) {
3682
+ state.entities[name].recipeData = records;
3519
3683
  }
3520
- p5.log.success("Full lifecycle works. All data was created and torn down cleanly.");
3521
3684
  }
3522
- return true;
3685
+ await saveRecipeState(outputDir, state);
3686
+ fullRecipe = buildFullRecipe(state.entityOrder, state.entities);
3687
+ p5.note(JSON.stringify(fullRecipe, null, 2), "Revised recipe \u2014 re-running full validation", { format: codeNoteFormat });
3523
3688
  }
3524
3689
  }
3525
3690
  var init_full_validation = __esm({
@@ -3527,6 +3692,9 @@ var init_full_validation = __esm({
3527
3692
  "use strict";
3528
3693
  init_esm_shims();
3529
3694
  init_notify();
3695
+ init_agent();
3696
+ init_tools();
3697
+ init_highlight();
3530
3698
  init_state();
3531
3699
  init_recipe();
3532
3700
  init_http_client();
@@ -3632,7 +3800,7 @@ async function runRecipeBuilder(input) {
3632
3800
  }
3633
3801
  }
3634
3802
  if (state.phase === "full-validation") {
3635
- const success = await runFullValidation(state, models, input.outputDir);
3803
+ const success = await runFullValidation(state, models, input.outputDir, model);
3636
3804
  if (success) {
3637
3805
  state.phase = "submit";
3638
3806
  await saveRecipeState(input.outputDir, state);
@@ -3682,22 +3850,22 @@ var init_recipe_builder = __esm({
3682
3850
  });
3683
3851
 
3684
3852
  // src/agents/05-test-generator/rubrics.ts
3685
- import { z as z15 } from "zod";
3853
+ import { z as z16 } from "zod";
3686
3854
  var dimensionResultSchema, structuralIntentRubric, flowCompletenessRubric, uiTextRubric, dataAccuracyRubric, ALL_RUBRICS;
3687
3855
  var init_rubrics = __esm({
3688
3856
  "src/agents/05-test-generator/rubrics.ts"() {
3689
3857
  "use strict";
3690
3858
  init_esm_shims();
3691
- dimensionResultSchema = z15.object({
3692
- pass: z15.boolean(),
3693
- evidence: z15.string().describe("What you checked and found \u2014 cite file paths, line content, or specific strings"),
3694
- suggestion: z15.string().optional().describe("What the planner agent should fix, if failing")
3859
+ dimensionResultSchema = z16.object({
3860
+ pass: z16.boolean(),
3861
+ evidence: z16.string().describe("What you checked and found \u2014 cite file paths, line content, or specific strings"),
3862
+ suggestion: z16.string().optional().describe("What the planner agent should fix, if failing")
3695
3863
  });
3696
3864
  structuralIntentRubric = {
3697
3865
  name: "structural-intent",
3698
3866
  maxSteps: 8,
3699
3867
  dimensions: ["structuralValidity", "intentQuality", "missionAlignment"],
3700
- resultSchema: z15.object({
3868
+ resultSchema: z16.object({
3701
3869
  structuralValidity: dimensionResultSchema.describe(
3702
3870
  "Are all step verbs valid (click/type/scroll/assert/hover/drag/read/refresh)? Are asserts visual-only (no URLs, network, console)? No code selectors? No login steps?"
3703
3871
  ),
@@ -3738,7 +3906,7 @@ When done reviewing, call finish with your structured evaluation.`
3738
3906
  name: "flow-completeness",
3739
3907
  maxSteps: 12,
3740
3908
  dimensions: ["actionCompletion", "mutationVerification"],
3741
- resultSchema: z15.object({
3909
+ resultSchema: z16.object({
3742
3910
  actionCompletion: dimensionResultSchema.describe(
3743
3911
  "Does the test complete a core action and reach an OUTCOME? Not just opening a modal or clicking a tab."
3744
3912
  ),
@@ -3774,7 +3942,7 @@ When done reviewing, call finish with your structured evaluation.`
3774
3942
  name: "ui-text",
3775
3943
  maxSteps: 20,
3776
3944
  dimensions: ["uiTextAuthenticity"],
3777
- resultSchema: z15.object({
3945
+ resultSchema: z16.object({
3778
3946
  uiTextAuthenticity: dimensionResultSchema.describe(
3779
3947
  "Do all quoted strings in steps reference text a human would actually see on screen? Not translation keys, config paths, component names, enum identifiers, or CSS classes."
3780
3948
  )
@@ -3813,7 +3981,7 @@ When done reviewing, call finish with your structured evaluation.`
3813
3981
  name: "data-accuracy",
3814
3982
  maxSteps: 20,
3815
3983
  dimensions: ["dataAccuracy"],
3816
- resultSchema: z15.object({
3984
+ resultSchema: z16.object({
3817
3985
  dataAccuracy: dimensionResultSchema.describe(
3818
3986
  "Do the referenced UI elements (buttons, labels, fields, headings, toasts) actually exist in the source code for this page? Are default states correct? Does all test data (names, values, entities) come from the scenario data \u2014 NOT from other tests?"
3819
3987
  )
@@ -3866,12 +4034,12 @@ When done reviewing, call finish with your structured evaluation.`
3866
4034
  // src/agents/05-test-generator/review-pass.ts
3867
4035
  import { basename } from "path";
3868
4036
  import "ai";
3869
- import { tool as tool15 } from "ai";
4037
+ import { tool as tool16 } from "ai";
3870
4038
  async function runReviewPass(testContent, testPath, rubric, projectRoot, model, scenarioData) {
3871
4039
  let result;
3872
4040
  const agentLabel = `review:${rubric.name}:${basename(testPath)}`;
3873
4041
  const { onStepFinish } = buildDefaultStepLogger(agentLabel, rubric.maxSteps);
3874
- const finishTool = tool15({
4042
+ const finishTool = tool16({
3875
4043
  description: "Submit your structured review. Every dimension must have evidence from your investigation.",
3876
4044
  inputSchema: rubric.resultSchema,
3877
4045
  execute: async (input) => {
@@ -3928,8 +4096,8 @@ var init_review_pass = __esm({
3928
4096
  });
3929
4097
 
3930
4098
  // src/agents/05-test-generator/review.ts
3931
- import { readFile as readFile13 } from "fs/promises";
3932
- import { join as join17, relative as relative5, basename as basename2 } from "path";
4099
+ import { readFile as readFile14 } from "fs/promises";
4100
+ import { join as join20, relative as relative5, basename as basename2 } from "path";
3933
4101
  import { glob as glob4 } from "glob";
3934
4102
  import "ai";
3935
4103
  async function reviewSingleTest(testContent, testPath, projectRoot, model, scenarioData) {
@@ -3956,19 +4124,19 @@ async function reviewSingleTest(testContent, testPath, projectRoot, model, scena
3956
4124
  return merged;
3957
4125
  }
3958
4126
  async function runConsolidatedReview(outputDir, projectRoot, model) {
3959
- const testsDir = join17(outputDir, "qa-tests");
4127
+ const testsDir = join20(outputDir, "qa-tests");
3960
4128
  const logger = createStepLogger("review", 5);
3961
4129
  let scenarioData;
3962
4130
  try {
3963
- scenarioData = await readFile13(join17(outputDir, "scenarios.md"), "utf-8");
4131
+ scenarioData = await readFile14(join20(outputDir, "scenarios.md"), "utf-8");
3964
4132
  } catch {
3965
4133
  }
3966
- const testFiles = await glob4(join17(testsDir, "**/*.md"));
4134
+ const testFiles = await glob4(join20(testsDir, "**/*.md"));
3967
4135
  const tests = [];
3968
4136
  for (const testPath of testFiles) {
3969
4137
  if (basename2(testPath) === "INDEX.md") continue;
3970
4138
  if (testPath.includes("/_invalid/")) continue;
3971
- const content = await readFile13(testPath, "utf-8");
4139
+ const content = await readFile14(testPath, "utf-8");
3972
4140
  const flowMatch = content.match(/^---\n[\s\S]*?flow:\s*["']?([^"'\n]+)["']?\s*\n[\s\S]*?---/m);
3973
4141
  tests.push({
3974
4142
  path: testPath,
@@ -4045,16 +4213,16 @@ var init_review2 = __esm({
4045
4213
  });
4046
4214
 
4047
4215
  // src/agents/05-test-generator/graph.ts
4048
- import { readFile as readFile14, writeFile as writeFile8 } from "fs/promises";
4049
- import { join as join18 } from "path";
4216
+ import { readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
4217
+ import { join as join21 } from "path";
4050
4218
  async function saveBfsState(outputDir, state) {
4051
- const path3 = join18(outputDir, STATE_FILE3);
4219
+ const path3 = join21(outputDir, STATE_FILE3);
4052
4220
  await writeFile8(path3, JSON.stringify(state.serialize(), null, 2), "utf-8");
4053
4221
  }
4054
4222
  async function loadBfsState(outputDir) {
4055
- const path3 = join18(outputDir, STATE_FILE3);
4223
+ const path3 = join21(outputDir, STATE_FILE3);
4056
4224
  try {
4057
- const raw = await readFile14(path3, "utf-8");
4225
+ const raw = await readFile15(path3, "utf-8");
4058
4226
  return CoverageState.deserialize(JSON.parse(raw));
4059
4227
  } catch {
4060
4228
  return null;
@@ -4146,17 +4314,17 @@ var init_graph = __esm({
4146
4314
  });
4147
4315
 
4148
4316
  // src/agents/00b-feature-discovery/index.ts
4149
- import { readFile as readFile15, writeFile as writeFile9 } from "fs/promises";
4150
- import { join as join19 } from "path";
4151
- import { z as z16 } from "zod";
4152
- import { tool as tool16 } from "ai";
4317
+ import { readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
4318
+ import { join as join22 } from "path";
4319
+ import { z as z17 } from "zod";
4320
+ import { tool as tool17 } from "ai";
4153
4321
  async function saveFeatures(outputDir, features) {
4154
4322
  const obj = Object.fromEntries(features);
4155
- await writeFile9(join19(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
4323
+ await writeFile9(join22(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
4156
4324
  }
4157
4325
  async function loadFeatures(outputDir) {
4158
4326
  try {
4159
- const raw = await readFile15(join19(outputDir, FEATURES_FILE), "utf-8");
4327
+ const raw = await readFile16(join22(outputDir, FEATURES_FILE), "utf-8");
4160
4328
  const obj = JSON.parse(raw);
4161
4329
  return new Map(Object.entries(obj));
4162
4330
  } catch {
@@ -4187,10 +4355,10 @@ Process every page. Call add_feature for each sub-feature you discover. When don
4187
4355
  const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
4188
4356
  return {
4189
4357
  ...tools,
4190
- add_feature: tool16({
4358
+ add_feature: tool17({
4191
4359
  description: "Add a discovered sub-feature",
4192
4360
  inputSchema: Feature.extend({
4193
- id: z16.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
4361
+ id: z17.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
4194
4362
  }),
4195
4363
  execute: (featureInput) => {
4196
4364
  const { id, ...rest } = featureInput;
@@ -4202,19 +4370,19 @@ Process every page. Call add_feature for each sub-feature you discover. When don
4202
4370
  return `Feature "${id}" added (${collector.features.size} total)`;
4203
4371
  }
4204
4372
  }),
4205
- view_features: tool16({
4373
+ view_features: tool17({
4206
4374
  description: "View all discovered features so far",
4207
- inputSchema: z16.object({}),
4375
+ inputSchema: z17.object({}),
4208
4376
  execute: () => collector.viewFeatures()
4209
4377
  }),
4210
- view_pages: tool16({
4378
+ view_pages: tool17({
4211
4379
  description: "View the pages list to know what to analyze",
4212
- inputSchema: z16.object({}),
4380
+ inputSchema: z17.object({}),
4213
4381
  execute: () => pagesDescription
4214
4382
  }),
4215
- finish: tool16({
4383
+ finish: tool17({
4216
4384
  description: "Signal that feature discovery is complete",
4217
- inputSchema: z16.object({ summary: z16.string() }),
4385
+ inputSchema: z17.object({ summary: z17.string() }),
4218
4386
  execute: async (finishInput) => {
4219
4387
  result = {
4220
4388
  success: true,
@@ -4245,13 +4413,13 @@ var init_b_feature_discovery = __esm({
4245
4413
  init_model();
4246
4414
  init_tools();
4247
4415
  FEATURES_FILE = "features.json";
4248
- Feature = z16.object({
4249
- name: z16.string().min(1).describe("Human-readable name (e.g. 'Settings > Notifications Tab', 'Create Project Modal')"),
4250
- type: z16.enum(["tab", "modal", "form", "table", "wizard", "nested-route", "complex-component"]),
4251
- parentPagePath: z16.string().min(1).describe("The page path this feature belongs to (from the pages list)"),
4252
- sourceFiles: z16.array(z16.string()).min(1).describe("Relative paths to the source files for this sub-feature"),
4253
- interactiveElements: z16.number().int().min(0).describe("Count of interactive elements found (buttons, inputs, toggles, etc.)"),
4254
- description: z16.string().min(10).describe("What this sub-feature does")
4416
+ Feature = z17.object({
4417
+ name: z17.string().min(1).describe("Human-readable name (e.g. 'Settings > Notifications Tab', 'Create Project Modal')"),
4418
+ type: z17.enum(["tab", "modal", "form", "table", "wizard", "nested-route", "complex-component"]),
4419
+ parentPagePath: z17.string().min(1).describe("The page path this feature belongs to (from the pages list)"),
4420
+ sourceFiles: z17.array(z17.string()).min(1).describe("Relative paths to the source files for this sub-feature"),
4421
+ interactiveElements: z17.number().int().min(0).describe("Count of interactive elements found (buttons, inputs, toggles, etc.)"),
4422
+ description: z17.string().min(10).describe("What this sub-feature does")
4255
4423
  });
4256
4424
  FeatureCollector = class {
4257
4425
  features = /* @__PURE__ */ new Map();
@@ -4332,14 +4500,14 @@ Use kebab-case IDs that indicate the parent page and feature type:
4332
4500
  });
4333
4501
 
4334
4502
  // src/agents/05-test-generator/validation.ts
4335
- import matter from "gray-matter";
4503
+ import matter2 from "gray-matter";
4336
4504
  function validateTestContent(content) {
4337
4505
  const errors = [];
4338
4506
  if (!/^---\n[\s\S]*?\n---/.test(content)) {
4339
4507
  errors.push("Missing frontmatter");
4340
4508
  } else {
4341
4509
  try {
4342
- const { data } = matter(content);
4510
+ const { data } = matter2(content);
4343
4511
  if (!data.verification || typeof data.verification !== "string" || data.verification.length < 20) {
4344
4512
  errors.push("Missing or insufficient 'verification' field in frontmatter \u2014 must describe WHERE to navigate and WHAT to assert at the source of truth");
4345
4513
  }
@@ -4394,18 +4562,18 @@ var init_validation = __esm({
4394
4562
 
4395
4563
  // src/agents/05-test-generator/tools.ts
4396
4564
  import { mkdir as mkdir3, writeFile as writeFile10 } from "fs/promises";
4397
- import { dirname as dirname2, join as join20 } from "path";
4398
- import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as tool17, ToolLoopAgent as ToolLoopAgent3 } from "ai";
4399
- import matter2 from "gray-matter";
4400
- import { z as z17 } from "zod";
4565
+ import { dirname as dirname2, join as join23 } from "path";
4566
+ import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as tool18, ToolLoopAgent as ToolLoopAgent3 } from "ai";
4567
+ import matter3 from "gray-matter";
4568
+ import { z as z18 } from "zod";
4401
4569
  function buildWriteTestTool(state, outputDir) {
4402
- return tool17({
4570
+ return tool18({
4403
4571
  description: "Write a test file to qa-tests/{folder}/{filename}.md. Validates frontmatter before writing. Returns error if frontmatter is invalid.",
4404
- inputSchema: z17.object({
4405
- folder: z17.string().describe("Subfolder name under qa-tests/"),
4406
- filename: z17.string().describe("File name (e.g. login-valid-credentials.md)"),
4407
- content: z17.string().describe("Full file content including YAML frontmatter"),
4408
- nodeId: z17.string().describe("The FeatureNode ID this test belongs to")
4572
+ inputSchema: z18.object({
4573
+ folder: z18.string().describe("Subfolder name under qa-tests/"),
4574
+ filename: z18.string().describe("File name (e.g. login-valid-credentials.md)"),
4575
+ content: z18.string().describe("Full file content including YAML frontmatter"),
4576
+ nodeId: z18.string().describe("The FeatureNode ID this test belongs to")
4409
4577
  }),
4410
4578
  execute: async (input) => {
4411
4579
  const frontmatter = extractFrontmatter(input.content);
@@ -4454,8 +4622,8 @@ function buildWriteTestTool(state, outputDir) {
4454
4622
  };
4455
4623
  }
4456
4624
  }
4457
- const relPath = join20("qa-tests", input.folder, input.filename);
4458
- const absPath = join20(outputDir, relPath);
4625
+ const relPath = join23("qa-tests", input.folder, input.filename);
4626
+ const absPath = join23(outputDir, relPath);
4459
4627
  try {
4460
4628
  await mkdir3(dirname2(absPath), { recursive: true });
4461
4629
  await writeFile10(absPath, input.content, "utf-8");
@@ -4470,16 +4638,16 @@ function buildWriteTestTool(state, outputDir) {
4470
4638
  });
4471
4639
  }
4472
4640
  function buildCreateFolderTool(outputDir) {
4473
- return tool17({
4641
+ return tool18({
4474
4642
  description: "Create a folder under qa-tests/ for organizing tests.",
4475
- inputSchema: z17.object({
4476
- folder: z17.string().describe("Folder name (kebab-case)")
4643
+ inputSchema: z18.object({
4644
+ folder: z18.string().describe("Folder name (kebab-case)")
4477
4645
  }),
4478
4646
  execute: async (input) => {
4479
- const absPath = join20(outputDir, "qa-tests", input.folder);
4647
+ const absPath = join23(outputDir, "qa-tests", input.folder);
4480
4648
  try {
4481
4649
  await mkdir3(absPath, { recursive: true });
4482
- return { path: join20("qa-tests", input.folder) };
4650
+ return { path: join23("qa-tests", input.folder) };
4483
4651
  } catch (err) {
4484
4652
  const message = err instanceof Error ? err.message : String(err);
4485
4653
  return { error: `Failed to create folder: ${message}` };
@@ -4488,9 +4656,9 @@ function buildCreateFolderTool(outputDir) {
4488
4656
  });
4489
4657
  }
4490
4658
  function buildNextNodeTool(state, outputDir) {
4491
- return tool17({
4659
+ return tool18({
4492
4660
  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.",
4493
- inputSchema: z17.object({}),
4661
+ inputSchema: z18.object({}),
4494
4662
  execute: async () => {
4495
4663
  const next = state.nextNode();
4496
4664
  await saveBfsState(outputDir, state);
@@ -4517,9 +4685,9 @@ function buildNextNodeTool(state, outputDir) {
4517
4685
  });
4518
4686
  }
4519
4687
  function buildGetProgressTool(state) {
4520
- return tool17({
4688
+ return tool18({
4521
4689
  description: "Check how many nodes have been tested vs how many remain.",
4522
- inputSchema: z17.object({}),
4690
+ inputSchema: z18.object({}),
4523
4691
  execute: async () => {
4524
4692
  const stats = state.summary();
4525
4693
  const nodes = [...state.nodes.values()].map((n) => ({
@@ -4533,14 +4701,14 @@ function buildGetProgressTool(state) {
4533
4701
  });
4534
4702
  }
4535
4703
  function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
4536
- return tool17({
4704
+ return tool18({
4537
4705
  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.",
4538
- inputSchema: z17.object({
4539
- instruction: z17.string().describe("What to research \u2014 be specific about files and what to look for")
4706
+ inputSchema: z18.object({
4707
+ instruction: z18.string().describe("What to research \u2014 be specific about files and what to look for")
4540
4708
  }),
4541
4709
  execute: async (input) => {
4542
- const resultSchema2 = z17.object({
4543
- findings: z17.string().describe("Summary of what was found")
4710
+ const resultSchema2 = z18.object({
4711
+ findings: z18.string().describe("Summary of what was found")
4544
4712
  });
4545
4713
  let result;
4546
4714
  const subagent = new ToolLoopAgent3({
@@ -4551,7 +4719,7 @@ function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
4551
4719
  glob: buildGlobTool(workingDirectory),
4552
4720
  grep: buildGrepTool(workingDirectory),
4553
4721
  read_file: buildReadFileTool(workingDirectory),
4554
- finish: tool17({
4722
+ finish: tool18({
4555
4723
  description: "Report your findings.",
4556
4724
  inputSchema: resultSchema2,
4557
4725
  execute: async (output) => {
@@ -4578,7 +4746,7 @@ function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
4578
4746
  }
4579
4747
  function extractFrontmatter(content) {
4580
4748
  try {
4581
- const { data } = matter2(content);
4749
+ const { data } = matter3(content);
4582
4750
  return data && Object.keys(data).length > 0 ? data : null;
4583
4751
  } catch {
4584
4752
  return null;
@@ -4592,14 +4760,14 @@ var init_tools2 = __esm({
4592
4760
  init_tools();
4593
4761
  init_graph();
4594
4762
  init_validation();
4595
- testFrontmatterSchema = z17.object({
4596
- title: z17.string().min(1),
4597
- description: z17.string().min(1),
4598
- intent: z17.string().min(30, "Intent must be at least 30 characters \u2014 describe the BEHAVIOR being tested, not the steps"),
4599
- criticality: z17.enum(["critical", "high", "mid", "low"]),
4600
- scenario: z17.string().min(1),
4601
- flow: z17.string().min(1),
4602
- verification: z17.string().min(20, "Verification must describe WHERE to navigate and WHAT to assert at the source of truth \u2014 not UI acknowledgments like toasts")
4763
+ testFrontmatterSchema = z18.object({
4764
+ title: z18.string().min(1),
4765
+ description: z18.string().min(1),
4766
+ intent: z18.string().min(30, "Intent must be at least 30 characters \u2014 describe the BEHAVIOR being tested, not the steps"),
4767
+ criticality: z18.enum(["critical", "high", "mid", "low"]),
4768
+ scenario: z18.string().min(1),
4769
+ flow: z18.string().min(1),
4770
+ verification: z18.string().min(20, "Verification must describe WHERE to navigate and WHAT to assert at the source of truth \u2014 not UI acknowledgments like toasts")
4603
4771
  });
4604
4772
  }
4605
4773
  });
@@ -4995,10 +5163,10 @@ var test_generator_exports = {};
4995
5163
  __export(test_generator_exports, {
4996
5164
  runTestGenerator: () => runTestGenerator
4997
5165
  });
4998
- import { mkdir as mkdir4, readFile as readFile16, rmdir, unlink, writeFile as writeFile11 } from "fs/promises";
4999
- import { basename as basename3, join as join21 } from "path";
5000
- import { tool as tool18 } from "ai";
5001
- import { z as z18 } from "zod";
5166
+ import { mkdir as mkdir4, readFile as readFile17, rmdir, unlink, writeFile as writeFile11 } from "fs/promises";
5167
+ import { basename as basename3, join as join24 } from "path";
5168
+ import { tool as tool19 } from "ai";
5169
+ import { z as z19 } from "zod";
5002
5170
  import { glob as glob5 } from "glob";
5003
5171
  async function preseedQueue(state, projectRoot, pages, features) {
5004
5172
  let seeded = 0;
@@ -5046,10 +5214,10 @@ async function runTestGenerator(input) {
5046
5214
  const existingState = await loadBfsState(input.outputDir);
5047
5215
  const state = existingState ?? new CoverageState();
5048
5216
  let result;
5049
- const finishTool = tool18({
5217
+ const finishTool = tool19({
5050
5218
  description: "Call when the BFS queue is empty and all routes have been explored.",
5051
- inputSchema: z18.object({
5052
- summary: z18.string().describe("Coverage summary")
5219
+ inputSchema: z19.object({
5220
+ summary: z19.string().describe("Coverage summary")
5053
5221
  }),
5054
5222
  execute: async (finishInput) => {
5055
5223
  const stats = state.summary();
@@ -5078,8 +5246,8 @@ async function runTestGenerator(input) {
5078
5246
  });
5079
5247
  let kbContext = "";
5080
5248
  try {
5081
- const autonomaMd = await readFile16(
5082
- join21(input.outputDir, "AUTONOMA.md"),
5249
+ const autonomaMd = await readFile17(
5250
+ join24(input.outputDir, "AUTONOMA.md"),
5083
5251
  "utf-8"
5084
5252
  );
5085
5253
  kbContext += `
@@ -5090,8 +5258,8 @@ ${autonomaMd}
5090
5258
  } catch {
5091
5259
  }
5092
5260
  try {
5093
- const scenariosMd = await readFile16(
5094
- join21(input.outputDir, "scenarios.md"),
5261
+ const scenariosMd = await readFile17(
5262
+ join24(input.outputDir, "scenarios.md"),
5095
5263
  "utf-8"
5096
5264
  );
5097
5265
  kbContext += `
@@ -5287,18 +5455,18 @@ IMPORTANT: Do NOT try to finish early. Process every node via next_node until it
5287
5455
  console.log(` Fix pass complete`);
5288
5456
  }
5289
5457
  const allTestFiles = await glob5(
5290
- join21(input.outputDir, "qa-tests", "**/*.md")
5458
+ join24(input.outputDir, "qa-tests", "**/*.md")
5291
5459
  );
5292
5460
  let markedInvalid = 0;
5293
5461
  for (const testPath of allTestFiles) {
5294
5462
  if (basename3(testPath) === "INDEX.md") continue;
5295
5463
  if (testPath.includes("/_invalid/")) continue;
5296
- const content = await readFile16(testPath, "utf-8");
5464
+ const content = await readFile17(testPath, "utf-8");
5297
5465
  const validation = validateTestContent(content);
5298
5466
  if (!validation.valid) {
5299
- const invalidDir = join21(input.outputDir, "qa-tests", "_invalid");
5467
+ const invalidDir = join24(input.outputDir, "qa-tests", "_invalid");
5300
5468
  await mkdir4(invalidDir, { recursive: true });
5301
- const dest = join21(invalidDir, basename3(testPath));
5469
+ const dest = join24(invalidDir, basename3(testPath));
5302
5470
  const annotated = `<!-- VALIDATION ERRORS: ${validation.errors.join("; ")} -->
5303
5471
  ${content}`;
5304
5472
  await writeFile11(dest, annotated, "utf-8");
@@ -5311,7 +5479,7 @@ ${content}`;
5311
5479
  ` ${markedInvalid} tests still invalid after review cycles \u2014 moved to _invalid/`
5312
5480
  );
5313
5481
  }
5314
- const dirs = await glob5(join21(input.outputDir, "qa-tests", "**/"), {
5482
+ const dirs = await glob5(join24(input.outputDir, "qa-tests", "**/"), {
5315
5483
  dot: false
5316
5484
  });
5317
5485
  for (const dir of dirs.sort((a, b) => b.length - a.length)) {
@@ -5403,7 +5571,7 @@ async function generateIndex(outputDir, state) {
5403
5571
  for (const paths of state.testsWritten.values()) {
5404
5572
  for (const p9 of paths) {
5405
5573
  try {
5406
- const content2 = await readFile16(join21(outputDir, p9), "utf-8");
5574
+ const content2 = await readFile17(join24(outputDir, p9), "utf-8");
5407
5575
  const critMatch = content2.match(/criticality:\s*(\w+)/);
5408
5576
  const critVal = critMatch?.[1] ?? "";
5409
5577
  if (critCounts.has(critVal))
@@ -5454,26 +5622,26 @@ ${folders.map((f) => `| ${f.name} | ${f.test_count} |`).join("\n")}
5454
5622
 
5455
5623
  ${[...testsByFolder.entries()].flatMap(([_folder, tests]) => tests.map((t) => `- \`${t}\``)).join("\n")}
5456
5624
  `;
5457
- await writeFile11(join21(outputDir, "qa-tests", "INDEX.md"), content, "utf-8");
5625
+ await writeFile11(join24(outputDir, "qa-tests", "INDEX.md"), content, "utf-8");
5458
5626
  }
5459
5627
  async function generateJourneyTests(outputDir, model, projectRoot) {
5460
5628
  const logger = createStepLogger("journeys", 50);
5461
5629
  let autonomaMd = "";
5462
5630
  let scenariosMd = "";
5463
5631
  try {
5464
- autonomaMd = await readFile16(join21(outputDir, "AUTONOMA.md"), "utf-8");
5632
+ autonomaMd = await readFile17(join24(outputDir, "AUTONOMA.md"), "utf-8");
5465
5633
  } catch {
5466
5634
  }
5467
5635
  try {
5468
- scenariosMd = await readFile16(join21(outputDir, "scenarios.md"), "utf-8");
5636
+ scenariosMd = await readFile17(join24(outputDir, "scenarios.md"), "utf-8");
5469
5637
  } catch {
5470
5638
  }
5471
5639
  if (!autonomaMd) return 0;
5472
- const existingTests = await glob5(join21(outputDir, "qa-tests", "**/*.md"));
5640
+ const existingTests = await glob5(join24(outputDir, "qa-tests", "**/*.md"));
5473
5641
  const existingTitles = [];
5474
5642
  for (const t of existingTests) {
5475
5643
  if (basename3(t) === "INDEX.md") continue;
5476
- const content = await readFile16(t, "utf-8");
5644
+ const content = await readFile17(t, "utf-8");
5477
5645
  const titleMatch = content.match(/title:\s*"([^"]+)"/);
5478
5646
  if (titleMatch) existingTitles.push(titleMatch[1]);
5479
5647
  }
@@ -5516,9 +5684,9 @@ Write 5-8 journey tests using the write_test tool with folder "journeys". Then c
5516
5684
  status: "queued"
5517
5685
  });
5518
5686
  let journeyResult;
5519
- const journeyFinish = tool18({
5687
+ const journeyFinish = tool19({
5520
5688
  description: "Signal journey generation is complete.",
5521
- inputSchema: z18.object({ summary: z18.string() }),
5689
+ inputSchema: z19.object({ summary: z19.string() }),
5522
5690
  execute: async (finishInput) => {
5523
5691
  journeyResult = {
5524
5692
  success: true,
@@ -5578,17 +5746,90 @@ var init_test_generator = __esm({
5578
5746
  // src/index.ts
5579
5747
  init_esm_shims();
5580
5748
  import * as p8 from "@clack/prompts";
5581
- import { readFile as readFile17, writeFile as writeFile12 } from "fs/promises";
5582
- import { join as join22 } from "path";
5749
+ import { readFile as readFile18, writeFile as writeFile12 } from "fs/promises";
5750
+ import { join as join25 } from "path";
5583
5751
 
5584
5752
  // src/config.ts
5585
5753
  init_esm_shims();
5586
- import { resolve, join } from "path";
5587
- import { readFileSync } from "fs";
5754
+ import { resolve, join as join2 } from "path";
5755
+ import { readFileSync as readFileSync2 } from "fs";
5756
+
5757
+ // src/core/global-env.ts
5758
+ init_esm_shims();
5759
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
5760
+ import { join } from "path";
5761
+ import { homedir } from "os";
5762
+ var AUTONOMA_HOME = join(homedir(), ".autonoma");
5763
+ var GLOBAL_ENV_PATH = join(AUTONOMA_HOME, ".env");
5764
+ function getGlobalEnvPath() {
5765
+ return GLOBAL_ENV_PATH;
5766
+ }
5767
+ function parseEnvContent(content) {
5768
+ const out = {};
5769
+ for (const line of content.split("\n")) {
5770
+ const trimmed = line.trim();
5771
+ if (!trimmed || trimmed.startsWith("#")) continue;
5772
+ const eqIdx = trimmed.indexOf("=");
5773
+ if (eqIdx === -1) continue;
5774
+ const key = trimmed.slice(0, eqIdx).trim();
5775
+ let value = trimmed.slice(eqIdx + 1).trim();
5776
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
5777
+ value = value.slice(1, -1);
5778
+ }
5779
+ out[key] = value;
5780
+ }
5781
+ return out;
5782
+ }
5783
+ function loadGlobalEnv() {
5784
+ let content;
5785
+ try {
5786
+ content = readFileSync(GLOBAL_ENV_PATH, "utf-8");
5787
+ } catch {
5788
+ return;
5789
+ }
5790
+ for (const [key, value] of Object.entries(parseEnvContent(content))) {
5791
+ if (!(key in process.env)) {
5792
+ process.env[key] = value;
5793
+ }
5794
+ }
5795
+ }
5796
+ function setGlobalEnv(key, value) {
5797
+ mkdirSync(AUTONOMA_HOME, { recursive: true });
5798
+ let lines = [];
5799
+ try {
5800
+ lines = readFileSync(GLOBAL_ENV_PATH, "utf-8").split("\n");
5801
+ } catch {
5802
+ lines = [];
5803
+ }
5804
+ const serialized = `${key}=${value}`;
5805
+ let replaced = false;
5806
+ lines = lines.map((line) => {
5807
+ const trimmed = line.trim();
5808
+ if (trimmed.startsWith("#") || !trimmed.includes("=")) return line;
5809
+ const lineKey = trimmed.slice(0, trimmed.indexOf("=")).trim();
5810
+ if (lineKey === key) {
5811
+ replaced = true;
5812
+ return serialized;
5813
+ }
5814
+ return line;
5815
+ });
5816
+ if (!replaced) {
5817
+ if (lines.length > 0 && lines[lines.length - 1].trim() === "") {
5818
+ lines.splice(lines.length - 1, 0, serialized);
5819
+ } else {
5820
+ lines.push(serialized);
5821
+ }
5822
+ }
5823
+ const output = lines.join("\n").replace(/\n*$/, "\n");
5824
+ writeFileSync(GLOBAL_ENV_PATH, output, { encoding: "utf-8", mode: 384 });
5825
+ process.env[key] = value;
5826
+ }
5827
+
5828
+ // src/config.ts
5588
5829
  function loadProjectEnv(projectRoot) {
5589
5830
  let content;
5590
5831
  try {
5591
- content = readFileSync(join(projectRoot, ".env"), "utf-8");
5832
+ content = readFileSync2(join2(projectRoot, ".env"), "utf-8");
5592
5833
  } catch {
5593
5834
  return;
5594
5835
  }
@@ -5610,6 +5851,7 @@ function loadProjectEnv(projectRoot) {
5610
5851
  function loadConfig(args) {
5611
5852
  const projectRoot = resolve(args.project ?? process.cwd());
5612
5853
  loadProjectEnv(projectRoot);
5854
+ loadGlobalEnv();
5613
5855
  const projectSlug = args.slug ?? projectRoot.split("/").pop()?.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ?? "default";
5614
5856
  return {
5615
5857
  projectRoot,
@@ -5632,11 +5874,11 @@ init_model();
5632
5874
  // src/core/output.ts
5633
5875
  init_esm_shims();
5634
5876
  import { mkdir } from "fs/promises";
5635
- import { join as join3 } from "path";
5636
- import { homedir } from "os";
5637
- var AUTONOMA_HOME = join3(homedir(), ".autonoma");
5877
+ import { join as join4 } from "path";
5878
+ import { homedir as homedir2 } from "os";
5879
+ var AUTONOMA_HOME2 = join4(homedir2(), ".autonoma");
5638
5880
  function getOutputDir(projectSlug) {
5639
- return join3(AUTONOMA_HOME, projectSlug);
5881
+ return join4(AUTONOMA_HOME2, projectSlug);
5640
5882
  }
5641
5883
  async function ensureOutputDir(projectSlug) {
5642
5884
  const dir = getOutputDir(projectSlug);
@@ -5644,10 +5886,140 @@ async function ensureOutputDir(projectSlug) {
5644
5886
  return dir;
5645
5887
  }
5646
5888
 
5889
+ // src/core/interrupt.ts
5890
+ init_esm_shims();
5891
+ import readline from "readline";
5892
+ import { settings } from "@clack/core";
5893
+ var DIM = "\x1B[2m";
5894
+ var RESET = "\x1B[0m";
5895
+ var SHOW_CURSOR = "\x1B[?25h";
5896
+ var EXIT_HINT = `${DIM}(press Ctrl+C again to exit)${RESET}`;
5897
+ var ARM_WINDOW_MS = 3e3;
5898
+ var installed = false;
5899
+ var armed = false;
5900
+ var armTimer = null;
5901
+ var onExit = null;
5902
+ function disarm() {
5903
+ if (armTimer) clearTimeout(armTimer);
5904
+ armTimer = null;
5905
+ armed = false;
5906
+ }
5907
+ function handleInterrupt() {
5908
+ if (armed) {
5909
+ disarm();
5910
+ onExit?.();
5911
+ return;
5912
+ }
5913
+ armed = true;
5914
+ process.stderr.write(`
5915
+ ${EXIT_HINT}
5916
+ `);
5917
+ armTimer = setTimeout(disarm, ARM_WINDOW_MS);
5918
+ }
5919
+ function installInterruptHandler(opts) {
5920
+ onExit = opts.onExit;
5921
+ if (installed) return;
5922
+ installed = true;
5923
+ settings.aliases.delete("escape");
5924
+ process.on("SIGINT", handleInterrupt);
5925
+ const original = readline.createInterface.bind(readline);
5926
+ readline.createInterface = ((...args) => {
5927
+ const iface = original(...args);
5928
+ iface.on("SIGINT", handleInterrupt);
5929
+ return iface;
5930
+ });
5931
+ }
5932
+ function restoreTerminal() {
5933
+ try {
5934
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
5935
+ } catch {
5936
+ }
5937
+ process.stdout.write(SHOW_CURSOR);
5938
+ }
5939
+
5940
+ // src/core/analytics.ts
5941
+ init_esm_shims();
5942
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
5943
+ import { join as join5 } from "path";
5944
+ import { homedir as homedir3 } from "os";
5945
+ import { randomUUID } from "crypto";
5946
+ var AUTONOMA_HOME3 = join5(homedir3(), ".autonoma");
5947
+ var DEVICE_ID_PATH = join5(AUTONOMA_HOME3, ".device-id");
5948
+ var POSTHOG_PUBLIC_KEY = "phc_mUOwUj62r8vyiisFPvXLC3G5RftETIBMnKNSHqTBdka";
5949
+ var DEFAULT_HOST = "https://us.i.posthog.com";
5950
+ function resolveKey() {
5951
+ return (process.env.AUTONOMA_POSTHOG_KEY ?? POSTHOG_PUBLIC_KEY).trim();
5952
+ }
5953
+ function resolveHost() {
5954
+ return (process.env.AUTONOMA_POSTHOG_HOST ?? DEFAULT_HOST).replace(/\/+$/, "");
5955
+ }
5956
+ function trackingDisabled() {
5957
+ const v = process.env.DONT_TRACK;
5958
+ return v === "1" || v === "true";
5959
+ }
5960
+ function getIdentity() {
5961
+ const id = process.env.AUTONOMA_DISTINCT_ID?.trim();
5962
+ return id && id.length > 0 ? id : void 0;
5963
+ }
5964
+ var cachedDeviceId = null;
5965
+ function getDeviceId() {
5966
+ if (cachedDeviceId) return cachedDeviceId;
5967
+ try {
5968
+ cachedDeviceId = readFileSync3(DEVICE_ID_PATH, "utf-8").trim();
5969
+ if (cachedDeviceId) return cachedDeviceId;
5970
+ } catch {
5971
+ }
5972
+ cachedDeviceId = randomUUID();
5973
+ try {
5974
+ mkdirSync2(AUTONOMA_HOME3, { recursive: true });
5975
+ writeFileSync2(DEVICE_ID_PATH, cachedDeviceId, { encoding: "utf-8", mode: 384 });
5976
+ } catch {
5977
+ }
5978
+ return cachedDeviceId;
5979
+ }
5980
+ var enabled = null;
5981
+ function isEnabled() {
5982
+ if (enabled === null) {
5983
+ enabled = !trackingDisabled() && resolveKey().length > 0;
5984
+ }
5985
+ return enabled;
5986
+ }
5987
+ var pending = /* @__PURE__ */ new Set();
5988
+ function track(event, properties = {}) {
5989
+ if (!isEnabled()) return;
5990
+ const identity = getIdentity();
5991
+ const body = JSON.stringify({
5992
+ api_key: resolveKey(),
5993
+ event,
5994
+ distinct_id: identity ?? getDeviceId(),
5995
+ properties: {
5996
+ ...properties,
5997
+ // Only build a person profile when we have a real identity from the app,
5998
+ // so the CLI joins the existing funnel person instead of creating a new one.
5999
+ $process_person_profile: identity != null,
6000
+ cli_version: process.env.npm_package_version
6001
+ }
6002
+ });
6003
+ const promise = fetch(`${resolveHost()}/capture/`, {
6004
+ method: "POST",
6005
+ headers: { "Content-Type": "application/json" },
6006
+ body
6007
+ }).catch(() => {
6008
+ }).finally(() => pending.delete(promise));
6009
+ pending.add(promise);
6010
+ }
6011
+ async function flushAnalytics(timeoutMs = 1500) {
6012
+ if (pending.size === 0) return;
6013
+ await Promise.race([
6014
+ Promise.allSettled([...pending]),
6015
+ new Promise((resolve5) => setTimeout(resolve5, timeoutMs))
6016
+ ]);
6017
+ }
6018
+
5647
6019
  // src/core/state.ts
5648
6020
  init_esm_shims();
5649
6021
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
5650
- import { join as join4 } from "path";
6022
+ import { join as join6 } from "path";
5651
6023
  var STATE_FILE = ".pipeline-state.json";
5652
6024
  function initialState() {
5653
6025
  return {
@@ -5662,7 +6034,7 @@ function initialState() {
5662
6034
  };
5663
6035
  }
5664
6036
  async function loadState(outputDir) {
5665
- const path3 = join4(outputDir, STATE_FILE);
6037
+ const path3 = join6(outputDir, STATE_FILE);
5666
6038
  try {
5667
6039
  const raw = await readFile2(path3, "utf-8");
5668
6040
  return JSON.parse(raw);
@@ -5671,7 +6043,7 @@ async function loadState(outputDir) {
5671
6043
  }
5672
6044
  }
5673
6045
  async function saveState(outputDir, state) {
5674
- const path3 = join4(outputDir, STATE_FILE);
6046
+ const path3 = join6(outputDir, STATE_FILE);
5675
6047
  await writeFile2(path3, JSON.stringify(state, null, 2), "utf-8");
5676
6048
  }
5677
6049
  async function markStep(outputDir, state, step, status) {
@@ -5691,11 +6063,11 @@ function nextPendingStep(state) {
5691
6063
  var PAGES_FILE = "pages.json";
5692
6064
  async function savePages(outputDir, pages) {
5693
6065
  const obj = Object.fromEntries(pages);
5694
- await writeFile12(join22(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
6066
+ await writeFile12(join25(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
5695
6067
  }
5696
6068
  async function loadPages(outputDir) {
5697
6069
  try {
5698
- const raw = await readFile17(join22(outputDir, PAGES_FILE), "utf-8");
6070
+ const raw = await readFile18(join25(outputDir, PAGES_FILE), "utf-8");
5699
6071
  const obj = JSON.parse(raw);
5700
6072
  return new Map(Object.entries(obj));
5701
6073
  } catch {
@@ -5732,12 +6104,14 @@ var STEP_INTROS = {
5732
6104
  kb: "Reading every page file to build a knowledge base (AUTONOMA.md). This gives the AI context about your features, flows, and UI patterns.",
5733
6105
  entityAudit: "Identifying every database model and tracing how each gets created \u2014 which service function, what side effects. This determines which entities need test data factories.",
5734
6106
  scenarioRecipe: "Designing test data scenarios with realistic values from your entity audit. The scenario defines exactly WHAT data will exist in the database during tests.",
5735
- recipeBuilder: "Guiding you through implementing Autonoma SDK factories for each entity. You'll implement each factory and we'll test them live (create + teardown) before moving on.",
6107
+ recipeBuilder: "Guiding you through implementing Autonoma SDK factories for each entity. For each one we give you a copy-paste guide to hand to Claude (or your AI assistant), which implements the factory in your codebase. Work locally: run your app on localhost and we'll test each factory live (create + teardown) against it. You deploy later, once everything passes.",
5736
6108
  testGenerator: "Generating exhaustive E2E test cases by exploring every page and feature. Each area gets test coverage proportional to its complexity."
5737
6109
  };
5738
6110
  async function runStep(step, outputDir, state, config, projectContext, nonInteractive) {
5739
6111
  const label = STEP_LABELS[step];
5740
6112
  p8.note(STEP_INTROS[step], `Step: ${label}`);
6113
+ const stepStartedAt = Date.now();
6114
+ track("cli_step_started", { step });
5741
6115
  state = await markStep(outputDir, state, step, "running");
5742
6116
  if (step !== "pagesFinder" && projectContext && !projectContext.pages) {
5743
6117
  const pages = await loadPages(outputDir);
@@ -5837,6 +6211,11 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
5837
6211
  const message = err instanceof Error ? err.message : String(err);
5838
6212
  p8.log.error(`Failed: ${label} \u2014 ${message}`);
5839
6213
  }
6214
+ track("cli_step_completed", {
6215
+ step,
6216
+ status: state.steps[step],
6217
+ duration_ms: Date.now() - stepStartedAt
6218
+ });
5840
6219
  return state;
5841
6220
  }
5842
6221
  async function showStatus(outputDir) {
@@ -5849,15 +6228,33 @@ async function showStatus(outputDir) {
5849
6228
  }
5850
6229
  }
5851
6230
  var BANNER = `
5852
- \x1B[36m\x1B[1m ___ _
5853
- / _ \\ | |
5854
- / /_\\ \\_ _| |_ ___ _ __ ___ _ __ ___ __ _
5855
- | _ | | | | __/ _ \\| '_ \\ / _ \\| '_ \` _ \\ / _\` |
5856
- | | | | |_| | || (_) | | | | (_) | | | | | | (_| |
5857
- \\_| |_/\\__,_|\\__\\___/|_| |_|\\___/|_| |_| |_|\\__,_|
6231
+ \x1B[36m\x1B[1m \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557
6232
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
6233
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
6234
+ \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551
6235
+ \u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
6236
+ \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
5858
6237
  \x1B[0m
5859
6238
  \x1B[2m E2E Test Planner \u2014 Generate exhaustive test suites from your codebase\x1B[0m
5860
6239
  `;
6240
+ async function ensureOpenRouterKey(nonInteractive) {
6241
+ if (process.env.OPENROUTER_API_KEY) return true;
6242
+ if (nonInteractive) {
6243
+ p8.log.error(
6244
+ "OPENROUTER_API_KEY is not set. Set it in your environment or run interactively once to save it."
6245
+ );
6246
+ return false;
6247
+ }
6248
+ p8.log.info("You'll need an OpenRouter API key to run the planner. Get one at https://openrouter.ai/keys");
6249
+ const key = await p8.password({
6250
+ message: "Paste your OpenRouter API key",
6251
+ validate: (value) => (value ?? "").trim().length === 0 ? "API key cannot be empty" : void 0
6252
+ });
6253
+ if (p8.isCancel(key)) return false;
6254
+ setGlobalEnv("OPENROUTER_API_KEY", key.trim());
6255
+ p8.log.success(`Saved your API key to ${getGlobalEnvPath()} \u2014 you won't be asked again.`);
6256
+ return true;
6257
+ }
5861
6258
  async function gatherProjectContext() {
5862
6259
  const description = await p8.text({
5863
6260
  message: "What is this project? (a short description so the agent knows what it's looking at)",
@@ -5905,18 +6302,34 @@ async function main() {
5905
6302
  }
5906
6303
  console.log(BANNER);
5907
6304
  p8.intro("Let's generate your test suite");
6305
+ const resumeCommand = `autonoma-planner --resume` + (args.project ? ` --project ${args.project}` : "");
6306
+ installInterruptHandler({
6307
+ onExit: () => {
6308
+ track("cli_run_exited");
6309
+ restoreTerminal();
6310
+ console.log("");
6311
+ p8.log.warn(`Your progress is saved. To resume, run:
6312
+ ${resumeCommand}`);
6313
+ void flushAnalytics().finally(() => process.exit(0));
6314
+ }
6315
+ });
5908
6316
  const config = loadConfig({
5909
6317
  project: args.project,
5910
6318
  model: args.model,
5911
6319
  slug: args.slug
5912
6320
  });
6321
+ const nonInteractive = !!args["non-interactive"];
6322
+ if (!await ensureOpenRouterKey(nonInteractive)) {
6323
+ p8.log.warn("Cancelled.");
6324
+ return;
6325
+ }
5913
6326
  const modelName = config.modelId ?? process.env.OPENROUTER_MODEL ?? DEFAULT_MODEL;
5914
6327
  if (!args.project) {
5915
6328
  p8.log.info(`No --project flag passed; using current working directory.`);
5916
6329
  }
5917
6330
  p8.log.info(`Project: ${config.projectRoot}`);
5918
6331
  p8.log.info(`Model: ${modelName}`);
5919
- const nonInteractive = !!args["non-interactive"];
6332
+ track("cli_run_started", { model: modelName, non_interactive: nonInteractive });
5920
6333
  const outputDir = await ensureOutputDir(config.projectSlug);
5921
6334
  let state = await loadState(outputDir);
5922
6335
  let isResuming = !!(args.resume || args.step);
@@ -5956,7 +6369,14 @@ async function main() {
5956
6369
  }
5957
6370
  await saveContext(outputDir, projectContext);
5958
6371
  }
5959
- p8.log.step(`Output: ${outputDir}`);
6372
+ p8.note(
6373
+ `${outputDir}
6374
+
6375
+ All generated files (knowledge base, scenarios, recipe, tests) live here.
6376
+ It's a hidden folder in your home directory \u2014 in Finder/Explorer use "Go to folder"
6377
+ or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
6378
+ "Output folder"
6379
+ );
5960
6380
  console.log("");
5961
6381
  p8.log.info(
5962
6382
  `Got it. I'll focus on: ${projectContext.criticalFlows}
@@ -5991,7 +6411,8 @@ async function main() {
5991
6411
  p8.log.error("Pipeline stopped due to failure.");
5992
6412
  break;
5993
6413
  }
5994
- if (i < steps.length - 1 && !nonInteractive) {
6414
+ const skipConfirmAfter = ["pagesFinder"];
6415
+ if (i < steps.length - 1 && !nonInteractive && !skipConfirmAfter.includes(step)) {
5995
6416
  const nextStep = steps[i + 1];
5996
6417
  const shouldContinue = await p8.confirm({
5997
6418
  message: `Continue to ${STEP_LABELS[nextStep]}?`
@@ -6009,10 +6430,13 @@ async function main() {
6009
6430
  }
6010
6431
  throw err;
6011
6432
  }
6433
+ const stepsDone = Object.values(state.steps).filter((s) => s === "done").length;
6434
+ track("cli_run_completed", { steps_done: stepsDone });
6012
6435
  p8.outro("Done");
6013
6436
  }
6014
- main().catch((err) => {
6437
+ main().then(() => flushAnalytics()).catch(async (err) => {
6015
6438
  console.error(err);
6439
+ await flushAnalytics();
6016
6440
  process.exit(1);
6017
6441
  });
6018
6442
  //# sourceMappingURL=index.js.map