@autonoma-ai/planner 0.1.25 → 0.1.26

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
@@ -9,11 +9,11 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // ../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.15.3_jiti@2.6.1_postcss@8.5.22_tsx@4.21.0_typescript@7.0.2_yaml@2.8.3/node_modules/tsup/assets/esm_shims.js
12
+ // ../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.15.3_jiti@2.6.1_postcss@8.5.25_tsx@4.21.0_typescript@7.0.2_yaml@2.8.3/node_modules/tsup/assets/esm_shims.js
13
13
  import path from "path";
14
14
  import { fileURLToPath } from "url";
15
15
  var init_esm_shims = __esm({
16
- "../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.15.3_jiti@2.6.1_postcss@8.5.22_tsx@4.21.0_typescript@7.0.2_yaml@2.8.3/node_modules/tsup/assets/esm_shims.js"() {
16
+ "../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.15.3_jiti@2.6.1_postcss@8.5.25_tsx@4.21.0_typescript@7.0.2_yaml@2.8.3/node_modules/tsup/assets/esm_shims.js"() {
17
17
  "use strict";
18
18
  }
19
19
  });
@@ -1019,6 +1019,11 @@ function budgetFor(step, sizes) {
1019
1019
  }
1020
1020
  function runningFraction(step, now, budget) {
1021
1021
  if (step.sub && step.sub.total > 0) {
1022
+ if (step.sub.done === 0 && step.sub.startedAt != null) {
1023
+ const elapsed = now - step.sub.startedAt;
1024
+ const unspendable = Math.min(budget, RUNNING_MIN_REMAINING_MS);
1025
+ return clamp01((budget - unspendable) * (elapsed / budget));
1026
+ }
1022
1027
  return clamp01(step.sub.done / step.sub.total);
1023
1028
  }
1024
1029
  if (step.startedAt != null) {
@@ -1028,8 +1033,9 @@ function runningFraction(step, now, budget) {
1028
1033
  }
1029
1034
  function liveRemainingMs(step, now) {
1030
1035
  if (step.sub == null || step.sub.total <= 0 || step.startedAt == null) return void 0;
1036
+ if (step.sub.done < LIVE_RATE_MIN_DONE) return void 0;
1031
1037
  const elapsed = now - step.startedAt;
1032
- if (step.sub.done < LIVE_RATE_MIN_DONE || elapsed < LIVE_RATE_MIN_ELAPSED_MS) return void 0;
1038
+ if (elapsed < LIVE_RATE_MIN_ELAPSED_MS) return void 0;
1033
1039
  return elapsed / step.sub.done * Math.max(0, step.sub.total - step.sub.done);
1034
1040
  }
1035
1041
  function paceRatio(state) {
@@ -1316,6 +1322,19 @@ var init_prompt = __esm({
1316
1322
  }
1317
1323
  });
1318
1324
 
1325
+ // src/ui/types.ts
1326
+ function isReviewStatus(status) {
1327
+ return REVIEW_STATUSES.has(status);
1328
+ }
1329
+ var REVIEW_STATUSES;
1330
+ var init_types = __esm({
1331
+ "src/ui/types.ts"() {
1332
+ "use strict";
1333
+ init_esm_shims();
1334
+ REVIEW_STATUSES = /* @__PURE__ */ new Set(["REVIEWING", "REVIEWED", "FIXING"]);
1335
+ }
1336
+ });
1337
+
1319
1338
  // src/ui/store.ts
1320
1339
  import { join as join5 } from "path";
1321
1340
  function initialSteps() {
@@ -1519,7 +1538,7 @@ function createStore(opts) {
1519
1538
  if (status === "done") {
1520
1539
  for (const id of state.steps[name].artifactIds) {
1521
1540
  const art = artifacts[id];
1522
- if (art) artifacts[id] = { ...art, status: "DONE" };
1541
+ if (art != null && !isReviewStatus(art.status)) artifacts[id] = { ...art, status: "DONE" };
1523
1542
  }
1524
1543
  }
1525
1544
  set({
@@ -1603,6 +1622,23 @@ function createStore(opts) {
1603
1622
  setLiveFile(relPath, text2, kind) {
1604
1623
  setLiveText(relPath, text2, kind);
1605
1624
  },
1625
+ setArtifactReview(path3, status) {
1626
+ const relPath = relOf(path3);
1627
+ const art = state.artifacts[relPath];
1628
+ if (art == null || art.status === status) return;
1629
+ set({ ...state, artifacts: { ...state.artifacts, [relPath]: { ...art, status } } });
1630
+ },
1631
+ settleReviews() {
1632
+ const artifacts = { ...state.artifacts };
1633
+ let changed = false;
1634
+ for (const [id, art] of Object.entries(artifacts)) {
1635
+ if (art.status !== "REVIEWING" && art.status !== "FIXING") continue;
1636
+ artifacts[id] = { ...art, status: "DONE" };
1637
+ changed = true;
1638
+ }
1639
+ if (!changed) return;
1640
+ set({ ...state, artifacts });
1641
+ },
1606
1642
  finish(outcome) {
1607
1643
  store.stopClock();
1608
1644
  set({
@@ -1709,11 +1745,11 @@ function createStore(opts) {
1709
1745
  if (state.countdown != null) set({ ...state, countdown: void 0 });
1710
1746
  resolve6?.();
1711
1747
  },
1712
- runWelcome({ title, lines, cta }) {
1748
+ runWelcome({ title, lines, cta, eyebrow }) {
1713
1749
  return new Promise((resolve6) => {
1714
1750
  welcomeResolve = resolve6;
1715
1751
  if (process.stdout.isTTY) process.stdout.write(BELL);
1716
- set({ ...state, welcome: { title, lines, cta } });
1752
+ set({ ...state, welcome: { title, lines, cta, eyebrow } });
1717
1753
  });
1718
1754
  },
1719
1755
  dismissWelcome() {
@@ -1806,6 +1842,7 @@ var init_store = __esm({
1806
1842
  init_nav();
1807
1843
  init_prompt();
1808
1844
  init_steps();
1845
+ init_types();
1809
1846
  LOG_CAP = 500;
1810
1847
  ACTIVITY_CAP = 200;
1811
1848
  EMIT_COALESCE_MS = 16;
@@ -4702,6 +4739,22 @@ var init_app_links = __esm({
4702
4739
  }
4703
4740
  });
4704
4741
 
4742
+ // ../../packages/types/src/agent-handoff-links.ts
4743
+ var init_agent_handoff_links = __esm({
4744
+ "../../packages/types/src/agent-handoff-links.ts"() {
4745
+ "use strict";
4746
+ init_esm_shims();
4747
+ }
4748
+ });
4749
+
4750
+ // ../../packages/types/src/sdk-error-signals.ts
4751
+ var init_sdk_error_signals = __esm({
4752
+ "../../packages/types/src/sdk-error-signals.ts"() {
4753
+ "use strict";
4754
+ init_esm_shims();
4755
+ }
4756
+ });
4757
+
4705
4758
  // ../../packages/types/src/designated-run.ts
4706
4759
  var init_designated_run = __esm({
4707
4760
  "../../packages/types/src/designated-run.ts"() {
@@ -4718,6 +4771,14 @@ var init_deployment_signal_template = __esm({
4718
4771
  }
4719
4772
  });
4720
4773
 
4774
+ // ../../packages/types/src/constants/analytics-headers.ts
4775
+ var init_analytics_headers = __esm({
4776
+ "../../packages/types/src/constants/analytics-headers.ts"() {
4777
+ "use strict";
4778
+ init_esm_shims();
4779
+ }
4780
+ });
4781
+
4721
4782
  // ../../packages/types/src/constants/pipeline-labels.ts
4722
4783
  var init_pipeline_labels = __esm({
4723
4784
  "../../packages/types/src/constants/pipeline-labels.ts"() {
@@ -4731,6 +4792,7 @@ var init_constants = __esm({
4731
4792
  "../../packages/types/src/constants/index.ts"() {
4732
4793
  "use strict";
4733
4794
  init_esm_shims();
4795
+ init_analytics_headers();
4734
4796
  init_billing();
4735
4797
  init_pipeline_labels();
4736
4798
  }
@@ -4790,6 +4852,8 @@ var init_src = __esm({
4790
4852
  init_sensitive_detection();
4791
4853
  init_preview_url();
4792
4854
  init_app_links();
4855
+ init_agent_handoff_links();
4856
+ init_sdk_error_signals();
4793
4857
  init_designated_run();
4794
4858
  init_deployment_signal_template();
4795
4859
  init_constants();
@@ -5561,6 +5625,98 @@ var init_autonoma_client = __esm({
5561
5625
  }
5562
5626
  });
5563
5627
 
5628
+ // src/core/output.ts
5629
+ import { mkdir, rm, writeFile as writeFile2 } from "fs/promises";
5630
+ import { homedir as homedir3 } from "os";
5631
+ import { join as join10 } from "path";
5632
+ function getOutputDir(projectSlug) {
5633
+ return join10(AUTONOMA_HOME3, projectSlug);
5634
+ }
5635
+ async function clearOutputDir(projectSlug) {
5636
+ const dir = getOutputDir(projectSlug);
5637
+ await rm(dir, { recursive: true, force: true });
5638
+ await mkdir(dir, { recursive: true });
5639
+ debugLog("Cleared the run directory for a fresh start", { dir });
5640
+ }
5641
+ function displayPath(absPath) {
5642
+ const home = homedir3();
5643
+ return absPath.startsWith(home) ? `~${absPath.slice(home.length)}` : absPath;
5644
+ }
5645
+ async function ensureOutputDir(projectSlug) {
5646
+ const dir = getOutputDir(projectSlug);
5647
+ await mkdir(dir, { recursive: true });
5648
+ const probe = join10(dir, WRITE_PROBE_FILE);
5649
+ try {
5650
+ await writeFile2(probe, "ok", "utf-8");
5651
+ } catch (err) {
5652
+ const reason = err instanceof Error ? err.message : String(err);
5653
+ throw new Error(
5654
+ `Cannot write to ${displayPath(dir)} - the planner keeps every artifact it generates there.
5655
+ ${reason}
5656
+ Free up disk space or fix the directory's permissions, then re-run.`
5657
+ );
5658
+ }
5659
+ await rm(probe, { force: true }).catch((err) => {
5660
+ debugLog("Could not remove the write probe", { probe, err });
5661
+ });
5662
+ return dir;
5663
+ }
5664
+ var AUTONOMA_HOME3, WRITE_PROBE_FILE;
5665
+ var init_output = __esm({
5666
+ "src/core/output.ts"() {
5667
+ "use strict";
5668
+ init_esm_shims();
5669
+ init_debug();
5670
+ AUTONOMA_HOME3 = join10(homedir3(), ".autonoma");
5671
+ WRITE_PROBE_FILE = ".write-probe";
5672
+ }
5673
+ });
5674
+
5675
+ // src/core/preferences.ts
5676
+ import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
5677
+ import { join as join11 } from "path";
5678
+ import { z as z36 } from "zod";
5679
+ async function readPreferences() {
5680
+ try {
5681
+ const raw = await readFile4(PREFERENCES_FILE, "utf-8");
5682
+ const parsed = PreferencesSchema.safeParse(JSON.parse(raw));
5683
+ if (!parsed.success) {
5684
+ debugLog("Ignoring unreadable preferences file", { issues: parsed.error.issues });
5685
+ return {};
5686
+ }
5687
+ return parsed.data;
5688
+ } catch (err) {
5689
+ debugLog("No preferences to read", { err });
5690
+ return {};
5691
+ }
5692
+ }
5693
+ async function updatePreferences(update) {
5694
+ try {
5695
+ const current = await readPreferences();
5696
+ const next = { ...current, ...update };
5697
+ await mkdir2(AUTONOMA_HOME3, { recursive: true });
5698
+ await writeFile3(PREFERENCES_FILE, `${JSON.stringify(next, null, 2)}
5699
+ `, "utf-8");
5700
+ debugLog("Saved preferences", { update });
5701
+ } catch (err) {
5702
+ debugLog("Could not save preferences", { err });
5703
+ }
5704
+ }
5705
+ var PREFERENCES_FILE, PreferencesSchema;
5706
+ var init_preferences = __esm({
5707
+ "src/core/preferences.ts"() {
5708
+ "use strict";
5709
+ init_esm_shims();
5710
+ init_debug();
5711
+ init_output();
5712
+ PREFERENCES_FILE = join11(AUTONOMA_HOME3, "preferences.json");
5713
+ PreferencesSchema = z36.object({
5714
+ /** The coding agent id (`claude`, `codex`) last chosen from the picker. */
5715
+ agentId: z36.string().optional()
5716
+ });
5717
+ }
5718
+ });
5719
+
5564
5720
  // src/core/coding-agent.ts
5565
5721
  var coding_agent_exports = {};
5566
5722
  __export(coding_agent_exports, {
@@ -5588,10 +5744,14 @@ async function selectLauncher(launchers, presetId, interactive = true) {
5588
5744
  const availability = await Promise.all(launchers.map((l) => l.isAvailable()));
5589
5745
  const available = launchers.filter((_, i) => availability[i]);
5590
5746
  debugLog("Detected available agents", { available: available.map((l) => l.id), presetId, interactive });
5591
- if (presetId != null) {
5592
- const preset = available.find((l) => l.id === presetId);
5593
- if (preset != null) return preset;
5594
- log.warn(`Requested agent "${presetId}" is not installed or not supported.`);
5747
+ const remembered = presetId ?? (await readPreferences()).agentId;
5748
+ if (remembered != null) {
5749
+ const preset = available.find((l) => l.id === remembered);
5750
+ if (preset != null) {
5751
+ if (presetId == null) log.info(`Using ${preset.label} - the agent you picked last time.`);
5752
+ return preset;
5753
+ }
5754
+ if (presetId != null) log.warn(`Requested agent "${presetId}" is not installed or not supported.`);
5595
5755
  }
5596
5756
  if (available.length === 0) return void 0;
5597
5757
  if (available.length === 1) {
@@ -5611,6 +5771,7 @@ async function selectLauncher(launchers, presetId, interactive = true) {
5611
5771
  options: available.map((l) => ({ value: l.id, label: l.label }))
5612
5772
  });
5613
5773
  if (isCancel(selected)) throw new Error("Agent selection cancelled");
5774
+ await updatePreferences({ agentId: selected });
5614
5775
  return available.find((l) => l.id === selected);
5615
5776
  }
5616
5777
  async function selectPermissionMode(preset) {
@@ -5641,6 +5802,7 @@ var init_coding_agent = __esm({
5641
5802
  init_esm_shims();
5642
5803
  init_prompts();
5643
5804
  init_debug();
5805
+ init_preferences();
5644
5806
  MCP_COMMAND_TIMEOUT_MS = 6e4;
5645
5807
  SPAWNED_BY_PLANNER_ENV = "AUTONOMA_PLANNER_SPAWNED_AGENT";
5646
5808
  CODEX_BEARER_TOKEN_ENV = "AUTONOMA_MCP_TOKEN";
@@ -6385,16 +6547,16 @@ var init_notify = __esm({
6385
6547
  });
6386
6548
 
6387
6549
  // src/core/project-map.ts
6388
- import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
6389
- import { join as join12 } from "path";
6390
- import { z as z36 } from "zod";
6550
+ import { readFile as readFile6, writeFile as writeFile5 } from "fs/promises";
6551
+ import { join as join13 } from "path";
6552
+ import { z as z37 } from "zod";
6391
6553
  async function saveProjectMap(outputDir, map) {
6392
- await writeFile4(join12(outputDir, PROJECT_MAP_FILE), JSON.stringify(map, null, 2), "utf-8");
6554
+ await writeFile5(join13(outputDir, PROJECT_MAP_FILE), JSON.stringify(map, null, 2), "utf-8");
6393
6555
  }
6394
6556
  async function loadProjectMap(outputDir) {
6395
- const path3 = join12(outputDir, PROJECT_MAP_FILE);
6557
+ const path3 = join13(outputDir, PROJECT_MAP_FILE);
6396
6558
  try {
6397
- const raw = await readFile5(path3, "utf-8");
6559
+ const raw = await readFile6(path3, "utf-8");
6398
6560
  const parsed = ProjectMapSchema.safeParse(JSON.parse(raw));
6399
6561
  if (parsed.success) return parsed.data;
6400
6562
  debugLog("project-map.json failed schema validation, ignoring it", { path: path3, issues: parsed.error.issues });
@@ -6496,34 +6658,34 @@ var init_project_map = __esm({
6496
6658
  "use strict";
6497
6659
  init_esm_shims();
6498
6660
  init_debug();
6499
- FrontendEntry = z36.object({
6500
- path: z36.string().min(1).describe("Repo-relative path to the frontend app/directory (the UI surface)."),
6501
- framework: z36.string().describe("Detected UI framework/stack, or 'unknown' if not obvious (e.g. next, react, vue, svelte)."),
6502
- dependsOn: z36.array(z36.string()).describe(
6661
+ FrontendEntry = z37.object({
6662
+ path: z37.string().min(1).describe("Repo-relative path to the frontend app/directory (the UI surface)."),
6663
+ framework: z37.string().describe("Detected UI framework/stack, or 'unknown' if not obvious (e.g. next, react, vue, svelte)."),
6664
+ dependsOn: z37.array(z37.string()).describe(
6503
6665
  "Repo-relative paths (each matching a backend in this map) that THIS frontend needs in order to function - the API/service(s) it calls and the data layer(s) that own the records it renders. These are pre-selected when the user picks this frontend, so only list backends this frontend actually depends on. Empty if it needs none."
6504
6666
  ),
6505
- why: z36.string().min(1).describe("One line: the evidence that made you classify this as a frontend.")
6506
- });
6507
- BackendEntry = z36.object({
6508
- path: z36.string().min(1).describe("Repo-relative path to the backend/API/service or the package that owns the data layer."),
6509
- language: z36.string().describe("Primary language, or 'unknown' (e.g. typescript, python, go, rust)."),
6510
- framework: z36.string().describe("Web/service framework or ORM stack, or 'unknown' (e.g. express, hono, fastapi, rails)."),
6511
- dataLayer: z36.object({
6512
- kind: z36.string().describe("How models are defined (e.g. prisma, drizzle, sqlalchemy, typeorm, raw-sql, unknown)."),
6513
- schemaPath: z36.string().optional().describe("Repo-relative path to the schema/models definition, if found.")
6667
+ why: z37.string().min(1).describe("One line: the evidence that made you classify this as a frontend.")
6668
+ });
6669
+ BackendEntry = z37.object({
6670
+ path: z37.string().min(1).describe("Repo-relative path to the backend/API/service or the package that owns the data layer."),
6671
+ language: z37.string().describe("Primary language, or 'unknown' (e.g. typescript, python, go, rust)."),
6672
+ framework: z37.string().describe("Web/service framework or ORM stack, or 'unknown' (e.g. express, hono, fastapi, rails)."),
6673
+ dataLayer: z37.object({
6674
+ kind: z37.string().describe("How models are defined (e.g. prisma, drizzle, sqlalchemy, typeorm, raw-sql, unknown)."),
6675
+ schemaPath: z37.string().optional().describe("Repo-relative path to the schema/models definition, if found.")
6514
6676
  }).optional().describe("Where this backend's database models live. Omit if this backend owns no data layer."),
6515
- why: z36.string().min(1).describe("One line: the evidence that made you classify this as a backend.")
6677
+ why: z37.string().min(1).describe("One line: the evidence that made you classify this as a backend.")
6516
6678
  });
6517
- IgnoreEntry = z36.object({
6518
- path: z36.string().min(1).describe("Repo-relative path to a directory that is NOT relevant to testing this app."),
6519
- why: z36.string().min(1).describe("One line: why it is irrelevant (e.g. docs site, infra, tooling, examples, unrelated app).")
6679
+ IgnoreEntry = z37.object({
6680
+ path: z37.string().min(1).describe("Repo-relative path to a directory that is NOT relevant to testing this app."),
6681
+ why: z37.string().min(1).describe("One line: why it is irrelevant (e.g. docs site, infra, tooling, examples, unrelated app).")
6520
6682
  });
6521
- ProjectMapSchema = z36.object({
6522
- frontends: z36.array(FrontendEntry).describe("The UI surface(s) whose pages/flows get tested. Usually 1; may be more."),
6523
- backends: z36.array(BackendEntry).describe(
6683
+ ProjectMapSchema = z37.object({
6684
+ frontends: z37.array(FrontendEntry).describe("The UI surface(s) whose pages/flows get tested. Usually 1; may be more."),
6685
+ backends: z37.array(BackendEntry).describe(
6524
6686
  "The API/service(s) and data layer(s) that own the models we seed. May be 0 (frontend-only), 1, or many."
6525
6687
  ),
6526
- ignore: z36.array(IgnoreEntry).describe("Directories judged irrelevant to this app's tests, so later steps skip them.")
6688
+ ignore: z37.array(IgnoreEntry).describe("Directories judged irrelevant to this app's tests, so later steps skip them.")
6527
6689
  });
6528
6690
  PROJECT_MAP_FILE = "project-map.json";
6529
6691
  }
@@ -6534,11 +6696,11 @@ var parse_entity_audit_exports = {};
6534
6696
  __export(parse_entity_audit_exports, {
6535
6697
  parseEntityNames: () => parseEntityNames
6536
6698
  });
6537
- import { readFile as readFile7 } from "fs/promises";
6538
- import { join as join14 } from "path";
6699
+ import { readFile as readFile8 } from "fs/promises";
6700
+ import { join as join15 } from "path";
6539
6701
  async function parseEntityNames(outputDir) {
6540
6702
  try {
6541
- const content = await readFile7(join14(outputDir, "entity-audit.md"), "utf-8");
6703
+ const content = await readFile8(join15(outputDir, "entity-audit.md"), "utf-8");
6542
6704
  const names = [];
6543
6705
  for (const line of content.split("\n")) {
6544
6706
  const match = line.match(/^\s+-\s+name:\s+(.+)$/);
@@ -6573,7 +6735,7 @@ var init_count_generated_tests = __esm({
6573
6735
  });
6574
6736
 
6575
6737
  // src/core/display.ts
6576
- import { join as join15 } from "path";
6738
+ import { join as join16 } from "path";
6577
6739
  function formatArgs(input, keys) {
6578
6740
  const parts = [];
6579
6741
  for (const key of keys) {
@@ -6665,7 +6827,7 @@ function logToStore(store, agentId, info, stats) {
6665
6827
  const folder = String(tc.input.folder ?? "");
6666
6828
  const filename = String(tc.input.filename ?? "");
6667
6829
  if (folder && filename) {
6668
- store.noteWrite(join15(TESTS_DIR, folder, normalizeTestFilename(filename)));
6830
+ store.noteWrite(join16(TESTS_DIR, folder, normalizeTestFilename(filename)));
6669
6831
  }
6670
6832
  }
6671
6833
  if (tc.name === "bash") {
@@ -6917,7 +7079,7 @@ async function runAgent(config, prompt, extractResult) {
6917
7079
  agent: config.id,
6918
7080
  nudge: nudges
6919
7081
  });
6920
- messages.push(...generation.response.messages);
7082
+ messages.push(...generation.responseMessages);
6921
7083
  messages.push({ role: "user", content: NUDGE_PROMPT });
6922
7084
  generation = await agent.generate({ messages, ...callOptions });
6923
7085
  }
@@ -6973,8 +7135,8 @@ var init_agent = __esm({
6973
7135
  });
6974
7136
 
6975
7137
  // src/core/gitignore.ts
6976
- import { readFile as readFile8 } from "fs/promises";
6977
- import { join as join16, relative as relative2 } from "path";
7138
+ import { readFile as readFile9 } from "fs/promises";
7139
+ import { join as join17, relative as relative2 } from "path";
6978
7140
  import { glob as glob2 } from "glob";
6979
7141
  async function loadGitignorePatterns(projectRoot) {
6980
7142
  const patterns = [
@@ -6994,10 +7156,10 @@ async function loadGitignorePatterns(projectRoot) {
6994
7156
  ];
6995
7157
  const matches = await glob2("**/.gitignore", { cwd: projectRoot, dot: true });
6996
7158
  for (const match of matches) {
6997
- const fullPath = join16(projectRoot, match);
7159
+ const fullPath = join17(projectRoot, match);
6998
7160
  try {
6999
- const content = await readFile8(fullPath, "utf-8");
7000
- const prefix = relative2(projectRoot, join16(projectRoot, match, ".."));
7161
+ const content = await readFile9(fullPath, "utf-8");
7162
+ const prefix = relative2(projectRoot, join17(projectRoot, match, ".."));
7001
7163
  const parsed = parseGitignore(content, prefix);
7002
7164
  patterns.push(...parsed);
7003
7165
  } catch (err) {
@@ -7060,7 +7222,7 @@ var init_exec_error = __esm({
7060
7222
  import { execFile as execFile3 } from "child_process";
7061
7223
  import { promisify as promisify2 } from "util";
7062
7224
  import { tool } from "ai";
7063
- import { z as z37 } from "zod";
7225
+ import { z as z38 } from "zod";
7064
7226
  function validateCommand(command, allowed) {
7065
7227
  const trimmed = command.trim();
7066
7228
  if (trimmed.length === 0) return "Empty command";
@@ -7126,8 +7288,8 @@ var init_bash = __esm({
7126
7288
  DEFAULT_ALLOWED = /* @__PURE__ */ new Set(["git", "wc", "sort", "head", "tail", "cat", "ls", "find", "diff", "echo"]);
7127
7289
  TIMEOUT_MS = 3e4;
7128
7290
  MAX_OUTPUT_BYTES = 1024 * 512;
7129
- inputSchema = z37.object({
7130
- command: z37.string().describe("Shell command to execute")
7291
+ inputSchema = z38.object({
7292
+ command: z38.string().describe("Shell command to execute")
7131
7293
  });
7132
7294
  }
7133
7295
  });
@@ -7135,7 +7297,7 @@ var init_bash = __esm({
7135
7297
  // src/tools/glob.ts
7136
7298
  import { tool as tool2 } from "ai";
7137
7299
  import { glob as glob3 } from "glob";
7138
- import { z as z38 } from "zod";
7300
+ import { z as z39 } from "zod";
7139
7301
  async function executeGlob(pattern, cwd, ignorePatterns = DEFAULT_IGNORE) {
7140
7302
  try {
7141
7303
  const matches = await glob3(pattern, {
@@ -7169,9 +7331,9 @@ var init_glob = __esm({
7169
7331
  "src/tools/glob.ts"() {
7170
7332
  "use strict";
7171
7333
  init_esm_shims();
7172
- inputSchema2 = z38.object({
7173
- pattern: z38.string().describe("Glob pattern to match files (e.g. '**/*.ts', 'src/**/*.py')"),
7174
- cwd: z38.string().optional().describe("Directory to search in. Defaults to working directory.")
7334
+ inputSchema2 = z39.object({
7335
+ pattern: z39.string().describe("Glob pattern to match files (e.g. '**/*.ts', 'src/**/*.py')"),
7336
+ cwd: z39.string().optional().describe("Directory to search in. Defaults to working directory.")
7175
7337
  });
7176
7338
  DEFAULT_IGNORE = [
7177
7339
  "**/node_modules/**",
@@ -7189,7 +7351,7 @@ var init_glob = __esm({
7189
7351
  import { execFile as execFile4 } from "child_process";
7190
7352
  import { promisify as promisify3 } from "util";
7191
7353
  import { tool as tool3 } from "ai";
7192
- import { z as z39 } from "zod";
7354
+ import { z as z40 } from "zod";
7193
7355
  function buildGrepTool(workingDirectory) {
7194
7356
  return tool3({
7195
7357
  description: "Search file contents with ripgrep. Returns matching lines with file paths and line numbers.",
@@ -7233,10 +7395,10 @@ var init_grep = __esm({
7233
7395
  init_esm_shims();
7234
7396
  init_exec_error();
7235
7397
  execFileAsync3 = promisify3(execFile4);
7236
- inputSchema3 = z39.object({
7237
- pattern: z39.string().describe("Regex pattern to search for in file contents"),
7238
- glob: z39.string().optional().describe("Glob to filter files (e.g. '*.ts')"),
7239
- path: z39.string().optional().describe("File or directory to search in")
7398
+ inputSchema3 = z40.object({
7399
+ pattern: z40.string().describe("Regex pattern to search for in file contents"),
7400
+ glob: z40.string().optional().describe("Glob to filter files (e.g. '*.ts')"),
7401
+ path: z40.string().optional().describe("File or directory to search in")
7240
7402
  });
7241
7403
  }
7242
7404
  });
@@ -7244,10 +7406,10 @@ var init_grep = __esm({
7244
7406
  // src/tools/list-directory.ts
7245
7407
  import { readdir } from "fs/promises";
7246
7408
  import { stat } from "fs/promises";
7247
- import { join as join17, relative as relative3 } from "path";
7409
+ import { join as join18, relative as relative3 } from "path";
7248
7410
  import { tool as tool4 } from "ai";
7249
7411
  import { minimatch } from "minimatch";
7250
- import { z as z40 } from "zod";
7412
+ import { z as z41 } from "zod";
7251
7413
  function buildMatcher(patterns) {
7252
7414
  const positive = patterns.filter((p) => !p.startsWith("!"));
7253
7415
  const negative = patterns.filter((p) => p.startsWith("!")).map((p) => p.slice(1));
@@ -7269,7 +7431,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
7269
7431
  const withTypes = [];
7270
7432
  for (const name of rawEntries) {
7271
7433
  try {
7272
- const s = await stat(join17(dirPath, name));
7434
+ const s = await stat(join18(dirPath, name));
7273
7435
  withTypes.push({ name, isDir: s.isDirectory() });
7274
7436
  } catch {
7275
7437
  withTypes.push({ name, isDir: false });
@@ -7289,7 +7451,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
7289
7451
  }
7290
7452
  if (entry.isDir) {
7291
7453
  const children = await buildTree(
7292
- join17(dirPath, entry.name),
7454
+ join18(dirPath, entry.name),
7293
7455
  maxDepth,
7294
7456
  currentDepth + 1,
7295
7457
  isIgnored,
@@ -7327,10 +7489,10 @@ async function buildListDirectoryTool(workingDirectory) {
7327
7489
  const isIgnored = buildMatcher(patterns);
7328
7490
  return tool4({
7329
7491
  description: "List directory structure as a tree. Use this for an overview of the project layout. Start at the root (path='.') with depth 3, then increase depth or narrow path if needed. Do NOT call this on every subdirectory - use glob to find specific files instead. Returns cached result if the same path+depth was already requested.",
7330
- inputSchema: z40.object({
7331
- path: z40.string().default(".").describe("Directory path relative to project root. Defaults to root."),
7332
- depth: z40.number().min(1).max(15).default(10).describe("Max depth to traverse (1-15). Default 10."),
7333
- gitignore: z40.boolean().describe(
7492
+ inputSchema: z41.object({
7493
+ path: z41.string().default(".").describe("Directory path relative to project root. Defaults to root."),
7494
+ depth: z41.number().min(1).max(15).default(10).describe("Max depth to traverse (1-15). Default 10."),
7495
+ gitignore: z41.boolean().describe(
7334
7496
  "Whether to respect the gitignore or to ignore it. true will respect it. false will ignore it. Default true"
7335
7497
  ).default(true)
7336
7498
  }),
@@ -7342,7 +7504,7 @@ async function buildListDirectoryTool(workingDirectory) {
7342
7504
  };
7343
7505
  }
7344
7506
  seen.add(cacheKey);
7345
- const targetDir = input.path === "." ? workingDirectory : join17(workingDirectory, input.path);
7507
+ const targetDir = input.path === "." ? workingDirectory : join18(workingDirectory, input.path);
7346
7508
  try {
7347
7509
  const s = await stat(targetDir);
7348
7510
  if (!s.isDirectory()) {
@@ -7378,10 +7540,10 @@ var init_list_directory = __esm({
7378
7540
  });
7379
7541
 
7380
7542
  // src/tools/read-file.ts
7381
- import { readFile as readFile9 } from "fs/promises";
7543
+ import { readFile as readFile10 } from "fs/promises";
7382
7544
  import { relative as relative4, resolve as resolve2 } from "path";
7383
7545
  import { tool as tool5 } from "ai";
7384
- import { z as z41 } from "zod";
7546
+ import { z as z42 } from "zod";
7385
7547
  function resolveSandboxedPath(workingDirectory, filePath) {
7386
7548
  const absolutePath = resolve2(workingDirectory, filePath);
7387
7549
  const relativePath = relative4(workingDirectory, absolutePath);
@@ -7406,7 +7568,7 @@ async function executeReadFile(workingDirectory, filePath, offset, limit) {
7406
7568
  const resolved = resolveSandboxedPath(workingDirectory, filePath);
7407
7569
  if ("error" in resolved) return resolved;
7408
7570
  try {
7409
- const content = await readFile9(resolved.absolutePath, "utf-8");
7571
+ const content = await readFile10(resolved.absolutePath, "utf-8");
7410
7572
  const sliced = sliceLines(content, offset ?? 0, limit ?? MAX_LINES);
7411
7573
  if (sliced.numbered.length > MAX_OUTPUT_BYTES2) {
7412
7574
  sliced.numbered = sliced.numbered.slice(0, MAX_OUTPUT_BYTES2) + `
@@ -7439,10 +7601,10 @@ var init_read_file = __esm({
7439
7601
  init_esm_shims();
7440
7602
  MAX_LINES = 2e3;
7441
7603
  MAX_OUTPUT_BYTES2 = 256 * 1024;
7442
- inputSchema4 = z41.object({
7443
- filePath: z41.string().describe("Path to the file (absolute or relative to working directory)"),
7444
- offset: z41.number().int().min(0).optional().describe("Line number to start reading from (0-based)"),
7445
- limit: z41.number().int().min(1).optional().describe("Maximum number of lines to read")
7604
+ inputSchema4 = z42.object({
7605
+ filePath: z42.string().describe("Path to the file (absolute or relative to working directory)"),
7606
+ offset: z42.number().int().min(0).optional().describe("Line number to start reading from (0-based)"),
7607
+ limit: z42.number().int().min(1).optional().describe("Maximum number of lines to read")
7446
7608
  });
7447
7609
  }
7448
7610
  });
@@ -7465,10 +7627,10 @@ var init_pick_string = __esm({
7465
7627
 
7466
7628
  // src/tools/subagent.ts
7467
7629
  import { ToolLoopAgent as ToolLoopAgent2, hasToolCall as hasToolCall2, stepCountIs as stepCountIs2, tool as tool6 } from "ai";
7468
- import { z as z42 } from "zod";
7630
+ import { z as z43 } from "zod";
7469
7631
  function buildSubagentTools(workingDirectory, onFileRead) {
7470
7632
  const baseReadFile = buildReadFileTool(workingDirectory);
7471
- const readFile25 = onFileRead ? tool6({
7633
+ const readFile26 = onFileRead ? tool6({
7472
7634
  description: baseReadFile.description,
7473
7635
  inputSchema: baseReadFile.inputSchema,
7474
7636
  execute: async (input, options) => {
@@ -7481,7 +7643,7 @@ function buildSubagentTools(workingDirectory, onFileRead) {
7481
7643
  bash: buildBashTool(workingDirectory),
7482
7644
  glob: buildGlobTool(workingDirectory),
7483
7645
  grep: buildGrepTool(workingDirectory),
7484
- read_file: readFile25
7646
+ read_file: readFile26
7485
7647
  };
7486
7648
  }
7487
7649
  function buildSubagentTool(model, workingDirectory, onHeartbeat, onFileRead) {
@@ -7538,11 +7700,11 @@ var init_subagent = __esm({
7538
7700
  init_glob();
7539
7701
  init_grep();
7540
7702
  init_read_file();
7541
- inputSchema5 = z42.object({
7542
- instruction: z42.string().describe("Focused task for the subagent. Be specific about files and patterns to investigate.")
7703
+ inputSchema5 = z43.object({
7704
+ instruction: z43.string().describe("Focused task for the subagent. Be specific about files and patterns to investigate.")
7543
7705
  });
7544
- resultSchema = z42.object({
7545
- findings: z42.string().describe("Summary of what was found")
7706
+ resultSchema = z43.object({
7707
+ findings: z43.string().describe("Summary of what was found")
7546
7708
  });
7547
7709
  SYSTEM_PROMPT = `You are a code research assistant. You have tools to explore a codebase: bash (shell commands, mainly git), glob (find files), grep (search content), and read_file (read files).
7548
7710
 
@@ -7553,10 +7715,10 @@ Be thorough but focused - only investigate what's relevant to your instruction.`
7553
7715
  });
7554
7716
 
7555
7717
  // src/tools/write-file.ts
7556
- import { writeFile as writeFile5, mkdir as mkdir2 } from "fs/promises";
7718
+ import { writeFile as writeFile6, mkdir as mkdir3 } from "fs/promises";
7557
7719
  import { dirname as dirname2, relative as relative5, resolve as resolve3 } from "path";
7558
7720
  import { tool as tool7 } from "ai";
7559
- import { z as z43 } from "zod";
7721
+ import { z as z44 } from "zod";
7560
7722
  async function executeWriteFile(outputDirectory, filePath, content) {
7561
7723
  const cleaned = filePath.replace(/^autonoma\//, "");
7562
7724
  const absolutePath = resolve3(outputDirectory, cleaned);
@@ -7565,8 +7727,8 @@ async function executeWriteFile(outputDirectory, filePath, content) {
7565
7727
  return { error: "Cannot write files outside the output directory" };
7566
7728
  }
7567
7729
  try {
7568
- await mkdir2(dirname2(absolutePath), { recursive: true });
7569
- await writeFile5(absolutePath, content, "utf-8");
7730
+ await mkdir3(dirname2(absolutePath), { recursive: true });
7731
+ await writeFile6(absolutePath, content, "utf-8");
7570
7732
  return { path: relativePath, bytesWritten: content.length };
7571
7733
  } catch (err) {
7572
7734
  const message = err instanceof Error ? err.message : String(err);
@@ -7585,21 +7747,21 @@ var init_write_file = __esm({
7585
7747
  "src/tools/write-file.ts"() {
7586
7748
  "use strict";
7587
7749
  init_esm_shims();
7588
- inputSchema6 = z43.object({
7589
- filePath: z43.string().describe("Path to write (absolute or relative to output directory)"),
7590
- content: z43.string().describe("File content to write")
7750
+ inputSchema6 = z44.object({
7751
+ filePath: z44.string().describe("Path to write (absolute or relative to output directory)"),
7752
+ content: z44.string().describe("File content to write")
7591
7753
  });
7592
7754
  }
7593
7755
  });
7594
7756
 
7595
7757
  // src/tools/ask-user.ts
7596
7758
  import { tool as tool8 } from "ai";
7597
- import { z as z44 } from "zod";
7759
+ import { z as z45 } from "zod";
7598
7760
  function buildAskUserTool() {
7599
7761
  return tool8({
7600
7762
  description: "Ask the user a question ONLY when the answer is truly unknowable from the codebase. Valid reasons: untyped JSON/JSONB field schemas, business rules not in code, config values not in source. NEVER ask about: field names (read the schema), field types (read the ORM model), enum values (read the code), relationships (read foreign keys), numeric values (read the seed data or defaults). If you can find it by reading a file, DO NOT ask - read the file instead.",
7601
- inputSchema: z44.object({
7602
- question: z44.string().describe(
7763
+ inputSchema: z45.object({
7764
+ question: z45.string().describe(
7603
7765
  "A clear, plain-language question. State exactly what you need to know and why you can't find it in code. BAD: 'What are the decimal values for checking_balance?' - GOOD: 'Your Account model has a metadata JSON column with no type definition. What fields go inside it?'"
7604
7766
  )
7605
7767
  }),
@@ -7659,7 +7821,7 @@ var init_tools = __esm({
7659
7821
 
7660
7822
  // src/agents/00-project-mapper/coverage.ts
7661
7823
  import { readdir as readdir2 } from "fs/promises";
7662
- import { join as join18 } from "path";
7824
+ import { join as join19 } from "path";
7663
7825
  async function findUncoveredDirs(projectRoot, map) {
7664
7826
  const entries = [...map.frontends, ...map.backends, ...map.ignore].map(
7665
7827
  (e) => e.path.replace(/^\.\//, "").replace(/\/+$/, "")
@@ -7683,7 +7845,7 @@ async function findUncoveredDirs(projectRoot, map) {
7683
7845
  const childRel = rel === "" ? child.name : `${rel}/${child.name}`;
7684
7846
  if (underEntry(childRel)) continue;
7685
7847
  if (hasEntryWithin(childRel)) {
7686
- if (depth + 1 < MAX_DEPTH) await walk(join18(abs, child.name), childRel, depth + 1);
7848
+ if (depth + 1 < MAX_DEPTH) await walk(join19(abs, child.name), childRel, depth + 1);
7687
7849
  } else {
7688
7850
  out.push(childRel);
7689
7851
  }
@@ -7710,7 +7872,7 @@ __export(project_mapper_exports, {
7710
7872
  runProjectMapper: () => runProjectMapper
7711
7873
  });
7712
7874
  import { tool as tool9 } from "ai";
7713
- import { z as z45 } from "zod";
7875
+ import { z as z46 } from "zod";
7714
7876
  async function runProjectMapper(input) {
7715
7877
  const model = getModel(input.modelId);
7716
7878
  const { logger, onStepFinish } = buildDefaultStepLogger("project-map", MAX_STEPS);
@@ -7744,7 +7906,7 @@ First build the full inventory (workspace/package manifests + top-level director
7744
7906
  }),
7745
7907
  finish: tool9({
7746
7908
  description: "End this step. Call once set_project_map has recorded a complete map.",
7747
- inputSchema: z45.object({}),
7909
+ inputSchema: z46.object({}),
7748
7910
  execute: () => {
7749
7911
  if (captured == null) {
7750
7912
  return { error: "Cannot finish: no map recorded - call set_project_map first." };
@@ -7788,7 +7950,7 @@ __export(pages_finder_exports, {
7788
7950
  import { existsSync } from "fs";
7789
7951
  import * as path2 from "path";
7790
7952
  import { tool as tool10 } from "ai";
7791
- import { z as z46 } from "zod";
7953
+ import { z as z47 } from "zod";
7792
7954
  async function runPageFinder(input) {
7793
7955
  const model = getModel(input.modelId);
7794
7956
  const pageCollector = new PageCollector();
@@ -7823,12 +7985,12 @@ ${input.extraMessage}`;
7823
7985
  }),
7824
7986
  view_pages: tool10({
7825
7987
  description: "use this tool to view all the pages that you already added",
7826
- inputSchema: z46.object(),
7988
+ inputSchema: z47.object(),
7827
7989
  execute: () => pageCollector.viewPages()
7828
7990
  }),
7829
7991
  finish: tool10({
7830
7992
  description: "End this step. Call once every page in the codebase has been added via add_page.",
7831
- inputSchema: z46.object({}),
7993
+ inputSchema: z47.object({}),
7832
7994
  execute: () => {
7833
7995
  if (pageCollector.pages.size === 0) {
7834
7996
  return {
@@ -7855,10 +8017,10 @@ var init_pages_finder = __esm({
7855
8017
  init_agent();
7856
8018
  init_model();
7857
8019
  init_tools();
7858
- Page = z46.object({
7859
- route: z46.string().min(1),
7860
- path: z46.string().min(1),
7861
- description: z46.string().min(10)
8020
+ Page = z47.object({
8021
+ route: z47.string().min(1),
8022
+ path: z47.string().min(1),
8023
+ description: z47.string().min(10)
7862
8024
  });
7863
8025
  PageCollector = class {
7864
8026
  // the key is the path
@@ -7887,14 +8049,20 @@ var init_pages_finder = __esm({
7887
8049
  });
7888
8050
 
7889
8051
  // src/core/progress.ts
7890
- function reportSubProgress(step, done, total, unit, note2) {
8052
+ function activeAgentNow() {
8053
+ const store = getActiveStore();
8054
+ if (store == null) return void 0;
8055
+ return agentNow(store.getState());
8056
+ }
8057
+ function reportSubProgress(step, done, total, unit, note2, startedAt) {
7891
8058
  if (total <= 0) return;
7892
- getActiveStore()?.setSubProgress(step, { done: Math.min(done, total), total, unit, note: note2 });
8059
+ getActiveStore()?.setSubProgress(step, { done: Math.min(done, total), total, unit, note: note2, startedAt });
7893
8060
  }
7894
8061
  var init_progress = __esm({
7895
8062
  "src/core/progress.ts"() {
7896
8063
  "use strict";
7897
8064
  init_esm_shims();
8065
+ init_eta();
7898
8066
  init_store();
7899
8067
  }
7900
8068
  });
@@ -8048,15 +8216,15 @@ var kb_generator_exports = {};
8048
8216
  __export(kb_generator_exports, {
8049
8217
  runKBGenerator: () => runKBGenerator
8050
8218
  });
8051
- import { readFile as readFile10 } from "fs/promises";
8052
- import { join as join19, resolve as resolve5 } from "path";
8219
+ import { readFile as readFile11 } from "fs/promises";
8220
+ import { join as join20, resolve as resolve5 } from "path";
8053
8221
  import { tool as tool11 } from "ai";
8054
- import { z as z47 } from "zod";
8222
+ import { z as z48 } from "zod";
8055
8223
  function buildRegisterPagesTool(tracker) {
8056
8224
  return tool11({
8057
8225
  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.",
8058
- inputSchema: z47.object({
8059
- pages: z47.array(z47.string()).describe("All page file paths found by glob")
8226
+ inputSchema: z48.object({
8227
+ pages: z48.array(z48.string()).describe("All page file paths found by glob")
8060
8228
  }),
8061
8229
  execute: async (input) => {
8062
8230
  tracker.register(input.pages);
@@ -8070,7 +8238,7 @@ function buildRegisterPagesTool(tracker) {
8070
8238
  function buildPageCoverageTool(tracker) {
8071
8239
  return tool11({
8072
8240
  description: "Check how many registered pages you've read vs how many remain.",
8073
- inputSchema: z47.object({}),
8241
+ inputSchema: z48.object({}),
8074
8242
  execute: async () => tracker.coverage()
8075
8243
  });
8076
8244
  }
@@ -8081,9 +8249,9 @@ function requiredReads(total) {
8081
8249
  function buildFinishTool(tracker, onFinish) {
8082
8250
  return tool11({
8083
8251
  description: "Call when you have finished generating the knowledge base. BLOCKED until you have read enough of the registered routes (every route on a small app; a strong majority on a large one) - call page_coverage first to check how many remain.",
8084
- inputSchema: z47.object({
8085
- summary: z47.string().describe("Summary of what was generated"),
8086
- artifacts: z47.array(z47.string()).describe("List of files written")
8252
+ inputSchema: z48.object({
8253
+ summary: z48.string().describe("Summary of what was generated"),
8254
+ artifacts: z48.array(z48.string()).describe("List of files written")
8087
8255
  }),
8088
8256
  execute: async (input) => {
8089
8257
  const cov = tracker.coverage();
@@ -8162,8 +8330,8 @@ Output files:
8162
8330
  const agentConfig = buildKbAgentConfig(tracker, model, input, onStepFinish, setResult);
8163
8331
  await runAgent(agentConfig, prompt, () => result);
8164
8332
  logger.summary();
8165
- const autonomaPath = join19(input.outputDir, "AUTONOMA.md");
8166
- const autonomaExists = await readFile10(autonomaPath, "utf-8").then(() => true).catch((err) => {
8333
+ const autonomaPath = join20(input.outputDir, "AUTONOMA.md");
8334
+ const autonomaExists = await readFile11(autonomaPath, "utf-8").then(() => true).catch((err) => {
8167
8335
  debugLog("AUTONOMA.md not found while checking step completion", { err });
8168
8336
  return false;
8169
8337
  });
@@ -8349,17 +8517,17 @@ var entity_audit_exports = {};
8349
8517
  __export(entity_audit_exports, {
8350
8518
  runEntityAudit: () => runEntityAudit
8351
8519
  });
8352
- import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
8353
- import { join as join20 } from "path";
8520
+ import { readFile as readFile12, writeFile as writeFile7 } from "fs/promises";
8521
+ import { join as join21 } from "path";
8354
8522
  import { tool as tool12 } from "ai";
8355
8523
  import { glob as glob4 } from "glob";
8356
- import { z as z48 } from "zod";
8524
+ import { z as z49 } from "zod";
8357
8525
  function buildRegisterModelsTool(tracker) {
8358
8526
  return tool12({
8359
8527
  description: "Register ALL database models discovered via grep. Call this ONCE after grepping for model definitions. After registering, use next_model to process them one at a time.",
8360
- inputSchema: z48.object({
8361
- models: z48.array(z48.string()).describe("All model/table names found by grep"),
8362
- framework: z48.string().describe("Database framework detected (e.g. 'sqlalchemy', 'prisma', 'drizzle')")
8528
+ inputSchema: z49.object({
8529
+ models: z49.array(z49.string()).describe("All model/table names found by grep"),
8530
+ framework: z49.string().describe("Database framework detected (e.g. 'sqlalchemy', 'prisma', 'drizzle')")
8363
8531
  }),
8364
8532
  execute: async (input) => {
8365
8533
  tracker.register(input.models);
@@ -8375,7 +8543,7 @@ function buildRegisterModelsTool(tracker) {
8375
8543
  function buildNextModelTool(tracker) {
8376
8544
  return tool12({
8377
8545
  description: "Get the next model to audit from the queue. If you called next_model before without calling mark_model_audited, the previous model is auto-skipped (marked as no creation path found). Returns done:true when all models are processed.",
8378
- inputSchema: z48.object({}),
8546
+ inputSchema: z49.object({}),
8379
8547
  execute: async () => {
8380
8548
  const next = tracker.nextModel();
8381
8549
  if (!next) {
@@ -8395,17 +8563,17 @@ function buildNextModelTool(tracker) {
8395
8563
  function buildMarkModelAuditedTool(tracker) {
8396
8564
  return tool12({
8397
8565
  description: "Mark a model as audited after you have determined its creation paths. Call this for EACH model after reading its creation code and determining independently_created + created_by. Include creation_function (e.g. 'UserService.create'), side_effects (list of things the creation does beyond the model itself), and for each created_by entry include owner, via (function name), and why (one sentence explaining the relationship).",
8398
- inputSchema: z48.object({
8399
- model: z48.string().describe("Model name"),
8400
- independently_created: z48.boolean(),
8401
- creation_file: z48.string().optional().describe("File containing the creation function"),
8402
- creation_function: z48.string().optional().describe("Function/method name (e.g. 'UserService.create' or 'create_user')"),
8403
- side_effects: z48.array(z48.string()).optional().describe("Side effects of creation (e.g. 'creates default Settings row', 'hashes password')"),
8404
- created_by: z48.array(
8405
- z48.object({
8406
- owner: z48.string().describe("Owner model name"),
8407
- via: z48.string().optional().describe("Function that creates this model (e.g. 'OrganizationService.create')"),
8408
- why: z48.string().optional().describe("One sentence explaining why this model is created as a side effect")
8566
+ inputSchema: z49.object({
8567
+ model: z49.string().describe("Model name"),
8568
+ independently_created: z49.boolean(),
8569
+ creation_file: z49.string().optional().describe("File containing the creation function"),
8570
+ creation_function: z49.string().optional().describe("Function/method name (e.g. 'UserService.create' or 'create_user')"),
8571
+ side_effects: z49.array(z49.string()).optional().describe("Side effects of creation (e.g. 'creates default Settings row', 'hashes password')"),
8572
+ created_by: z49.array(
8573
+ z49.object({
8574
+ owner: z49.string().describe("Owner model name"),
8575
+ via: z49.string().optional().describe("Function that creates this model (e.g. 'OrganizationService.create')"),
8576
+ why: z49.string().optional().describe("One sentence explaining why this model is created as a side effect")
8409
8577
  })
8410
8578
  ).describe("List of owner models that create this as a side effect, empty array if none")
8411
8579
  }),
@@ -8444,16 +8612,16 @@ function buildMarkModelAuditedTool(tracker) {
8444
8612
  function buildModelCoverageTool(tracker) {
8445
8613
  return tool12({
8446
8614
  description: "Check how many registered models you've audited vs how many remain.",
8447
- inputSchema: z48.object({}),
8615
+ inputSchema: z49.object({}),
8448
8616
  execute: async () => tracker.coverage()
8449
8617
  });
8450
8618
  }
8451
8619
  function buildFinishTool2(tracker, onFinish) {
8452
8620
  return tool12({
8453
8621
  description: "Call when entity audit is complete. BLOCKED if there are unaudited models - call model_coverage first to check.",
8454
- inputSchema: z48.object({
8455
- summary: z48.string().describe("Summary of the audit"),
8456
- artifacts: z48.array(z48.string()).describe("Files written")
8622
+ inputSchema: z49.object({
8623
+ summary: z49.string().describe("Summary of the audit"),
8624
+ artifacts: z49.array(z49.string()).describe("Files written")
8457
8625
  }),
8458
8626
  execute: async (input) => {
8459
8627
  const cov = tracker.coverage();
@@ -8481,7 +8649,7 @@ async function findPrismaSchema(projectRoot) {
8481
8649
  return candidates[0] ?? void 0;
8482
8650
  }
8483
8651
  async function extractPrismaModels(schemaPath) {
8484
- const content = await readFile11(schemaPath, "utf-8");
8652
+ const content = await readFile12(schemaPath, "utf-8");
8485
8653
  return content.split("\n").filter((line) => line.startsWith("model ")).map((line) => line.split(/\s+/)[1]).filter((name) => name != null);
8486
8654
  }
8487
8655
  async function detectFrameworkAndModels(projectRoot) {
@@ -8568,8 +8736,8 @@ ${formatException(err)}`);
8568
8736
  logger.summary();
8569
8737
  const writeCanonicalAudit = async () => {
8570
8738
  if (tracker.auditedModels.size === 0) return void 0;
8571
- const auditPath = join20(input.outputDir, "entity-audit.md");
8572
- await writeFile6(auditPath, tracker.generateAuditMarkdown(), "utf-8");
8739
+ const auditPath = join21(input.outputDir, "entity-audit.md");
8740
+ await writeFile7(auditPath, tracker.generateAuditMarkdown(), "utf-8");
8573
8741
  return auditPath;
8574
8742
  };
8575
8743
  const canonicalPath = await writeCanonicalAudit();
@@ -8583,9 +8751,9 @@ ${formatException(err)}`);
8583
8751
  }
8584
8752
  const reviewed = result;
8585
8753
  if (!reviewed) {
8586
- const auditPath = join20(input.outputDir, "entity-audit.md");
8754
+ const auditPath = join21(input.outputDir, "entity-audit.md");
8587
8755
  try {
8588
- await readFile11(auditPath, "utf-8");
8756
+ await readFile12(auditPath, "utf-8");
8589
8757
  return {
8590
8758
  success: true,
8591
8759
  artifacts: ["entity-audit.md"],
@@ -8795,8 +8963,8 @@ on a unique constraint. Everything else stays concrete and static, and no other
8795
8963
  });
8796
8964
 
8797
8965
  // src/agents/03-scenario-recipe/scenario-table.ts
8798
- import { readFile as readFile12 } from "fs/promises";
8799
- import { join as join21 } from "path";
8966
+ import { readFile as readFile13 } from "fs/promises";
8967
+ import { join as join22 } from "path";
8800
8968
  import matter2 from "gray-matter";
8801
8969
  function validateScenarioIsConcrete(content) {
8802
8970
  const errors = findUnknownRecipeTokens(content).map(
@@ -8831,22 +8999,22 @@ __export(scenario_recipe_exports, {
8831
8999
  feedbackToScenario: () => feedbackToScenario,
8832
9000
  runScenarioRecipe: () => runScenarioRecipe
8833
9001
  });
8834
- import { readFile as readFile13 } from "fs/promises";
8835
- import { join as join22 } from "path";
9002
+ import { readFile as readFile14 } from "fs/promises";
9003
+ import { join as join23 } from "path";
8836
9004
  import { tool as tool13 } from "ai";
8837
- import { z as z49 } from "zod";
9005
+ import { z as z50 } from "zod";
8838
9006
  function buildFinishTool3(requiredEntities, outputDir, onFinish) {
8839
9007
  return tool13({
8840
9008
  description: "Call when scenario design is complete and scenarios.md is written. BLOCKED if any required entities are missing from the scenario.",
8841
- inputSchema: z49.object({
8842
- summary: z49.string().describe("Summary of the scenario"),
8843
- entityCount: z49.number().describe("Number of entity types in the scenario"),
8844
- artifacts: z49.array(z49.string()).describe("Files written")
9009
+ inputSchema: z50.object({
9010
+ summary: z50.string().describe("Summary of the scenario"),
9011
+ entityCount: z50.number().describe("Number of entity types in the scenario"),
9012
+ artifacts: z50.array(z50.string()).describe("Files written")
8845
9013
  }),
8846
9014
  execute: async (input) => {
8847
9015
  let content;
8848
9016
  try {
8849
- content = await readFile13(join22(outputDir, "scenarios.md"), "utf-8");
9017
+ content = await readFile14(join23(outputDir, "scenarios.md"), "utf-8");
8850
9018
  } catch {
8851
9019
  return { error: "Cannot finish: scenarios.md not found. Write it first." };
8852
9020
  }
@@ -8925,9 +9093,9 @@ When done, call finish.`;
8925
9093
  logger.summary();
8926
9094
  const reviewed = result;
8927
9095
  if (!reviewed) {
8928
- const scenariosPath = join22(input.outputDir, "scenarios.md");
9096
+ const scenariosPath = join23(input.outputDir, "scenarios.md");
8929
9097
  try {
8930
- await readFile13(scenariosPath, "utf-8");
9098
+ await readFile14(scenariosPath, "utf-8");
8931
9099
  return {
8932
9100
  success: true,
8933
9101
  artifacts: ["scenarios.md"],
@@ -8986,12 +9154,12 @@ var init_scenario_recipe = __esm({
8986
9154
  });
8987
9155
 
8988
9156
  // src/agents/04-recipe-builder/entity-order.ts
8989
- import { readFile as readFile14 } from "fs/promises";
8990
- import { join as join23 } from "path";
9157
+ import { readFile as readFile15 } from "fs/promises";
9158
+ import { join as join24 } from "path";
8991
9159
  import matter3 from "gray-matter";
8992
- import { z as z50 } from "zod";
9160
+ import { z as z51 } from "zod";
8993
9161
  async function parseEntityAudit(outputDir) {
8994
- const raw = await readFile14(join23(outputDir, "entity-audit.md"), "utf-8");
9162
+ const raw = await readFile15(join24(outputDir, "entity-audit.md"), "utf-8");
8995
9163
  try {
8996
9164
  const parsed = frontmatterSchema.safeParse(matter3(raw).data);
8997
9165
  if (parsed.success && parsed.data.models.length > 0) {
@@ -9152,34 +9320,34 @@ var init_entity_order = __esm({
9152
9320
  "src/agents/04-recipe-builder/entity-order.ts"() {
9153
9321
  "use strict";
9154
9322
  init_esm_shims();
9155
- createdBySchema = z50.object({
9156
- owner: z50.string(),
9157
- via: z50.string().optional(),
9158
- why: z50.string().optional()
9323
+ createdBySchema = z51.object({
9324
+ owner: z51.string(),
9325
+ via: z51.string().optional(),
9326
+ why: z51.string().optional()
9159
9327
  });
9160
- auditedModelSchema = z50.object({
9161
- name: z50.string(),
9162
- independently_created: z50.coerce.boolean().default(false),
9163
- creation_file: z50.string().optional(),
9164
- creation_function: z50.string().optional(),
9165
- side_effects: z50.array(z50.string()).optional(),
9328
+ auditedModelSchema = z51.object({
9329
+ name: z51.string(),
9330
+ independently_created: z51.coerce.boolean().default(false),
9331
+ creation_file: z51.string().optional(),
9332
+ creation_function: z51.string().optional(),
9333
+ side_effects: z51.array(z51.string()).optional(),
9166
9334
  // Tolerate a stray `created_by:` with no entries (parsed as null by YAML).
9167
- created_by: z50.array(createdBySchema).nullish().transform((v) => v ?? [])
9335
+ created_by: z51.array(createdBySchema).nullish().transform((v) => v ?? [])
9168
9336
  });
9169
- frontmatterSchema = z50.object({
9170
- models: z50.array(auditedModelSchema).nullish().transform((v) => v ?? [])
9337
+ frontmatterSchema = z51.object({
9338
+ models: z51.array(auditedModelSchema).nullish().transform((v) => v ?? [])
9171
9339
  });
9172
9340
  }
9173
9341
  });
9174
9342
 
9175
9343
  // src/agents/04-recipe-builder/completion.ts
9176
- import { readFile as readFile15 } from "fs/promises";
9177
- import { join as join24 } from "path";
9178
- import { z as z51 } from "zod";
9344
+ import { readFile as readFile16 } from "fs/promises";
9345
+ import { join as join25 } from "path";
9346
+ import { z as z52 } from "zod";
9179
9347
  async function readCompletion(outputDir) {
9180
9348
  let raw;
9181
9349
  try {
9182
- raw = await readFile15(join24(outputDir, COMPLETION_MARKER_FILE), "utf-8");
9350
+ raw = await readFile16(join25(outputDir, COMPLETION_MARKER_FILE), "utf-8");
9183
9351
  } catch (err) {
9184
9352
  debugLog("No completion marker yet", { err });
9185
9353
  return false;
@@ -9205,7 +9373,7 @@ var init_completion = __esm({
9205
9373
  init_esm_shims();
9206
9374
  init_debug();
9207
9375
  COMPLETION_MARKER_FILE = ".sdk-integration-complete";
9208
- completionSchema = z51.object({ complete: z51.literal(true) });
9376
+ completionSchema = z52.object({ complete: z52.literal(true) });
9209
9377
  }
9210
9378
  });
9211
9379
 
@@ -9253,8 +9421,8 @@ var init_completion_watch = __esm({
9253
9421
  });
9254
9422
 
9255
9423
  // src/agents/04-recipe-builder/integration-prompt.ts
9256
- import { writeFile as writeFile7 } from "fs/promises";
9257
- import { join as join25 } from "path";
9424
+ import { writeFile as writeFile8 } from "fs/promises";
9425
+ import { join as join26 } from "path";
9258
9426
  function renderIntegrationPrompt(params) {
9259
9427
  const priorFailureSection = params.priorFailure != null ? `
9260
9428
  \u2550\u2550\u2550 A PRIOR SESSION DID NOT COMPLETE - READ THIS FIRST \u2550\u2550\u2550
@@ -9543,7 +9711,7 @@ Write the completion marker once ALL of these hold:
9543
9711
  A step you documented as genuinely blocked NEVER justifies withholding the marker; a
9544
9712
  checklist item you simply have not finished always does. The marker is how the CLI knows
9545
9713
  the session is done and can upload the recipe:
9546
- ${join25(params.outputDir, COMPLETION_MARKER_FILE)}
9714
+ ${join26(params.outputDir, COMPLETION_MARKER_FILE)}
9547
9715
  Its contents MUST be exactly:
9548
9716
  { "complete": true }
9549
9717
  Writing it is not optional: the planner watches for this marker and takes the terminal
@@ -9555,8 +9723,8 @@ or the commit you left behind and what blocked the push - and then says:
9555
9723
  Nothing after that message - no further questions, summaries, or work.`;
9556
9724
  }
9557
9725
  async function writeIntegrationPrompt(params) {
9558
- const path3 = join25(params.outputDir, INTEGRATION_PROMPT_FILE);
9559
- await writeFile7(path3, renderIntegrationPrompt(params), "utf-8");
9726
+ const path3 = join26(params.outputDir, INTEGRATION_PROMPT_FILE);
9727
+ await writeFile8(path3, renderIntegrationPrompt(params), "utf-8");
9560
9728
  return path3;
9561
9729
  }
9562
9730
  var INTEGRATION_PROMPT_VERSION, INTEGRATION_PROMPT_FILE, DEFAULT_ENDPOINT_PATH, INTEGRATION_BRANCH;
@@ -9574,8 +9742,8 @@ var init_integration_prompt = __esm({
9574
9742
  });
9575
9743
 
9576
9744
  // src/agents/04-recipe-builder/state.ts
9577
- import { readFile as readFile16, writeFile as writeFile8 } from "fs/promises";
9578
- import { join as join26 } from "path";
9745
+ import { readFile as readFile17, writeFile as writeFile9 } from "fs/promises";
9746
+ import { join as join27 } from "path";
9579
9747
  function initialRecipeState() {
9580
9748
  return {
9581
9749
  phase: "tech-detect",
@@ -9585,7 +9753,7 @@ function initialRecipeState() {
9585
9753
  }
9586
9754
  async function loadRecipeState(outputDir) {
9587
9755
  try {
9588
- const raw = await readFile16(join26(outputDir, STATE_FILE2), "utf-8");
9756
+ const raw = await readFile17(join27(outputDir, STATE_FILE2), "utf-8");
9589
9757
  const parsed = JSON.parse(raw);
9590
9758
  return parsed;
9591
9759
  } catch {
@@ -9593,7 +9761,7 @@ async function loadRecipeState(outputDir) {
9593
9761
  }
9594
9762
  }
9595
9763
  async function saveRecipeState(outputDir, state) {
9596
- await writeFile8(join26(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
9764
+ await writeFile9(join27(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
9597
9765
  }
9598
9766
  var STATE_FILE2;
9599
9767
  var init_state2 = __esm({
@@ -9606,22 +9774,19 @@ var init_state2 = __esm({
9606
9774
 
9607
9775
  // src/agents/04-recipe-builder/phases/handoff.ts
9608
9776
  import { rm as rm2 } from "fs/promises";
9609
- import { join as join27 } from "path";
9777
+ import { join as join28 } from "path";
9610
9778
  function launchMessage(promptFile) {
9611
9779
  return `Read the file ${promptFile} and follow its instructions exactly to integrate Autonoma into this application. It is your complete spec. Do not stop until every item in it is done and you have written the completion marker it describes.`;
9612
9780
  }
9613
9781
  async function runHandoffPhase(state, deps, outputDir) {
9614
- const recipePath = state.recipePath ?? join27(outputDir, RECIPE_FILE);
9782
+ const recipePath = state.recipePath ?? join28(outputDir, RECIPE_FILE);
9615
9783
  state.recipePath = recipePath;
9616
9784
  const launcher = await selectLauncher(deps.launchers, state.agentId ?? deps.presetAgentId, deps.interactive);
9617
9785
  if (launcher == null) {
9618
9786
  if (!deps.interactive) {
9619
- return {
9620
- kind: "handback",
9621
- summary: "--non-interactive SDK implementation requires an installed, supported coding agent, but none was resolved on your PATH (name one with --agent if more than one is present). Install one, or run interactively."
9622
- };
9787
+ return { kind: "handback", summary: NO_AGENT_FOUND_HEADLESS };
9623
9788
  }
9624
- return manualFallback(outputDir, recipePath, deps.cliCommand, "No supported agent found on your PATH.");
9789
+ return manualFallback(outputDir, recipePath, deps.cliCommand, NO_AGENT_FOUND);
9625
9790
  }
9626
9791
  const permissionMode = deps.interactive ? await selectPermissionMode(state.permissionMode ?? deps.presetPermissionMode) : state.permissionMode ?? deps.presetPermissionMode ?? DEFAULT_PERMISSION_MODE;
9627
9792
  state.agentId = launcher.id;
@@ -9640,7 +9805,7 @@ async function runHandoffPhase(state, deps, outputDir) {
9640
9805
  return { kind: "advance" };
9641
9806
  }
9642
9807
  async function runCompletionPhase(state, deps, outputDir) {
9643
- const recipePath = state.recipePath ?? join27(outputDir, RECIPE_FILE);
9808
+ const recipePath = state.recipePath ?? join28(outputDir, RECIPE_FILE);
9644
9809
  while (true) {
9645
9810
  const [complete, read] = await Promise.all([readCompletion(outputDir), loadRecipe(outputDir)]);
9646
9811
  const uploadProblems = read.status === "ok" ? findRecipeUploadProblems(read.recipe) : [];
@@ -9649,8 +9814,13 @@ async function runCompletionPhase(state, deps, outputDir) {
9649
9814
  log.success("The agent reported the integration complete and validated.");
9650
9815
  return { kind: "advance" };
9651
9816
  }
9652
- const failure = describeIncompleteRecipe(read, uploadProblems, recipePath, deps.cliCommand);
9653
9817
  const launcher = await selectLauncher(deps.launchers, state.agentId ?? deps.presetAgentId, deps.interactive);
9818
+ const nothingEverRan = read.status === "absent" && launcher == null && (state.launchAttempts ?? 0) === 0;
9819
+ if (nothingEverRan) {
9820
+ if (!deps.interactive) return { kind: "handback", summary: NO_AGENT_FOUND_HEADLESS };
9821
+ return await manualFallback(outputDir, recipePath, deps.cliCommand, NO_AGENT_FOUND);
9822
+ }
9823
+ const failure = describeIncompleteRecipe(read, uploadProblems, recipePath, deps.cliCommand);
9654
9824
  if (launcher != null && (state.launchAttempts ?? 0) < MAX_LAUNCH_ATTEMPTS) {
9655
9825
  log.warn("The integration isn't complete yet. Launching the agent to finish it...");
9656
9826
  state.agentId = launcher.id;
@@ -9674,7 +9844,7 @@ async function runCompletionPhase(state, deps, outputDir) {
9674
9844
  function handbackSummary(failure, outputDir) {
9675
9845
  return `${failure}
9676
9846
 
9677
- Finish the integration (see the prompt at ${join27(outputDir, INTEGRATION_PROMPT_FILE)}), then re-run with --resume.`;
9847
+ Finish the integration (see the prompt at ${join28(outputDir, INTEGRATION_PROMPT_FILE)}), then re-run with --resume.`;
9678
9848
  }
9679
9849
  async function launchAgent(launcher, permissionMode, target) {
9680
9850
  const promptFile = await writeIntegrationPrompt({
@@ -9684,7 +9854,7 @@ async function launchAgent(launcher, permissionMode, target) {
9684
9854
  priorFailure: target.priorFailure,
9685
9855
  userGuidance: target.userGuidance
9686
9856
  });
9687
- await rm2(join27(target.outputDir, COMPLETION_MARKER_FILE), { force: true }).catch((err) => {
9857
+ await rm2(join28(target.outputDir, COMPLETION_MARKER_FILE), { force: true }).catch((err) => {
9688
9858
  debugLog("Could not clear a stale completion marker", { err });
9689
9859
  });
9690
9860
  log.info(`Launching ${launcher.label}. It will implement the integration - watch and steer it as it works.`);
@@ -9736,19 +9906,24 @@ ${recheck}`;
9736
9906
  }
9737
9907
  async function manualFallback(outputDir, recipePath, cliCommand, why) {
9738
9908
  const promptFile = await writeIntegrationPrompt({ outputDir, recipePath, cliCommand });
9739
- note(
9740
- `${why}
9741
-
9742
- The full integration instructions have been written to:
9743
- ${promptFile}
9744
-
9745
- Implement them with your AI assistant (or by hand), get your app running locally, then
9746
- re-run with --resume. The CLI will continue to test generation.`,
9747
- "Implement the integration yourself"
9748
- );
9749
- return { kind: "pause", summary: "Integration handed off for manual implementation - resume when it's ready." };
9909
+ await welcome({
9910
+ eyebrow: "NO CODING AGENT FOUND",
9911
+ title: "Finish this step in the coding agent you already use",
9912
+ lines: [
9913
+ why,
9914
+ "Any coding agent can do this step just as well - Cursor, Windsurf, Copilot, Zed, or whatever you have open. Point yours at this project and paste it this:",
9915
+ `Read ${promptFile} and follow its instructions exactly.`,
9916
+ "That file is the complete spec: the endpoint to implement, the factories to register, and the completion marker it must write when it is done.",
9917
+ `When your agent reports it finished, come back here and run: ${cliCommand} --resume`
9918
+ ],
9919
+ cta: "Press enter once you have handed it over"
9920
+ });
9921
+ return {
9922
+ kind: "pause",
9923
+ summary: `${why} Integration handed off for manual implementation - resume when it's ready.`
9924
+ };
9750
9925
  }
9751
- var MAX_LAUNCH_ATTEMPTS, HANDOFF_COUNTDOWN_SECONDS;
9926
+ var MAX_LAUNCH_ATTEMPTS, HANDOFF_COUNTDOWN_SECONDS, NO_AGENT_FOUND, NO_AGENT_FOUND_HEADLESS;
9752
9927
  var init_handoff = __esm({
9753
9928
  "src/agents/04-recipe-builder/phases/handoff.ts"() {
9754
9929
  "use strict";
@@ -9765,6 +9940,8 @@ var init_handoff = __esm({
9765
9940
  init_state2();
9766
9941
  MAX_LAUNCH_ATTEMPTS = 2;
9767
9942
  HANDOFF_COUNTDOWN_SECONDS = 10;
9943
+ NO_AGENT_FOUND = "Autonoma can launch Claude Code or Codex for you, but neither is on your PATH.";
9944
+ NO_AGENT_FOUND_HEADLESS = "--non-interactive SDK implementation requires an installed, supported coding agent, but none was resolved on your PATH (name one with --agent if more than one is present). Install one, or run interactively.";
9768
9945
  }
9769
9946
  });
9770
9947
 
@@ -9875,17 +10052,17 @@ var init_recipe_builder = __esm({
9875
10052
  });
9876
10053
 
9877
10054
  // src/agents/00b-feature-discovery/index.ts
9878
- import { readFile as readFile17, writeFile as writeFile9 } from "fs/promises";
9879
- import { join as join28 } from "path";
10055
+ import { readFile as readFile18, writeFile as writeFile10 } from "fs/promises";
10056
+ import { join as join29 } from "path";
9880
10057
  import { tool as tool14 } from "ai";
9881
- import { z as z52 } from "zod";
10058
+ import { z as z53 } from "zod";
9882
10059
  async function saveFeatures(outputDir, features) {
9883
10060
  const obj = Object.fromEntries(features);
9884
- await writeFile9(join28(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
10061
+ await writeFile10(join29(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
9885
10062
  }
9886
10063
  async function loadFeatures(outputDir) {
9887
10064
  try {
9888
- const raw = await readFile17(join28(outputDir, FEATURES_FILE), "utf-8");
10065
+ const raw = await readFile18(join29(outputDir, FEATURES_FILE), "utf-8");
9889
10066
  const obj = JSON.parse(raw);
9890
10067
  return new Map(Object.entries(obj));
9891
10068
  } catch {
@@ -9919,7 +10096,7 @@ Process every page. Call add_feature for each sub-feature you discover. When don
9919
10096
  add_feature: tool14({
9920
10097
  description: "Add a discovered sub-feature",
9921
10098
  inputSchema: Feature.extend({
9922
- id: z52.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
10099
+ id: z53.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
9923
10100
  }),
9924
10101
  execute: (featureInput) => {
9925
10102
  const { id, ...rest } = featureInput;
@@ -9933,17 +10110,17 @@ Process every page. Call add_feature for each sub-feature you discover. When don
9933
10110
  }),
9934
10111
  view_features: tool14({
9935
10112
  description: "View all discovered features so far",
9936
- inputSchema: z52.object({}),
10113
+ inputSchema: z53.object({}),
9937
10114
  execute: () => collector.viewFeatures()
9938
10115
  }),
9939
10116
  view_pages: tool14({
9940
10117
  description: "View the pages list to know what to analyze",
9941
- inputSchema: z52.object({}),
10118
+ inputSchema: z53.object({}),
9942
10119
  execute: () => pagesDescription
9943
10120
  }),
9944
10121
  finish: tool14({
9945
10122
  description: "Signal that feature discovery is complete",
9946
- inputSchema: z52.object({ summary: z52.string() }),
10123
+ inputSchema: z53.object({ summary: z53.string() }),
9947
10124
  execute: async (finishInput) => {
9948
10125
  result = {
9949
10126
  success: true,
@@ -9974,13 +10151,13 @@ var init_b_feature_discovery = __esm({
9974
10151
  init_model();
9975
10152
  init_tools();
9976
10153
  FEATURES_FILE = "features.json";
9977
- Feature = z52.object({
9978
- name: z52.string().min(1).describe("Human-readable name (e.g. 'Settings > Notifications Tab', 'Create Project Modal')"),
9979
- type: z52.enum(["tab", "modal", "form", "table", "wizard", "nested-route", "complex-component"]),
9980
- parentPagePath: z52.string().min(1).describe("The page path this feature belongs to (from the pages list)"),
9981
- sourceFiles: z52.array(z52.string()).min(1).describe("Relative paths to the source files for this sub-feature"),
9982
- interactiveElements: z52.number().int().min(0).describe("Count of interactive elements found (buttons, inputs, toggles, etc.)"),
9983
- description: z52.string().min(10).describe("What this sub-feature does")
10154
+ Feature = z53.object({
10155
+ name: z53.string().min(1).describe("Human-readable name (e.g. 'Settings > Notifications Tab', 'Create Project Modal')"),
10156
+ type: z53.enum(["tab", "modal", "form", "table", "wizard", "nested-route", "complex-component"]),
10157
+ parentPagePath: z53.string().min(1).describe("The page path this feature belongs to (from the pages list)"),
10158
+ sourceFiles: z53.array(z53.string()).min(1).describe("Relative paths to the source files for this sub-feature"),
10159
+ interactiveElements: z53.number().int().min(0).describe("Count of interactive elements found (buttons, inputs, toggles, etc.)"),
10160
+ description: z53.string().min(10).describe("What this sub-feature does")
9984
10161
  });
9985
10162
  FeatureCollector = class {
9986
10163
  features = /* @__PURE__ */ new Map();
@@ -10063,21 +10240,21 @@ Use kebab-case IDs that indicate the parent page and feature type:
10063
10240
  });
10064
10241
 
10065
10242
  // src/agents/05-test-generator/graph.ts
10066
- import { readFile as readFile18, writeFile as writeFile10 } from "fs/promises";
10067
- import { join as join29 } from "path";
10243
+ import { readFile as readFile19, writeFile as writeFile11 } from "fs/promises";
10244
+ import { join as join30 } from "path";
10068
10245
  function estimateExpectedTests(written, processed, totalNodes) {
10069
10246
  if (totalNodes <= 0) return written;
10070
10247
  const rate = written > 0 && processed >= LIVE_RATE_MIN_PROCESSED ? written / processed : TESTS_PER_NODE_PRIOR;
10071
10248
  return Math.max(written, Math.round(rate * totalNodes));
10072
10249
  }
10073
10250
  async function saveBfsState(outputDir, state) {
10074
- const path3 = join29(outputDir, state.stateFile);
10075
- await writeFile10(path3, JSON.stringify(state.serialize(), null, 2), "utf-8");
10251
+ const path3 = join30(outputDir, state.stateFile);
10252
+ await writeFile11(path3, JSON.stringify(state.serialize(), null, 2), "utf-8");
10076
10253
  }
10077
10254
  async function loadBfsState(outputDir) {
10078
- const path3 = join29(outputDir, BFS_STATE_FILE);
10255
+ const path3 = join30(outputDir, BFS_STATE_FILE);
10079
10256
  try {
10080
- const raw = await readFile18(path3, "utf-8");
10257
+ const raw = await readFile19(path3, "utf-8");
10081
10258
  return CoverageState.deserialize(JSON.parse(raw));
10082
10259
  } catch {
10083
10260
  return void 0;
@@ -10089,6 +10266,7 @@ var init_graph = __esm({
10089
10266
  "use strict";
10090
10267
  init_esm_shims();
10091
10268
  init_progress();
10269
+ init_store();
10092
10270
  BFS_STATE_FILE = ".bfs-state.json";
10093
10271
  JOURNEY_STATE_FILE = ".journey-state.json";
10094
10272
  TESTS_PER_NODE_PRIOR = 3;
@@ -10205,6 +10383,29 @@ var init_graph = __esm({
10205
10383
  this.phase = phase;
10206
10384
  this.reportProgress();
10207
10385
  }
10386
+ /**
10387
+ * Report the review pass as its own sub-progress, distinct from node
10388
+ * exploration. The step strip already shows "13/13 nodes" once generation
10389
+ * finishes; this replaces it with "Review 4/10" so the run does not look
10390
+ * done while the longest silent phase is still ahead.
10391
+ *
10392
+ * `done`/`total` count the tests reviewed this cycle. `startedAt` lets the
10393
+ * ETA bridge the gap before the first completion arrives.
10394
+ */
10395
+ setReviewProgress(done, total, startedAt) {
10396
+ if (!this.reportsProgress) return;
10397
+ const note2 = this.phase;
10398
+ reportSubProgress("testGenerator", done, total, "reviewed", note2, startedAt);
10399
+ }
10400
+ /**
10401
+ * The review pass is over: restore the node-exploration sub-progress on the
10402
+ * strip, and settle the per-test rows so nothing is left claiming to be
10403
+ * under review or awaiting a fix that the budget cut off.
10404
+ */
10405
+ clearReviewProgress() {
10406
+ this.reportProgress();
10407
+ getActiveStore()?.settleReviews();
10408
+ }
10208
10409
  /** Processed nodes (tested or skipped) over the known graph size, labelled
10209
10410
  * with the running phase or - while exploring - a live estimate of the final
10210
10411
  * test count, which isn't known upfront (each node's test count is decided as
@@ -10653,13 +10854,13 @@ When you finish, all dimensions are reported so the user knows what's covered an
10653
10854
  });
10654
10855
 
10655
10856
  // src/agents/05-test-generator/recipe-context.ts
10656
- import { readFile as readFile19 } from "fs/promises";
10657
- import { join as join30 } from "path";
10658
- import { z as z53 } from "zod";
10857
+ import { readFile as readFile20 } from "fs/promises";
10858
+ import { join as join31 } from "path";
10859
+ import { z as z54 } from "zod";
10659
10860
  async function loadRecipeContext(outputDir) {
10660
10861
  let raw;
10661
10862
  try {
10662
- raw = await readFile19(join30(outputDir, "recipe.json"), "utf-8");
10863
+ raw = await readFile20(join31(outputDir, "recipe.json"), "utf-8");
10663
10864
  } catch (err) {
10664
10865
  debugLog("recipe.json not present; test generation falls back to scenarios.md alone", { err });
10665
10866
  return "";
@@ -10717,13 +10918,13 @@ var init_recipe_context = __esm({
10717
10918
  init_debug();
10718
10919
  STRUCTURAL_KEYS = /* @__PURE__ */ new Set(["_alias", "_ref"]);
10719
10920
  TEMPLATE_TOKEN = /\{\{\s*\w+\s*\}\}/;
10720
- recipeRowSchema = z53.record(z53.string(), z53.unknown());
10721
- recipeFileSchema = z53.object({
10722
- recipes: z53.array(
10723
- z53.object({
10724
- name: z53.string(),
10725
- description: z53.string().optional(),
10726
- create: z53.record(z53.string(), z53.array(recipeRowSchema)).optional()
10921
+ recipeRowSchema = z54.record(z54.string(), z54.unknown());
10922
+ recipeFileSchema = z54.object({
10923
+ recipes: z54.array(
10924
+ z54.object({
10925
+ name: z54.string(),
10926
+ description: z54.string().optional(),
10927
+ create: z54.record(z54.string(), z54.array(recipeRowSchema)).optional()
10727
10928
  })
10728
10929
  )
10729
10930
  });
@@ -10731,7 +10932,7 @@ var init_recipe_context = __esm({
10731
10932
  });
10732
10933
 
10733
10934
  // src/agents/05-test-generator/restore-deleted-test.ts
10734
- import { access, mkdir as mkdir3, writeFile as writeFile11 } from "fs/promises";
10935
+ import { access, mkdir as mkdir4, writeFile as writeFile12 } from "fs/promises";
10735
10936
  import { dirname as dirname3 } from "path";
10736
10937
  async function restoreDeletedTest(testPath, originalContent) {
10737
10938
  try {
@@ -10741,8 +10942,8 @@ async function restoreDeletedTest(testPath, originalContent) {
10741
10942
  debugLog("Test absent after its fix pass - restoring the pre-review content", { testPath, err });
10742
10943
  }
10743
10944
  try {
10744
- await mkdir3(dirname3(testPath), { recursive: true });
10745
- await writeFile11(testPath, originalContent, "utf-8");
10945
+ await mkdir4(dirname3(testPath), { recursive: true });
10946
+ await writeFile12(testPath, originalContent, "utf-8");
10746
10947
  return true;
10747
10948
  } catch (err) {
10748
10949
  console.warn(` [fix] Could not restore ${testPath}: ${err instanceof Error ? err.message : String(err)}`);
@@ -10764,19 +10965,25 @@ var init_restore_deleted_test = __esm({
10764
10965
  });
10765
10966
 
10766
10967
  // src/core/pool.ts
10767
- async function runPool(items, { limit, shouldContinue }, work) {
10968
+ async function runPool(items, { limit, shouldContinue, onProgress, onStart }, work) {
10768
10969
  const completed = [];
10769
10970
  const failed = [];
10770
10971
  const skipped = [];
10771
10972
  const inFlight2 = /* @__PURE__ */ new Set();
10772
10973
  let next = 0;
10974
+ const total = items.length;
10975
+ const settled = { n: 0 };
10976
+ const fireProgress = () => onProgress?.(settled.n, total);
10773
10977
  const dispatch = (item, index) => {
10978
+ onStart?.(item, index);
10774
10979
  const promise = work(item, index).then((result) => {
10775
10980
  completed.push({ item, index, result });
10776
10981
  }).catch((error) => {
10777
10982
  failed.push({ item, index, error });
10778
10983
  }).finally(() => {
10779
10984
  inFlight2.delete(promise);
10985
+ settled.n++;
10986
+ fireProgress();
10780
10987
  });
10781
10988
  inFlight2.add(promise);
10782
10989
  };
@@ -10803,21 +11010,21 @@ var init_pool = __esm({
10803
11010
  });
10804
11011
 
10805
11012
  // src/agents/05-test-generator/rubrics.ts
10806
- import { z as z54 } from "zod";
11013
+ import { z as z55 } from "zod";
10807
11014
  function reviewResultSchema(shape) {
10808
- return z54.object(shape);
11015
+ return z55.object(shape);
10809
11016
  }
10810
11017
  var dimensionResultSchema, reviewResultRecordSchema, structuralIntentRubric, flowCompletenessRubric, uiTextRubric, dataAccuracyRubric, ALL_RUBRICS;
10811
11018
  var init_rubrics = __esm({
10812
11019
  "src/agents/05-test-generator/rubrics.ts"() {
10813
11020
  "use strict";
10814
11021
  init_esm_shims();
10815
- dimensionResultSchema = z54.object({
10816
- pass: z54.boolean(),
10817
- evidence: z54.string().describe("What you checked and found - cite file paths, line content, or specific strings"),
10818
- suggestion: z54.string().optional().describe("What the planner agent should fix, if failing")
11022
+ dimensionResultSchema = z55.object({
11023
+ pass: z55.boolean(),
11024
+ evidence: z55.string().describe("What you checked and found - cite file paths, line content, or specific strings"),
11025
+ suggestion: z55.string().optional().describe("What the planner agent should fix, if failing")
10819
11026
  });
10820
- reviewResultRecordSchema = z54.record(z54.string(), dimensionResultSchema);
11027
+ reviewResultRecordSchema = z55.record(z55.string(), dimensionResultSchema);
10821
11028
  structuralIntentRubric = {
10822
11029
  name: "structural-intent",
10823
11030
  maxSteps: 8,
@@ -11063,10 +11270,13 @@ var init_review_pass = __esm({
11063
11270
  });
11064
11271
 
11065
11272
  // src/agents/05-test-generator/review.ts
11066
- import { readFile as readFile20 } from "fs/promises";
11067
- import { join as join31, relative as relative7, basename as basename4 } from "path";
11273
+ import { readFile as readFile21 } from "fs/promises";
11274
+ import { join as join32, relative as relative7, basename as basename4 } from "path";
11068
11275
  import "ai";
11069
11276
  import { glob as glob5 } from "glob";
11277
+ function markReview(relativePath, status) {
11278
+ getActiveStore()?.setArtifactReview(join32(TESTS_DIR, relativePath), status);
11279
+ }
11070
11280
  function mergeRubricResults(reported) {
11071
11281
  const merged = {};
11072
11282
  for (const rubric of ALL_RUBRICS) {
@@ -11086,6 +11296,7 @@ async function reviewOneTest({
11086
11296
  test,
11087
11297
  dataContract
11088
11298
  }) {
11299
+ markReview(test.relativePath, "REVIEWING");
11089
11300
  const reported = /* @__PURE__ */ new Map();
11090
11301
  await Promise.all(
11091
11302
  ALL_RUBRICS.map(async (rubric) => {
@@ -11106,35 +11317,37 @@ async function reviewOneTest({
11106
11317
  })
11107
11318
  );
11108
11319
  const dimensions = mergeRubricResults(reported);
11320
+ const failedDimensions = Object.entries(dimensions).filter(([, dim]) => !dim.pass).map(([key]) => key);
11321
+ markReview(test.relativePath, failedDimensions.length === 0 ? "REVIEWED" : "FIXING");
11109
11322
  return {
11110
11323
  relativePath: test.relativePath,
11111
11324
  content: test.content,
11112
11325
  dimensions,
11113
- failedDimensions: Object.entries(dimensions).filter(([, dim]) => !dim.pass).map(([key]) => key)
11326
+ failedDimensions
11114
11327
  };
11115
11328
  }
11116
11329
  async function readDataContract(outputDir) {
11117
11330
  const recipe = await loadRecipeContext(outputDir);
11118
11331
  if (recipe !== "") return recipe;
11119
11332
  try {
11120
- return await readFile20(join31(outputDir, "scenarios.md"), "utf-8");
11333
+ return await readFile21(join32(outputDir, "scenarios.md"), "utf-8");
11121
11334
  } catch (err) {
11122
11335
  debugLog("No data contract available for review", { err });
11123
11336
  return void 0;
11124
11337
  }
11125
11338
  }
11126
- async function runConsolidatedReview(outputDir, projectRoot, model, deadline, settled = /* @__PURE__ */ new Set(), precomputed = []) {
11127
- const testsDir = join31(outputDir, TESTS_DIR);
11339
+ async function runConsolidatedReview(outputDir, projectRoot, model, deadline, settled = /* @__PURE__ */ new Set(), precomputed = [], onProgress) {
11340
+ const testsDir = join32(outputDir, TESTS_DIR);
11128
11341
  const logger = createStepLogger("review", 5);
11129
11342
  const scenarioData = await readDataContract(outputDir);
11130
- const testFiles = await glob5(join31(testsDir, TEST_FILE_GLOB));
11343
+ const testFiles = await glob5(join32(testsDir, TEST_FILE_GLOB));
11131
11344
  const tests = [];
11132
11345
  for (const testPath of testFiles) {
11133
11346
  if (!isTestFile(testPath)) continue;
11134
11347
  if (testPath.includes("/_invalid/")) continue;
11135
11348
  const relativePath = relative7(testsDir, testPath);
11136
11349
  if (settled.has(relativePath)) continue;
11137
- const content = await readFile20(testPath, "utf-8");
11350
+ const content = await readFile21(testPath, "utf-8");
11138
11351
  const flowMatch = content.match(/^---\n[\s\S]*?flow:\s*["']?([^"'\n]+)["']?\s*\n[\s\S]*?---/m);
11139
11352
  tests.push({ path: testPath, relativePath, content, flow: flowMatch?.[1]?.trim() ?? "unknown" });
11140
11353
  }
@@ -11153,15 +11366,24 @@ async function runConsolidatedReview(outputDir, projectRoot, model, deadline, se
11153
11366
  toolErrors: [],
11154
11367
  writtenFiles: []
11155
11368
  });
11369
+ getActiveStore()?.pushActivity({ call: "review", arg: `${toReview.length} tests \xD7 ${ALL_RUBRICS.length} rubrics` });
11156
11370
  let passed = 0;
11157
11371
  let failed = 0;
11158
11372
  const feedback = [];
11159
11373
  const passedPaths = [];
11160
11374
  const byTest = /* @__PURE__ */ new Map();
11161
11375
  let reviewed = 0;
11376
+ onProgress?.(0, jobs.length);
11162
11377
  const outcome = await runPool(
11163
11378
  jobs,
11164
- { limit: REVIEW_CONCURRENCY, shouldContinue: () => Date.now() <= deadline },
11379
+ {
11380
+ limit: REVIEW_CONCURRENCY,
11381
+ shouldContinue: () => Date.now() <= deadline,
11382
+ onProgress,
11383
+ // Jobs are (test, rubric) pairs, so a test's row lights up on its
11384
+ // first dispatched rubric; the store ignores the repeats.
11385
+ onStart: (job) => markReview(job.test.relativePath, "REVIEWING")
11386
+ },
11165
11387
  (job) => runReviewPass(job.test.content, job.test.relativePath, job.rubric, projectRoot, model, scenarioData)
11166
11388
  );
11167
11389
  for (const { item, error } of outcome.failed) {
@@ -11193,8 +11415,10 @@ async function runConsolidatedReview(outputDir, projectRoot, model, deadline, se
11193
11415
  if (failedDimensions.length === 0) {
11194
11416
  passed++;
11195
11417
  passedPaths.push(test.relativePath);
11418
+ markReview(test.relativePath, "REVIEWED");
11196
11419
  } else {
11197
11420
  failed++;
11421
+ markReview(test.relativePath, "FIXING");
11198
11422
  feedback.push({
11199
11423
  testPath: test.path,
11200
11424
  relativePath: test.relativePath,
@@ -11236,6 +11460,7 @@ var init_review = __esm({
11236
11460
  init_logs();
11237
11461
  init_pool();
11238
11462
  init_test_files();
11463
+ init_store();
11239
11464
  init_recipe_context();
11240
11465
  init_review_pass();
11241
11466
  init_rubrics();
@@ -11381,7 +11606,7 @@ var init_validation = __esm({
11381
11606
  });
11382
11607
 
11383
11608
  // src/agents/05-test-generator/test-spec.ts
11384
- import { z as z55 } from "zod";
11609
+ import { z as z56 } from "zod";
11385
11610
  function hasPlaceholder(text2) {
11386
11611
  if (text2 == null) return false;
11387
11612
  return PLACEHOLDER_PATTERNS.some(({ pattern }) => pattern.test(text2));
@@ -11432,12 +11657,12 @@ var init_test_spec = __esm({
11432
11657
  { pattern: /(?<!\{)\{[a-z][a-zA-Z]*\}(?!\})/, name: "bare {variable}" },
11433
11658
  { pattern: /\be\.g\./i, name: '"e.g." example' }
11434
11659
  ];
11435
- stepSchema = z55.object({
11436
- verb: z55.enum(STEP_VERBS).describe("The action. Only these verbs exist."),
11437
- description: z55.string().min(1).describe(
11660
+ stepSchema = z56.object({
11661
+ verb: z56.enum(STEP_VERBS).describe("The action. Only these verbs exist."),
11662
+ description: z56.string().min(1).describe(
11438
11663
  `What to do or check, naming the exact visible text. For assert, the thing expected on screen - e.g. 'text "Transfer Successful"'.`
11439
11664
  ),
11440
- location: z55.string().optional().describe(
11665
+ location: z56.string().optional().describe(
11441
11666
  'WHERE on screen the target is - "in the modal", "in the toast notification", "on the Sony WH-1000XM5 product card", "in the dashboard header", "as a page heading". REQUIRED for click, type and assert: the same label routinely appears more than once (a header button and the modal button it opens, "Buy" on every card in a list), and acting on the wrong one fails confusingly. Not needed for scroll or refresh, which act on the page.'
11442
11667
  )
11443
11668
  }).refine((step) => !requiresLocation(step.verb) || (step.location?.trim() ?? "") !== "", {
@@ -11447,27 +11672,27 @@ var init_test_spec = __esm({
11447
11672
  message: "steps carry no variables - use the exact value from the test data, not a placeholder or example",
11448
11673
  path: ["description"]
11449
11674
  });
11450
- testSpecSchema = z55.object({
11451
- title: z55.string().min(1).describe("Short, descriptive test name."),
11452
- description: z55.string().min(1).describe("One sentence explaining what the test verifies."),
11453
- intent: z55.string().min(30).describe(
11675
+ testSpecSchema = z56.object({
11676
+ title: z56.string().min(1).describe("Short, descriptive test name."),
11677
+ description: z56.string().min(1).describe("One sentence explaining what the test verifies."),
11678
+ intent: z56.string().min(30).describe(
11454
11679
  "A specific, falsifiable claim derived from the node's mission: what the user does, what the feature produces, why it matters. Not the steps, not 'the page displays correctly'."
11455
11680
  ),
11456
- criticality: z55.enum(CRITICALITY_LEVELS),
11457
- scenario: z55.string().min(1).describe('Which scenario this test uses (usually "standard").'),
11458
- flow: z55.string().min(1).describe("Which feature/flow this belongs to (must match a flow from AUTONOMA.md)."),
11459
- verification: z55.string().min(20).describe(
11681
+ criticality: z56.enum(CRITICALITY_LEVELS),
11682
+ scenario: z56.string().min(1).describe('Which scenario this test uses (usually "standard").'),
11683
+ flow: z56.string().min(1).describe("Which feature/flow this belongs to (must match a flow from AUTONOMA.md)."),
11684
+ verification: z56.string().min(20).describe(
11460
11685
  "WHERE to navigate and WHAT to assert to prove the mutation worked. Must name the source of truth - a toast, a confirmation dialog or an inline success indicator is an acknowledgment, not proof."
11461
11686
  ),
11462
- setup: z55.string().min(1).describe(
11687
+ setup: z56.string().min(1).describe(
11463
11688
  "Which page the user starts on and how they got there. Never authentication - the user is already signed in."
11464
11689
  ),
11465
- steps: z55.array(stepSchema).min(1).describe("The action sequence, in order."),
11466
- verificationSteps: z55.array(stepSchema).describe(
11690
+ steps: z56.array(stepSchema).min(1).describe("The action sequence, in order."),
11691
+ verificationSteps: z56.array(stepSchema).describe(
11467
11692
  "Steps that navigate to the source of truth and assert the mutation landed. Implements the `verification` field above."
11468
11693
  ),
11469
- expectedResult: z55.string().min(1).describe("What should be true when the test passes."),
11470
- notes: z55.string().optional().describe(
11694
+ expectedResult: z56.string().min(1).describe("What should be true when the test passes."),
11695
+ notes: z56.string().optional().describe(
11471
11696
  "Optional free text for anything you could not resolve or want to flag - an ambiguous default state, a element you could not find in the source, an assumption you had to make. Recorded for humans and NEVER written into the test file, so it will not affect execution. Use it instead of guessing silently."
11472
11697
  )
11473
11698
  }).refine((spec) => spec.steps.filter((step) => isInteractionVerb(step.verb)).length >= MIN_INTERACTIONS, {
@@ -11478,25 +11703,25 @@ var init_test_spec = __esm({
11478
11703
  });
11479
11704
 
11480
11705
  // src/agents/05-test-generator/tools.ts
11481
- import { mkdir as mkdir4, writeFile as writeFile12 } from "fs/promises";
11482
- import { dirname as dirname4, join as join32 } from "path";
11706
+ import { mkdir as mkdir5, writeFile as writeFile13 } from "fs/promises";
11707
+ import { dirname as dirname4, join as join33 } from "path";
11483
11708
  import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as tool16, ToolLoopAgent as ToolLoopAgent3 } from "ai";
11484
- import { z as z56 } from "zod";
11709
+ import { z as z57 } from "zod";
11485
11710
  function buildWriteTestTool(state, outputDir, onWritten) {
11486
11711
  return tool16({
11487
11712
  description: "Write one test to qa-tests/{folder}/{filename}.md. You supply the test's parts; the file is rendered for you, so do not write markdown or frontmatter yourself.",
11488
- inputSchema: z56.object({
11489
- folder: z56.string().describe("Subfolder name under qa-tests/"),
11490
- filename: z56.string().describe(`File name ending in ${TEST_FILE_EXT} (e.g. login-valid-credentials${TEST_FILE_EXT})`),
11491
- nodeId: z56.string().describe(
11713
+ inputSchema: z57.object({
11714
+ folder: z57.string().describe("Subfolder name under qa-tests/"),
11715
+ filename: z57.string().describe(`File name ending in ${TEST_FILE_EXT} (e.g. login-valid-credentials${TEST_FILE_EXT})`),
11716
+ nodeId: z57.string().describe(
11492
11717
  "The id next_node returned for this feature, copied verbatim. Not a re-slugged version of it, not a folder path, not the test filename."
11493
11718
  ),
11494
11719
  test: testSpecSchema
11495
11720
  }),
11496
11721
  execute: async (input) => {
11497
11722
  const content = renderTestMarkdown(input.test);
11498
- const relPath = join32(TESTS_DIR, input.folder, normalizeTestFilename(input.filename));
11499
- const absPath = join32(outputDir, relPath);
11723
+ const relPath = join33(TESTS_DIR, input.folder, normalizeTestFilename(input.filename));
11724
+ const absPath = join33(outputDir, relPath);
11500
11725
  const nodeId = state.resolveNodeId(input.nodeId, relPath);
11501
11726
  if (nodeId == null) {
11502
11727
  const stats = state.summary();
@@ -11514,8 +11739,8 @@ function buildWriteTestTool(state, outputDir, onWritten) {
11514
11739
  };
11515
11740
  }
11516
11741
  try {
11517
- await mkdir4(dirname4(absPath), { recursive: true });
11518
- await writeFile12(absPath, content, "utf-8");
11742
+ await mkdir5(dirname4(absPath), { recursive: true });
11743
+ await writeFile13(absPath, content, "utf-8");
11519
11744
  state.markTested(nodeId, [relPath]);
11520
11745
  await saveBfsState(outputDir, state);
11521
11746
  const notes = input.test.notes?.trim();
@@ -11528,7 +11753,7 @@ function buildWriteTestTool(state, outputDir, onWritten) {
11528
11753
  });
11529
11754
  }
11530
11755
  onWritten?.({
11531
- relativePath: join32(input.folder, normalizeTestFilename(input.filename)),
11756
+ relativePath: join33(input.folder, normalizeTestFilename(input.filename)),
11532
11757
  content,
11533
11758
  flow: input.test.flow
11534
11759
  });
@@ -11566,14 +11791,14 @@ function buildWriteTestTool(state, outputDir, onWritten) {
11566
11791
  function buildCreateFolderTool(outputDir) {
11567
11792
  return tool16({
11568
11793
  description: "Create a folder under qa-tests/ for organizing tests.",
11569
- inputSchema: z56.object({
11570
- folder: z56.string().describe("Folder name (kebab-case)")
11794
+ inputSchema: z57.object({
11795
+ folder: z57.string().describe("Folder name (kebab-case)")
11571
11796
  }),
11572
11797
  execute: async (input) => {
11573
- const absPath = join32(outputDir, TESTS_DIR, input.folder);
11798
+ const absPath = join33(outputDir, TESTS_DIR, input.folder);
11574
11799
  try {
11575
- await mkdir4(absPath, { recursive: true });
11576
- return { path: join32(TESTS_DIR, input.folder) };
11800
+ await mkdir5(absPath, { recursive: true });
11801
+ return { path: join33(TESTS_DIR, input.folder) };
11577
11802
  } catch (err) {
11578
11803
  const message = err instanceof Error ? err.message : String(err);
11579
11804
  return { error: `Failed to create folder: ${message}` };
@@ -11584,7 +11809,7 @@ function buildCreateFolderTool(outputDir) {
11584
11809
  function buildNextNodeTool(state, outputDir) {
11585
11810
  return tool16({
11586
11811
  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.",
11587
- inputSchema: z56.object({}),
11812
+ inputSchema: z57.object({}),
11588
11813
  execute: async () => {
11589
11814
  const next = state.nextNode();
11590
11815
  await saveBfsState(outputDir, state);
@@ -11618,7 +11843,7 @@ function buildNextNodeTool(state, outputDir) {
11618
11843
  function buildGetProgressTool(state) {
11619
11844
  return tool16({
11620
11845
  description: "Check how many nodes have been tested vs how many remain.",
11621
- inputSchema: z56.object({}),
11846
+ inputSchema: z57.object({}),
11622
11847
  execute: async () => {
11623
11848
  const stats = state.summary();
11624
11849
  const nodes = [...state.nodes.values()].map((n) => ({
@@ -11634,12 +11859,12 @@ function buildGetProgressTool(state) {
11634
11859
  function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
11635
11860
  return tool16({
11636
11861
  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.",
11637
- inputSchema: z56.object({
11638
- instruction: z56.string().describe("What to research - be specific about files and what to look for")
11862
+ inputSchema: z57.object({
11863
+ instruction: z57.string().describe("What to research - be specific about files and what to look for")
11639
11864
  }),
11640
11865
  execute: async (input) => {
11641
- const resultSchema2 = z56.object({
11642
- findings: z56.string().describe("Summary of what was found")
11866
+ const resultSchema2 = z57.object({
11867
+ findings: z57.string().describe("Summary of what was found")
11643
11868
  });
11644
11869
  let result;
11645
11870
  const subagent = new ToolLoopAgent3({
@@ -11692,11 +11917,11 @@ var init_tools2 = __esm({
11692
11917
  });
11693
11918
 
11694
11919
  // src/agents/05-test-generator/write-index.ts
11695
- import { readFile as readFile21, writeFile as writeFile13 } from "fs/promises";
11696
- import { join as join33 } from "path";
11920
+ import { readFile as readFile22, writeFile as writeFile14 } from "fs/promises";
11921
+ import { join as join34 } from "path";
11697
11922
  async function scanTest(outputDir, path3) {
11698
11923
  try {
11699
- const content = await readFile21(join33(outputDir, path3), "utf-8");
11924
+ const content = await readFile22(join34(outputDir, path3), "utf-8");
11700
11925
  return {
11701
11926
  criticality: content.match(/criticality:\s*(\w+)/)?.[1] ?? "",
11702
11927
  steps: content.match(STEP_VERBS2)?.length ?? 0,
@@ -11767,7 +11992,7 @@ ${folders.map((f) => `| ${f.name} | ${f.test_count} |`).join("\n")}
11767
11992
 
11768
11993
  ${[...testsByFolder.entries()].flatMap(([_folder, tests]) => tests.map((t) => `- \`${t}\``)).join("\n")}
11769
11994
  ${renderSection("Features with no tests", "the run walked these and produced no test for them - re-run the planner to cover them", untested)}${renderSection("Tests lost in review", "the review cycle removed these and nothing could put them back - re-run the planner to regenerate them", lost)}`;
11770
- await writeFile13(join33(outputDir, TESTS_DIR, TEST_INDEX_FILE), content, "utf-8");
11995
+ await writeFile14(join34(outputDir, TESTS_DIR, TEST_INDEX_FILE), content, "utf-8");
11771
11996
  }
11772
11997
  function untestedFeatures(state) {
11773
11998
  const withTests = new Set(state.testsWritten.keys());
@@ -11802,11 +12027,11 @@ __export(test_generator_exports, {
11802
12027
  runTestGenerator: () => runTestGenerator
11803
12028
  });
11804
12029
  import { existsSync as existsSync2 } from "fs";
11805
- import { mkdir as mkdir5, readFile as readFile22, rmdir, unlink, writeFile as writeFile14 } from "fs/promises";
11806
- import { basename as basename5, join as join34 } from "path";
12030
+ import { mkdir as mkdir6, readFile as readFile23, rmdir, unlink, writeFile as writeFile15 } from "fs/promises";
12031
+ import { basename as basename5, join as join35 } from "path";
11807
12032
  import { tool as tool17 } from "ai";
11808
12033
  import { glob as glob6 } from "glob";
11809
- import { z as z57 } from "zod";
12034
+ import { z as z58 } from "zod";
11810
12035
  async function preseedQueue(state, projectRoot, pages, features) {
11811
12036
  let seeded = 0;
11812
12037
  const pageIdByPath = /* @__PURE__ */ new Map();
@@ -11858,8 +12083,8 @@ async function runTestGenerator(input) {
11858
12083
  let result;
11859
12084
  const finishTool = tool17({
11860
12085
  description: "Call when the BFS queue is empty and all routes have been explored.",
11861
- inputSchema: z57.object({
11862
- summary: z57.string().describe("Coverage summary")
12086
+ inputSchema: z58.object({
12087
+ summary: z58.string().describe("Coverage summary")
11863
12088
  }),
11864
12089
  execute: async (finishInput) => {
11865
12090
  const stats = state.summary();
@@ -11887,7 +12112,7 @@ async function runTestGenerator(input) {
11887
12112
  });
11888
12113
  let kbContext = "";
11889
12114
  try {
11890
- const autonomaMd = await readFile22(join34(input.outputDir, "AUTONOMA.md"), "utf-8");
12115
+ const autonomaMd = await readFile23(join35(input.outputDir, "AUTONOMA.md"), "utf-8");
11891
12116
  kbContext += `
11892
12117
  ## Knowledge Base (AUTONOMA.md)
11893
12118
 
@@ -11900,7 +12125,7 @@ ${autonomaMd}
11900
12125
  kbContext += recipeContext;
11901
12126
  } else {
11902
12127
  try {
11903
- const scenariosMd = await readFile22(join34(input.outputDir, "scenarios.md"), "utf-8");
12128
+ const scenariosMd = await readFile23(join35(input.outputDir, "scenarios.md"), "utf-8");
11904
12129
  kbContext += `
11905
12130
  ## Scenarios
11906
12131
 
@@ -12092,13 +12317,15 @@ IMPORTANT: Do NOT try to finish early. Process every node via next_node until it
12092
12317
  console.log(` Review cycle ${cycle + 1}/${MAX_REVIEW_CYCLES}`);
12093
12318
  state.setPhase(`review cycle ${cycle + 1}/${MAX_REVIEW_CYCLES}`);
12094
12319
  const scanDeadline = Date.now() + Math.floor((reviewDeadline - Date.now()) * REVIEW_SCAN_SHARE);
12320
+ const reviewStartedAt = activeAgentNow() ?? Date.now();
12095
12321
  const reviewResult = await runConsolidatedReview(
12096
12322
  input.outputDir,
12097
12323
  input.projectRoot,
12098
12324
  model,
12099
12325
  scanDeadline,
12100
12326
  settled,
12101
- cycle === 0 ? pipelined : []
12327
+ cycle === 0 ? pipelined : [],
12328
+ (done, total) => state.setReviewProgress(done, total, reviewStartedAt)
12102
12329
  );
12103
12330
  for (const path3 of reviewResult.passedPaths) settled.add(path3);
12104
12331
  console.log(
@@ -12194,21 +12421,22 @@ IMPORTANT: Do NOT try to finish early. Process every node via next_node until it
12194
12421
  console.log(` Fix pass complete`);
12195
12422
  if (scanCutShort) break;
12196
12423
  }
12424
+ state.clearReviewProgress();
12197
12425
  state.setPhase("checking every test");
12198
- const allTestFiles = await glob6(join34(input.outputDir, TESTS_DIR, TEST_FILE_GLOB));
12426
+ const allTestFiles = await glob6(join35(input.outputDir, TESTS_DIR, TEST_FILE_GLOB));
12199
12427
  let markedInvalid = 0;
12200
12428
  for (const testPath of allTestFiles) {
12201
12429
  if (!isTestFile(testPath)) continue;
12202
12430
  if (testPath.includes(`/${INVALID_DIR}/`)) continue;
12203
- const content = await readFile22(testPath, "utf-8");
12431
+ const content = await readFile23(testPath, "utf-8");
12204
12432
  const validation = validateTestContent(content);
12205
12433
  if (!validation.valid) {
12206
- const invalidDir = join34(input.outputDir, TESTS_DIR, INVALID_DIR);
12207
- await mkdir5(invalidDir, { recursive: true });
12208
- const dest = join34(invalidDir, basename5(testPath));
12434
+ const invalidDir = join35(input.outputDir, TESTS_DIR, INVALID_DIR);
12435
+ await mkdir6(invalidDir, { recursive: true });
12436
+ const dest = join35(invalidDir, basename5(testPath));
12209
12437
  const annotated = `<!-- VALIDATION ERRORS: ${validation.errors.join("; ")} -->
12210
12438
  ${content}`;
12211
- await writeFile14(dest, annotated, "utf-8");
12439
+ await writeFile15(dest, annotated, "utf-8");
12212
12440
  await unlink(testPath);
12213
12441
  markedInvalid++;
12214
12442
  }
@@ -12216,7 +12444,7 @@ ${content}`;
12216
12444
  if (markedInvalid > 0) {
12217
12445
  console.log(` ${markedInvalid} tests still invalid after review cycles - moved to _invalid/`);
12218
12446
  }
12219
- const dirs = await glob6(join34(input.outputDir, TESTS_DIR, "**/"), {
12447
+ const dirs = await glob6(join35(input.outputDir, TESTS_DIR, "**/"), {
12220
12448
  dot: false
12221
12449
  });
12222
12450
  for (const dir of dirs.sort((a, b) => b.length - a.length)) {
@@ -12269,21 +12497,21 @@ async function generateJourneyTests(outputDir, model, projectRoot) {
12269
12497
  let autonomaMd = "";
12270
12498
  let scenariosMd = "";
12271
12499
  try {
12272
- autonomaMd = await readFile22(join34(outputDir, "AUTONOMA.md"), "utf-8");
12500
+ autonomaMd = await readFile23(join35(outputDir, "AUTONOMA.md"), "utf-8");
12273
12501
  } catch (err) {
12274
12502
  debugLog("AUTONOMA.md not present for journey generation", { err });
12275
12503
  }
12276
12504
  try {
12277
- scenariosMd = await readFile22(join34(outputDir, "scenarios.md"), "utf-8");
12505
+ scenariosMd = await readFile23(join35(outputDir, "scenarios.md"), "utf-8");
12278
12506
  } catch (err) {
12279
12507
  debugLog("scenarios.md not present for journey generation", { err });
12280
12508
  }
12281
12509
  if (!autonomaMd) return 0;
12282
- const existingTests = await glob6(join34(outputDir, TESTS_DIR, TEST_FILE_GLOB));
12510
+ const existingTests = await glob6(join35(outputDir, TESTS_DIR, TEST_FILE_GLOB));
12283
12511
  const existingTitles = [];
12284
12512
  for (const t of existingTests) {
12285
12513
  if (!isTestFile(t)) continue;
12286
- const content = await readFile22(t, "utf-8");
12514
+ const content = await readFile23(t, "utf-8");
12287
12515
  const titleMatch = content.match(/title:\s*"([^"]+)"/);
12288
12516
  if (titleMatch) existingTitles.push(titleMatch[1]);
12289
12517
  }
@@ -12329,7 +12557,7 @@ Write 5-8 journey tests using the write_test tool with folder "${JOURNEY_NODE_ID
12329
12557
  let journeyResult;
12330
12558
  const journeyFinish = tool17({
12331
12559
  description: "Signal journey generation is complete.",
12332
- inputSchema: z57.object({ summary: z57.string() }),
12560
+ inputSchema: z58.object({ summary: z58.string() }),
12333
12561
  execute: async (finishInput) => {
12334
12562
  journeyResult = {
12335
12563
  success: true,
@@ -12377,6 +12605,7 @@ var init_test_generator = __esm({
12377
12605
  init_gitignore();
12378
12606
  init_logs();
12379
12607
  init_model();
12608
+ init_progress();
12380
12609
  init_test_files();
12381
12610
  init_tools();
12382
12611
  init_b_feature_discovery();
@@ -12488,7 +12717,7 @@ var init_ansi_to_rows = __esm({
12488
12717
 
12489
12718
  // src/replay/types.ts
12490
12719
  var NODE_TYPE;
12491
- var init_types = __esm({
12720
+ var init_types2 = __esm({
12492
12721
  "src/replay/types.ts"() {
12493
12722
  "use strict";
12494
12723
  init_esm_shims();
@@ -12572,7 +12801,7 @@ var init_frame_differ = __esm({
12572
12801
  "use strict";
12573
12802
  init_esm_shims();
12574
12803
  init_ansi_to_rows();
12575
- init_types();
12804
+ init_types2();
12576
12805
  DOCUMENT_ID = 1;
12577
12806
  DOCTYPE_ID = 2;
12578
12807
  HTML_ID = 3;
@@ -13199,7 +13428,7 @@ function drawWelcomeModal(g, state) {
13199
13428
  for (let yy = y; yy < y + h; yy++) g.fillBg(x, yy, w, MODAL_FILL5);
13200
13429
  const bg = MODAL_FILL5;
13201
13430
  let cy = y + 1;
13202
- g.text(x + 4, cy, "\u25C6 WELCOME TO AUTONOMA", { color: theme.accent, bold: true, bg });
13431
+ g.text(x + 4, cy, `\u25C6 ${wel.eyebrow ?? DEFAULT_EYEBROW}`, { color: theme.accent, bold: true, bg });
13203
13432
  cy += 2;
13204
13433
  for (const line of titleLines) {
13205
13434
  g.text(x + 4, cy, line, { color: theme.text, bold: true, bg });
@@ -13213,7 +13442,7 @@ function drawWelcomeModal(g, state) {
13213
13442
  const ly = y + h - 2;
13214
13443
  g.text(x + 4, ly, wel.cta, { color: theme.accent, bold: true, bg });
13215
13444
  }
13216
- var MODAL_MAX_W5, MODAL_FILL5;
13445
+ var MODAL_MAX_W5, MODAL_FILL5, DEFAULT_EYEBROW;
13217
13446
  var init_welcome = __esm({
13218
13447
  "src/ui/draw/welcome.ts"() {
13219
13448
  "use strict";
@@ -13222,6 +13451,7 @@ var init_welcome = __esm({
13222
13451
  init_wrap();
13223
13452
  MODAL_MAX_W5 = 84;
13224
13453
  MODAL_FILL5 = "#141414";
13454
+ DEFAULT_EYEBROW = "WELCOME TO AUTONOMA";
13225
13455
  }
13226
13456
  });
13227
13457
 
@@ -13307,6 +13537,14 @@ function artifactStatusCell(a, now) {
13307
13537
  const frame = SPINNER_FRAMES2[Math.floor(now / WRITING_SPINNER_MS) % SPINNER_FRAMES2.length];
13308
13538
  return { t: `${frame} WRITING`, color: theme.sky };
13309
13539
  }
13540
+ // Deliberately no spinner on REVIEWING: 16 review agents run at once, so
13541
+ // a spinner per row would set most of the list twitching at 5fps.
13542
+ case "REVIEWING":
13543
+ return { t: "REVIEWING", color: theme.violet };
13544
+ case "REVIEWED":
13545
+ return { t: "\u2713 REVIEWED", color: theme.green };
13546
+ case "FIXING":
13547
+ return { t: "FIXING", color: theme.amber };
13310
13548
  default:
13311
13549
  return { t: "PENDING", color: theme.faint };
13312
13550
  }
@@ -13337,7 +13575,8 @@ function drawFiles(g, geo, state, base, bottom) {
13337
13575
  g.text(2, row, icon, { color: theme.tertiary, bg });
13338
13576
  const label = a.title ?? a.name;
13339
13577
  const detail = a.title != null ? [a.name, a.description].filter((s) => s != null).join(" \xB7 ") : a.description;
13340
- const nameColor = highlighted ? theme.accent : active2 || a.status === "DONE" ? theme.text : theme.secondary;
13578
+ const written = a.status !== "PENDING";
13579
+ const nameColor = highlighted ? theme.accent : written ? theme.text : theme.secondary;
13341
13580
  const nameMax = geo.div - 2 - STATUS_CELL_W - 2 - 5;
13342
13581
  const shown = label.length > nameMax ? label.slice(0, Math.max(1, nameMax - 1)) + "\u2026" : label;
13343
13582
  g.text(5, row, shown, { color: nameColor, bold: highlighted || active2, bg });
@@ -13461,6 +13700,7 @@ function callColor(call) {
13461
13700
  if (call === "done" || call === "success") return theme.green;
13462
13701
  if (call === "warn" || call === "checkpoint") return theme.amber;
13463
13702
  if (call === "error") return theme.red;
13703
+ if (call === "review") return theme.violet;
13464
13704
  return theme.secondary;
13465
13705
  }
13466
13706
  function drawActivity(g, state, top, h) {
@@ -13502,7 +13742,8 @@ function drawDashboard(g, state) {
13502
13742
  drawActivity(g, state, panelBottom, activityRows);
13503
13743
  const exitHint = state.ctrlCArmed ? { k: "Ctrl+C", label: "again to exit (progress saved)", color: theme.red } : { k: "^C ^C", label: "exit", color: theme.red };
13504
13744
  if (state.welcome != null) {
13505
- drawHints(g, H - 1, [{ k: "enter", label: "begin", color: theme.accent }], [exitHint]);
13745
+ const label = state.welcome.eyebrow == null ? "begin" : "continue";
13746
+ drawHints(g, H - 1, [{ k: "enter", label, color: theme.accent }], [exitHint]);
13506
13747
  drawWelcomeModal(g, state);
13507
13748
  return;
13508
13749
  }
@@ -13658,6 +13899,27 @@ var init_grid = __esm({
13658
13899
  this.h = h;
13659
13900
  this.cells = Array.from({ length: h }, () => Array.from({ length: w }, () => ({ ch: " " })));
13660
13901
  }
13902
+ /**
13903
+ * Reset every cell to a blank space, reusing the existing `cells` arrays.
13904
+ *
13905
+ * Allocation is the leak: a 100x30 terminal allocates 3000 Cell objects
13906
+ * per render, and at 10 renders/second (the clock tick) that is
13907
+ * ~30k objects/second of short-lived garbage. V8's GC runs constantly
13908
+ * to reclaim it, and the GC pauses block the event loop, starving the
13909
+ * pipeline. Clearing in place avoids the allocation entirely.
13910
+ */
13911
+ clear() {
13912
+ for (let y = 0; y < this.h; y++) {
13913
+ const row = this.cells[y];
13914
+ for (let x = 0; x < this.w; x++) {
13915
+ const c = row[x];
13916
+ c.ch = " ";
13917
+ c.color = void 0;
13918
+ c.bg = void 0;
13919
+ c.bold = void 0;
13920
+ }
13921
+ }
13922
+ }
13661
13923
  set(x, y, ch, st = {}) {
13662
13924
  if (y < 0 || y >= this.h || x < 0 || x >= this.w) return;
13663
13925
  const safe = ch < " " ? " " : ch;
@@ -13731,23 +13993,50 @@ var init_grid = __esm({
13731
13993
  * Flatten into per-row ANSI strings (24-bit color escapes). One string per
13732
13994
  * row keeps the Ink tree tiny - rendering thousands of nested <Text> spans
13733
13995
  * per frame is what makes large dashboards crawl.
13996
+ *
13997
+ * Builds the string directly from cells, skipping the intermediate Seg[][]
13998
+ * that `rows()` produces. The Seg array was allocated per render and
13999
+ * immediately discarded - avoiding it cuts another ~600 short-lived objects
14000
+ * per frame.
13734
14001
  */
13735
14002
  ansiRows() {
13736
- return this.rows().map((segs) => {
13737
- let out = "";
13738
- for (const s of segs) {
13739
- const codes = [];
13740
- if (s.bold) codes.push("1");
13741
- if (s.color != null) codes.push(fgCode(s.color));
13742
- if (s.bg != null) codes.push(bgCode(s.bg));
13743
- if (codes.length === 0) {
13744
- out += s.text;
14003
+ const out = [];
14004
+ for (let y = 0; y < this.h; y++) {
14005
+ const row = this.cells[y];
14006
+ let line = "";
14007
+ let curCh = "";
14008
+ let curColor;
14009
+ let curBg;
14010
+ let curBold = false;
14011
+ let hasStyle = false;
14012
+ const flush2 = () => {
14013
+ if (!hasStyle) {
14014
+ line += curCh;
13745
14015
  } else {
13746
- out += `\x1B[${codes.join(";")}m${s.text}\x1B[0m`;
14016
+ const codes = [];
14017
+ if (curBold) codes.push("1");
14018
+ if (curColor != null) codes.push(fgCode(curColor));
14019
+ if (curBg != null) codes.push(bgCode(curBg));
14020
+ line += `\x1B[${codes.join(";")}m${curCh}\x1B[0m`;
14021
+ }
14022
+ curCh = "";
14023
+ };
14024
+ for (let x = 0; x < this.w; x++) {
14025
+ const c = row[x];
14026
+ const cBold = !!c.bold;
14027
+ if (c.color !== curColor || c.bg !== curBg || cBold !== curBold) {
14028
+ if (curCh !== "") flush2();
14029
+ curColor = c.color;
14030
+ curBg = c.bg;
14031
+ curBold = cBold;
14032
+ hasStyle = curColor != null || curBg != null || curBold;
13747
14033
  }
14034
+ curCh += c.ch;
13748
14035
  }
13749
- return out;
13750
- });
14036
+ if (curCh !== "") flush2();
14037
+ out.push(line);
14038
+ }
14039
+ return out;
13751
14040
  }
13752
14041
  /** Flatten into per-row spans, merging adjacent cells with identical style. */
13753
14042
  rows() {
@@ -13775,9 +14064,15 @@ var init_grid = __esm({
13775
14064
 
13776
14065
  // src/ui/screens/Dashboard.tsx
13777
14066
  import { Box, Text } from "ink";
14067
+ import { useRef } from "react";
13778
14068
  import { jsx } from "react/jsx-runtime";
13779
14069
  function Dashboard({ state, width, rows }) {
13780
- const g = new Grid(width, rows);
14070
+ const gridRef = useRef(void 0);
14071
+ if (gridRef.current == null || gridRef.current.w !== width || gridRef.current.h !== rows) {
14072
+ gridRef.current = new Grid(width, rows);
14073
+ }
14074
+ const g = gridRef.current;
14075
+ g.clear();
13781
14076
  drawDashboard(g, state);
13782
14077
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", width, height: rows, children: g.ansiRows().map((row, y) => /* @__PURE__ */ jsx(Text, { children: row }, y)) });
13783
14078
  }
@@ -14109,14 +14404,14 @@ var init_session_recorder = __esm({
14109
14404
  });
14110
14405
 
14111
14406
  // src/ui/artifacts/reader.ts
14112
- import { open, readFile as readFile23, stat as stat2 } from "fs/promises";
14407
+ import { open, readFile as readFile24, stat as stat2 } from "fs/promises";
14113
14408
  async function readForLive(absPath) {
14114
14409
  try {
14115
14410
  const info = await stat2(absPath);
14116
14411
  if (!info.isFile()) return void 0;
14117
14412
  const kind = kindOf(absPath);
14118
14413
  if (info.size <= LARGE_BYTES) {
14119
- const text2 = await readFile23(absPath, "utf-8");
14414
+ const text2 = await readFile24(absPath, "utf-8");
14120
14415
  return { text: text2, kind, truncated: false };
14121
14416
  }
14122
14417
  const fh = await open(absPath, "r");
@@ -14410,8 +14705,8 @@ ensureSupportedNode();
14410
14705
 
14411
14706
  // src/index.ts
14412
14707
  init_submit();
14413
- import { readFile as readFile24, writeFile as writeFile15 } from "fs/promises";
14414
- import { join as join35 } from "path";
14708
+ import { readFile as readFile25, writeFile as writeFile16 } from "fs/promises";
14709
+ import { join as join36 } from "path";
14415
14710
 
14416
14711
  // src/agents/04-recipe-builder/sdk-command.ts
14417
14712
  init_esm_shims();
@@ -15499,49 +15794,7 @@ function wrap(text2, width) {
15499
15794
  init_interrupt();
15500
15795
  init_logs();
15501
15796
  init_model();
15502
-
15503
- // src/core/output.ts
15504
- init_esm_shims();
15505
- init_debug();
15506
- import { mkdir, rm, writeFile as writeFile2 } from "fs/promises";
15507
- import { homedir as homedir3 } from "os";
15508
- import { join as join10 } from "path";
15509
- var AUTONOMA_HOME3 = join10(homedir3(), ".autonoma");
15510
- var WRITE_PROBE_FILE = ".write-probe";
15511
- function getOutputDir(projectSlug) {
15512
- return join10(AUTONOMA_HOME3, projectSlug);
15513
- }
15514
- async function clearOutputDir(projectSlug) {
15515
- const dir = getOutputDir(projectSlug);
15516
- await rm(dir, { recursive: true, force: true });
15517
- await mkdir(dir, { recursive: true });
15518
- debugLog("Cleared the run directory for a fresh start", { dir });
15519
- }
15520
- function displayPath(absPath) {
15521
- const home = homedir3();
15522
- return absPath.startsWith(home) ? `~${absPath.slice(home.length)}` : absPath;
15523
- }
15524
- async function ensureOutputDir(projectSlug) {
15525
- const dir = getOutputDir(projectSlug);
15526
- await mkdir(dir, { recursive: true });
15527
- const probe = join10(dir, WRITE_PROBE_FILE);
15528
- try {
15529
- await writeFile2(probe, "ok", "utf-8");
15530
- } catch (err) {
15531
- const reason = err instanceof Error ? err.message : String(err);
15532
- throw new Error(
15533
- `Cannot write to ${displayPath(dir)} - the planner keeps every artifact it generates there.
15534
- ${reason}
15535
- Free up disk space or fix the directory's permissions, then re-run.`
15536
- );
15537
- }
15538
- await rm(probe, { force: true }).catch((err) => {
15539
- debugLog("Could not remove the write probe", { probe, err });
15540
- });
15541
- return dir;
15542
- }
15543
-
15544
- // src/index.ts
15797
+ init_output();
15545
15798
  init_session();
15546
15799
  init_ui_lifecycle();
15547
15800
  init_env();
@@ -15550,8 +15803,8 @@ init_prompts();
15550
15803
  // src/core/git.ts
15551
15804
  init_esm_shims();
15552
15805
  import { execFile } from "child_process";
15553
- import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
15554
- import { join as join11 } from "path";
15806
+ import { readFile as readFile5, writeFile as writeFile4 } from "fs/promises";
15807
+ import { join as join12 } from "path";
15555
15808
  import { promisify } from "util";
15556
15809
  var execFileAsync = promisify(execFile);
15557
15810
  var GIT_INFO_FILE = ".git-info.json";
@@ -15575,11 +15828,11 @@ async function readGitInfo(projectRoot) {
15575
15828
  };
15576
15829
  }
15577
15830
  async function saveGitInfo(outputDir, info) {
15578
- await writeFile3(join11(outputDir, GIT_INFO_FILE), JSON.stringify(info, null, 2), "utf-8");
15831
+ await writeFile4(join12(outputDir, GIT_INFO_FILE), JSON.stringify(info, null, 2), "utf-8");
15579
15832
  }
15580
15833
  async function loadGitInfo(outputDir) {
15581
15834
  try {
15582
- const raw = await readFile4(join11(outputDir, GIT_INFO_FILE), "utf-8");
15835
+ const raw = await readFile5(join12(outputDir, GIT_INFO_FILE), "utf-8");
15583
15836
  const parsed = JSON.parse(raw);
15584
15837
  if (typeof parsed === "object" && parsed != null && "sha" in parsed && typeof parsed.sha === "string") {
15585
15838
  const branch = "branch" in parsed && typeof parsed.branch === "string" ? parsed.branch : void 0;
@@ -15601,15 +15854,15 @@ init_state();
15601
15854
  init_esm_shims();
15602
15855
  init_prompts();
15603
15856
  init_debug();
15604
- import { readFile as readFile6 } from "fs/promises";
15605
- import { join as join13 } from "path";
15857
+ import { readFile as readFile7 } from "fs/promises";
15858
+ import { join as join14 } from "path";
15606
15859
  init_test_files();
15607
15860
  var ARTIFACT_FILES = ["AUTONOMA.md", "scenarios.md", "entity-audit.md"];
15608
15861
  async function readArtifacts(outputDir) {
15609
15862
  const files = await Promise.all(
15610
15863
  ARTIFACT_FILES.map(async (name) => {
15611
15864
  try {
15612
- return { name, content: await readFile6(join13(outputDir, name), "utf-8") };
15865
+ return { name, content: await readFile7(join14(outputDir, name), "utf-8") };
15613
15866
  } catch (err) {
15614
15867
  debugLog(`Artifact ${name} not on disk; skipping upload`, { err });
15615
15868
  return void 0;
@@ -15625,7 +15878,7 @@ async function readTestCases(outputDir) {
15625
15878
  const segments = testPath.split("/").slice(1);
15626
15879
  const name = segments[segments.length - 1];
15627
15880
  const folder = segments.slice(0, -1).join("/");
15628
- const content = await readFile6(join13(outputDir, testPath), "utf-8");
15881
+ const content = await readFile7(join14(outputDir, testPath), "utf-8");
15629
15882
  return { name, content, folder: folder.length > 0 ? folder : void 0 };
15630
15883
  })
15631
15884
  );
@@ -15690,11 +15943,11 @@ process.setSourceMapsEnabled(true);
15690
15943
  var PAGES_FILE = "pages.json";
15691
15944
  async function savePages(outputDir, pages) {
15692
15945
  const obj = Object.fromEntries(pages);
15693
- await writeFile15(join35(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
15946
+ await writeFile16(join36(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
15694
15947
  }
15695
15948
  async function loadPages(outputDir) {
15696
15949
  try {
15697
- const raw = await readFile24(join35(outputDir, PAGES_FILE), "utf-8");
15950
+ const raw = await readFile25(join36(outputDir, PAGES_FILE), "utf-8");
15698
15951
  const obj = JSON.parse(raw);
15699
15952
  return new Map(Object.entries(obj));
15700
15953
  } catch {
@@ -15745,14 +15998,32 @@ async function resolveScopeSelection(map, config, nonInteractive) {
15745
15998
  return pickDefaultSelection(map);
15746
15999
  }
15747
16000
  async function promptScopeSelection(map) {
16001
+ const onlyFrontend = map.frontends.length === 1 ? map.frontends[0] : void 0;
16002
+ const onlyBackend = map.backends.length === 1 ? map.backends[0] : void 0;
16003
+ if (onlyFrontend != null) {
16004
+ log.info(`Planning tests for ${onlyFrontend.path} [${onlyFrontend.framework}] - the only frontend found.`);
16005
+ if (map.backends.length === 0) return { frontend: onlyFrontend.path, backends: [] };
16006
+ if (onlyBackend != null) {
16007
+ log.info(`Using ${onlyBackend.path} - the only backend found.`);
16008
+ return { frontend: onlyFrontend.path, backends: [onlyBackend.path] };
16009
+ }
16010
+ }
15748
16011
  while (true) {
15749
- const frontend = await select({
16012
+ const frontend = onlyFrontend?.path ?? await select({
15750
16013
  message: "Which frontend do you want to plan tests for?",
15751
16014
  detail: "One frontend per run - the whole test suite is planned against it.",
15752
- options: map.frontends.map((f) => ({ value: f.path, label: `${f.path} [${f.framework}]`, hint: f.why }))
16015
+ options: map.frontends.map((f) => ({
16016
+ value: f.path,
16017
+ label: `${f.path} [${f.framework}]`,
16018
+ hint: f.why
16019
+ }))
15753
16020
  });
15754
16021
  if (isCancel(frontend)) continue;
15755
16022
  if (map.backends.length === 0) return { frontend, backends: [] };
16023
+ if (onlyBackend != null) {
16024
+ log.info(`Using ${onlyBackend.path} - the only backend found.`);
16025
+ return { frontend, backends: [onlyBackend.path] };
16026
+ }
15756
16027
  const needed = defaultBackendsFor(map, frontend);
15757
16028
  const backends = await multiselect({
15758
16029
  message: "Which backends does this frontend talk to?",
@@ -16117,6 +16388,7 @@ async function main() {
16117
16388
  const projectArg = args.value("project");
16118
16389
  const resumeCommand = `autonoma-planner --resume` + (projectArg != null ? ` --project ${projectArg}` : "");
16119
16390
  let mountedUi;
16391
+ let stoppedAt;
16120
16392
  installInterruptHandler({
16121
16393
  // exitCode defaults to 0 for a user-initiated Ctrl+C (progress saved, a clean stop);
16122
16394
  // an external SIGTERM/SIGHUP passes the conventional 143/129 so the flushed exit still
@@ -16290,9 +16562,11 @@ All of it runs from this one command - there is nothing to launch step by step.`
16290
16562
  const step = steps[i];
16291
16563
  state = await runStepWithRecovery(step, outputDir, state, config, nonInteractive);
16292
16564
  if (state.steps[step] === "paused") {
16565
+ stoppedAt = { label: STEP_LABELS[step], kind: "paused" };
16293
16566
  break;
16294
16567
  }
16295
16568
  if (state.steps[step] === "failed") {
16569
+ stoppedAt = { label: STEP_LABELS[step], kind: "failed" };
16296
16570
  log.error("Pipeline stopped due to failure.");
16297
16571
  log.warn(`Your progress is saved. To retry this step, run:
16298
16572
  ${resumeCommand}`);
@@ -16352,6 +16626,11 @@ All of it runs from this one command - there is nothing to launch step by step.`
16352
16626
  log.info(`Saved in ${displayPath(outputDir)}`);
16353
16627
  if (finish != null) for (const line of describeFinishPhase(finish)) log.info(line);
16354
16628
  log.info(nextStepLine(finish, nonInteractive));
16629
+ } else if (stoppedAt != null) {
16630
+ const verb = stoppedAt.kind === "paused" ? "paused at" : "stopped at";
16631
+ log.warn(`The run ${verb} "${stoppedAt.label}" and did not finish.`);
16632
+ log.info(`Your progress is saved. Continue with:
16633
+ ${resumeCommand}`);
16355
16634
  }
16356
16635
  outro("Done");
16357
16636
  }