@autonoma-ai/planner 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist/index.js +852 -434
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -14,11 +14,11 @@ var __export = (target, all) => {
|
|
|
14
14
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
// ../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.15.3_jiti@2.6.1_postcss@8.5.15_tsx@4.21.0_typescript@
|
|
17
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.15.3_jiti@2.6.1_postcss@8.5.15_tsx@4.21.0_typescript@7.0.2_yaml@2.8.3/node_modules/tsup/assets/esm_shims.js
|
|
18
18
|
import path from "path";
|
|
19
19
|
import { fileURLToPath } from "url";
|
|
20
20
|
var init_esm_shims = __esm({
|
|
21
|
-
"../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.15.3_jiti@2.6.1_postcss@8.5.15_tsx@4.21.0_typescript@
|
|
21
|
+
"../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.15.3_jiti@2.6.1_postcss@8.5.15_tsx@4.21.0_typescript@7.0.2_yaml@2.8.3/node_modules/tsup/assets/esm_shims.js"() {
|
|
22
22
|
"use strict";
|
|
23
23
|
}
|
|
24
24
|
});
|
|
@@ -199,7 +199,7 @@ function trackError(error, properties = {}, handled = true) {
|
|
|
199
199
|
}
|
|
200
200
|
async function flushAnalytics(timeoutMs = 1500) {
|
|
201
201
|
if (pending.size === 0) return;
|
|
202
|
-
await Promise.race([Promise.allSettled([...pending]), new Promise((
|
|
202
|
+
await Promise.race([Promise.allSettled([...pending]), new Promise((resolve6) => setTimeout(resolve6, timeoutMs))]);
|
|
203
203
|
}
|
|
204
204
|
var AUTONOMA_HOME2, DEVICE_ID_PATH, POSTHOG_PUBLIC_KEY, DEFAULT_HOST, RUN_ID, cachedDeviceId, enabled, pending;
|
|
205
205
|
var init_analytics = __esm({
|
|
@@ -218,6 +218,29 @@ var init_analytics = __esm({
|
|
|
218
218
|
}
|
|
219
219
|
});
|
|
220
220
|
|
|
221
|
+
// src/core/colors.ts
|
|
222
|
+
function fg(hex) {
|
|
223
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
224
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
225
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
226
|
+
return `${ESC}38;2;${r};${g};${b}m`;
|
|
227
|
+
}
|
|
228
|
+
var ESC, RESET, BOLD, DIM, PRIMARY, SUCCESS, ERROR, WARNING;
|
|
229
|
+
var init_colors = __esm({
|
|
230
|
+
"src/core/colors.ts"() {
|
|
231
|
+
"use strict";
|
|
232
|
+
init_esm_shims();
|
|
233
|
+
ESC = "\x1B[";
|
|
234
|
+
RESET = `${ESC}0m`;
|
|
235
|
+
BOLD = `${ESC}1m`;
|
|
236
|
+
DIM = `${ESC}2m`;
|
|
237
|
+
PRIMARY = fg("#CCFF00");
|
|
238
|
+
SUCCESS = fg("#3FB950");
|
|
239
|
+
ERROR = fg("#FF5C5C");
|
|
240
|
+
WARNING = fg("#FFB020");
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
221
244
|
// src/core/context.ts
|
|
222
245
|
import { readFile, writeFile } from "fs/promises";
|
|
223
246
|
import { join as join5 } from "path";
|
|
@@ -265,7 +288,7 @@ var init_context = __esm({
|
|
|
265
288
|
// src/core/errors.ts
|
|
266
289
|
import { APICallError, RetryError, LoadAPIKeyError, InvalidPromptError, NoSuchModelError } from "ai";
|
|
267
290
|
function sleep(ms) {
|
|
268
|
-
return new Promise((
|
|
291
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
269
292
|
}
|
|
270
293
|
function isUserCancellation(err) {
|
|
271
294
|
return err instanceof Error && /\bcancell?ed\b/i.test(err.message);
|
|
@@ -469,6 +492,151 @@ var init_notify = __esm({
|
|
|
469
492
|
}
|
|
470
493
|
});
|
|
471
494
|
|
|
495
|
+
// src/core/project-map.ts
|
|
496
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
497
|
+
import { join as join8 } from "path";
|
|
498
|
+
import { z as z2 } from "zod";
|
|
499
|
+
async function saveProjectMap(outputDir, map) {
|
|
500
|
+
await writeFile3(join8(outputDir, PROJECT_MAP_FILE), JSON.stringify(map, null, 2), "utf-8");
|
|
501
|
+
}
|
|
502
|
+
async function loadProjectMap(outputDir) {
|
|
503
|
+
const path3 = join8(outputDir, PROJECT_MAP_FILE);
|
|
504
|
+
try {
|
|
505
|
+
const raw = await readFile3(path3, "utf-8");
|
|
506
|
+
const parsed = ProjectMapSchema.safeParse(JSON.parse(raw));
|
|
507
|
+
if (parsed.success) return parsed.data;
|
|
508
|
+
debugLog("project-map.json failed schema validation, ignoring it", { path: path3, issues: parsed.error.issues });
|
|
509
|
+
return void 0;
|
|
510
|
+
} catch (err) {
|
|
511
|
+
const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
512
|
+
if (!isMissingFile) debugLog("Failed to read project-map.json, ignoring it", { path: path3, err });
|
|
513
|
+
return void 0;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
function renderProjectMap(map) {
|
|
517
|
+
const section = (title, rows, fmt) => {
|
|
518
|
+
if (rows.length === 0) return `${title}: (none)`;
|
|
519
|
+
return `${title}:
|
|
520
|
+
` + rows.map((r) => ` - ${fmt(r)}`).join("\n");
|
|
521
|
+
};
|
|
522
|
+
return [
|
|
523
|
+
section("Frontend(s)", map.frontends, (f) => {
|
|
524
|
+
const deps = f.dependsOn.length > 0 ? ` needs: ${f.dependsOn.join(", ")}` : "";
|
|
525
|
+
return `${f.path} [${f.framework}]${deps} - ${f.why}`;
|
|
526
|
+
}),
|
|
527
|
+
section("Backend(s)", map.backends, (b) => {
|
|
528
|
+
const dl = b.dataLayer != null ? ` data: ${b.dataLayer.kind}${b.dataLayer.schemaPath != null ? ` @ ${b.dataLayer.schemaPath}` : ""}` : "";
|
|
529
|
+
return `${b.path} [${b.language}/${b.framework}]${dl} - ${b.why}`;
|
|
530
|
+
}),
|
|
531
|
+
section("Ignoring", map.ignore, (i) => `${i.path} - ${i.why}`)
|
|
532
|
+
].join("\n\n");
|
|
533
|
+
}
|
|
534
|
+
function formatFrontendScope(map) {
|
|
535
|
+
if (map.frontends.length === 0) return void 0;
|
|
536
|
+
const fronts = map.frontends.map((f) => f.path).join(", ");
|
|
537
|
+
const ignore = map.ignore.map((i) => i.path);
|
|
538
|
+
const ignoreLine = ignore.length > 0 ? ` Do NOT spend time in these irrelevant directories: ${ignore.join(", ")}.` : "";
|
|
539
|
+
return `The project has already been mapped. The frontend surface to focus on is: ${fronts}. Confine your exploration to that surface.${ignoreLine}`;
|
|
540
|
+
}
|
|
541
|
+
function relatedPath(a, b) {
|
|
542
|
+
return a === b || a.startsWith(`${b}/`) || b.startsWith(`${a}/`);
|
|
543
|
+
}
|
|
544
|
+
function matchMapPath(requested, candidates) {
|
|
545
|
+
if (candidates.includes(requested)) return requested;
|
|
546
|
+
return candidates.find((c) => relatedPath(c, requested));
|
|
547
|
+
}
|
|
548
|
+
function matchBackend(requested, backends) {
|
|
549
|
+
const byPath = backends.find((b) => relatedPath(b.path, requested));
|
|
550
|
+
if (byPath != null) return byPath.path;
|
|
551
|
+
const bySchema = backends.find(
|
|
552
|
+
(b) => b.dataLayer?.schemaPath != null && relatedPath(b.dataLayer.schemaPath, requested)
|
|
553
|
+
);
|
|
554
|
+
return bySchema?.path;
|
|
555
|
+
}
|
|
556
|
+
function resolveSelection(map, requested) {
|
|
557
|
+
const frontendPaths = map.frontends.map((f) => f.path);
|
|
558
|
+
return {
|
|
559
|
+
frontend: matchMapPath(requested.frontend, frontendPaths) ?? requested.frontend,
|
|
560
|
+
backends: requested.backends.map((b) => matchBackend(b, map.backends) ?? b)
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function defaultBackendsFor(map, frontendPath) {
|
|
564
|
+
const declared = map.frontends.find((f) => f.path === frontendPath)?.dependsOn ?? [];
|
|
565
|
+
const resolved = declared.map((dep) => matchBackend(dep, map.backends)).filter((p10) => p10 != null);
|
|
566
|
+
return [...new Set(resolved)];
|
|
567
|
+
}
|
|
568
|
+
function pickDefaultSelection(map) {
|
|
569
|
+
if (map.frontends.length !== 1) return void 0;
|
|
570
|
+
const [only] = map.frontends;
|
|
571
|
+
if (only == null) return void 0;
|
|
572
|
+
return { frontend: only.path, backends: defaultBackendsFor(map, only.path) };
|
|
573
|
+
}
|
|
574
|
+
function applySelection(map, selection) {
|
|
575
|
+
const frontend = map.frontends.find((f) => f.path === selection.frontend);
|
|
576
|
+
if (frontend == null) {
|
|
577
|
+
const options = map.frontends.map((f) => f.path).join(", ") || "(none)";
|
|
578
|
+
throw new Error(`Selected frontend "${selection.frontend}" is not in the map. Candidates: ${options}`);
|
|
579
|
+
}
|
|
580
|
+
const chosenBackends = map.backends.filter((b) => selection.backends.includes(b.path));
|
|
581
|
+
const missing = selection.backends.filter((p10) => !map.backends.some((b) => b.path === p10));
|
|
582
|
+
if (missing.length > 0) {
|
|
583
|
+
const options = map.backends.map((b) => b.path).join(", ") || "(none)";
|
|
584
|
+
throw new Error(`Selected backend(s) not in the map: ${missing.join(", ")}. Candidates: ${options}`);
|
|
585
|
+
}
|
|
586
|
+
const droppedFrontends = map.frontends.filter((f) => f.path !== selection.frontend).map((f) => ({ path: f.path, why: "Another frontend in the monorepo; not the app under test." }));
|
|
587
|
+
const droppedBackends = map.backends.filter((b) => !selection.backends.includes(b.path)).map((b) => ({ path: b.path, why: "Backend not required by the selected frontend." }));
|
|
588
|
+
return {
|
|
589
|
+
frontends: [frontend],
|
|
590
|
+
backends: chosenBackends,
|
|
591
|
+
ignore: [...map.ignore, ...droppedFrontends, ...droppedBackends]
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function formatBackendScope(map) {
|
|
595
|
+
if (map.backends.length === 0) return void 0;
|
|
596
|
+
const backs = map.backends.map((b) => b.dataLayer?.schemaPath != null ? `${b.path} (models @ ${b.dataLayer.schemaPath})` : b.path).join(", ");
|
|
597
|
+
const ignore = map.ignore.map((i) => i.path);
|
|
598
|
+
const ignoreLine = ignore.length > 0 ? ` Ignore these irrelevant directories: ${ignore.join(", ")}.` : "";
|
|
599
|
+
return `The project has already been mapped. The backend(s)/data layer(s) that own the models to seed are: ${backs}. Scope your data-model work to those backend(s).${ignoreLine}`;
|
|
600
|
+
}
|
|
601
|
+
var FrontendEntry, BackendEntry, IgnoreEntry, ProjectMapSchema, PROJECT_MAP_FILE;
|
|
602
|
+
var init_project_map = __esm({
|
|
603
|
+
"src/core/project-map.ts"() {
|
|
604
|
+
"use strict";
|
|
605
|
+
init_esm_shims();
|
|
606
|
+
init_debug();
|
|
607
|
+
FrontendEntry = z2.object({
|
|
608
|
+
path: z2.string().min(1).describe("Repo-relative path to the frontend app/directory (the UI surface)."),
|
|
609
|
+
framework: z2.string().describe("Detected UI framework/stack, or 'unknown' if not obvious (e.g. next, react, vue, svelte)."),
|
|
610
|
+
dependsOn: z2.array(z2.string()).describe(
|
|
611
|
+
"Repo-relative paths (each matching a backend in this map) that THIS frontend needs in order to function - the API/service(s) it calls and the data layer(s) that own the records it renders. These are pre-selected when the user picks this frontend, so only list backends this frontend actually depends on. Empty if it needs none."
|
|
612
|
+
),
|
|
613
|
+
why: z2.string().min(1).describe("One line: the evidence that made you classify this as a frontend.")
|
|
614
|
+
});
|
|
615
|
+
BackendEntry = z2.object({
|
|
616
|
+
path: z2.string().min(1).describe("Repo-relative path to the backend/API/service or the package that owns the data layer."),
|
|
617
|
+
language: z2.string().describe("Primary language, or 'unknown' (e.g. typescript, python, go, rust)."),
|
|
618
|
+
framework: z2.string().describe("Web/service framework or ORM stack, or 'unknown' (e.g. express, hono, fastapi, rails)."),
|
|
619
|
+
dataLayer: z2.object({
|
|
620
|
+
kind: z2.string().describe("How models are defined (e.g. prisma, drizzle, sqlalchemy, typeorm, raw-sql, unknown)."),
|
|
621
|
+
schemaPath: z2.string().optional().describe("Repo-relative path to the schema/models definition, if found.")
|
|
622
|
+
}).optional().describe("Where this backend's database models live. Omit if this backend owns no data layer."),
|
|
623
|
+
why: z2.string().min(1).describe("One line: the evidence that made you classify this as a backend.")
|
|
624
|
+
});
|
|
625
|
+
IgnoreEntry = z2.object({
|
|
626
|
+
path: z2.string().min(1).describe("Repo-relative path to a directory that is NOT relevant to testing this app."),
|
|
627
|
+
why: z2.string().min(1).describe("One line: why it is irrelevant (e.g. docs site, infra, tooling, examples, unrelated app).")
|
|
628
|
+
});
|
|
629
|
+
ProjectMapSchema = z2.object({
|
|
630
|
+
frontends: z2.array(FrontendEntry).describe("The UI surface(s) whose pages/flows get tested. Usually 1; may be more."),
|
|
631
|
+
backends: z2.array(BackendEntry).describe(
|
|
632
|
+
"The API/service(s) and data layer(s) that own the models we seed. May be 0 (frontend-only), 1, or many."
|
|
633
|
+
),
|
|
634
|
+
ignore: z2.array(IgnoreEntry).describe("Directories judged irrelevant to this app's tests, so later steps skip them.")
|
|
635
|
+
});
|
|
636
|
+
PROJECT_MAP_FILE = "project-map.json";
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
472
640
|
// src/core/display.ts
|
|
473
641
|
function formatArgs(input, keys) {
|
|
474
642
|
const parts = [];
|
|
@@ -533,7 +701,7 @@ function createStepLogger(agentId, _maxSteps) {
|
|
|
533
701
|
function writeSpinner(message) {
|
|
534
702
|
const frame = SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length];
|
|
535
703
|
frameIdx++;
|
|
536
|
-
process.stderr.write(`${CLEAR_LINE} ${
|
|
704
|
+
process.stderr.write(`${CLEAR_LINE} ${DIM}${frame} ${message}${RESET}`);
|
|
537
705
|
lastSpinnerLine = true;
|
|
538
706
|
}
|
|
539
707
|
function writePermanent(message) {
|
|
@@ -564,55 +732,49 @@ function createStepLogger(agentId, _maxSteps) {
|
|
|
564
732
|
case "write_file": {
|
|
565
733
|
stats.filesWritten++;
|
|
566
734
|
const path3 = String(tc.input.path ?? tc.input.file_path ?? "");
|
|
567
|
-
writePermanent(` ${
|
|
735
|
+
writePermanent(` ${SUCCESS}\u270E write ${path3}${RESET}`);
|
|
568
736
|
break;
|
|
569
737
|
}
|
|
570
738
|
case "write_test":
|
|
571
739
|
stats.filesWritten++;
|
|
572
|
-
writePermanent(` ${
|
|
740
|
+
writePermanent(` ${SUCCESS}\u270E test ${summary2}${RESET}`);
|
|
573
741
|
break;
|
|
574
742
|
case "finish":
|
|
575
|
-
writePermanent(` ${
|
|
743
|
+
writePermanent(` ${SUCCESS}${BOLD}\u2713 done:${RESET} ${SUCCESS}${agentId}${RESET}`);
|
|
576
744
|
break;
|
|
577
745
|
case "subagent":
|
|
578
746
|
case "spawn_researcher":
|
|
579
|
-
writePermanent(` ${
|
|
747
|
+
writePermanent(` ${PRIMARY}\u2295 subagent: ${summary2}${RESET}`);
|
|
580
748
|
break;
|
|
581
749
|
default:
|
|
582
750
|
writeSpinner(`${tc.name}${summary2 ? " " + summary2 : ""}`);
|
|
583
751
|
}
|
|
584
752
|
}
|
|
585
753
|
for (const te of info.toolErrors) {
|
|
586
|
-
writePermanent(` ${
|
|
754
|
+
writePermanent(` ${ERROR}\u2717 ${te.name}: ${te.error}${RESET}`);
|
|
587
755
|
}
|
|
588
756
|
for (const f of info.writtenFiles) {
|
|
589
|
-
writePermanent(` ${
|
|
757
|
+
writePermanent(` ${SUCCESS}\u{1F4C4} wrote: ${f}${RESET}`);
|
|
590
758
|
}
|
|
591
759
|
}
|
|
592
760
|
function checkpoint(message) {
|
|
593
|
-
writePermanent(` ${
|
|
761
|
+
writePermanent(` ${WARNING}\u25B8 ${message}${RESET}`);
|
|
594
762
|
}
|
|
595
763
|
function summary() {
|
|
596
764
|
clearSpinner();
|
|
597
765
|
if (stats.filesRead > 0 || stats.filesWritten > 0) {
|
|
598
|
-
console.log(` ${
|
|
599
|
-
console.log(` ${
|
|
766
|
+
console.log(` ${DIM}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET}`);
|
|
767
|
+
console.log(` ${DIM}files read: ${stats.filesRead} | files written: ${stats.filesWritten}${RESET}`);
|
|
600
768
|
}
|
|
601
769
|
}
|
|
602
770
|
return { log: log9, checkpoint, summary, stats };
|
|
603
771
|
}
|
|
604
|
-
var
|
|
772
|
+
var SPINNER_FRAMES, CLEAR_LINE;
|
|
605
773
|
var init_display = __esm({
|
|
606
774
|
"src/core/display.ts"() {
|
|
607
775
|
"use strict";
|
|
608
776
|
init_esm_shims();
|
|
609
|
-
|
|
610
|
-
RESET2 = "\x1B[0m";
|
|
611
|
-
CYAN = "\x1B[36m";
|
|
612
|
-
GREEN = "\x1B[32m";
|
|
613
|
-
RED = "\x1B[31m";
|
|
614
|
-
YELLOW = "\x1B[33m";
|
|
615
|
-
BOLD = "\x1B[1m";
|
|
777
|
+
init_colors();
|
|
616
778
|
SPINNER_FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
|
|
617
779
|
CLEAR_LINE = "\x1B[2K\r";
|
|
618
780
|
}
|
|
@@ -674,7 +836,7 @@ async function runAgent(config, prompt, extractResult) {
|
|
|
674
836
|
const msg = err instanceof Error ? err.message : String(err);
|
|
675
837
|
throw new AgentError(`agent "${config.id}" (model ${modelIdOf(model)}) failed: ${msg}`, config.id, err);
|
|
676
838
|
};
|
|
677
|
-
const
|
|
839
|
+
const YELLOW3 = "\x1B[33m";
|
|
678
840
|
const RESET8 = "\x1B[0m";
|
|
679
841
|
for (let retry = 0; retry < MAX_RETRIES; retry++) {
|
|
680
842
|
const heartbeat = () => {
|
|
@@ -699,7 +861,7 @@ async function runAgent(config, prompt, extractResult) {
|
|
|
699
861
|
while (extractResult() === void 0 && nudges < MAX_NUDGES) {
|
|
700
862
|
nudges++;
|
|
701
863
|
console.log(
|
|
702
|
-
` ${
|
|
864
|
+
` ${YELLOW3}[${config.id}] agent stopped without finishing - nudging (${nudges}/${MAX_NUDGES})...${RESET8}`
|
|
703
865
|
);
|
|
704
866
|
track("cli_agent_nudged", { agent: config.id, nudge: nudges });
|
|
705
867
|
messages.push(...generation.response.messages);
|
|
@@ -715,13 +877,13 @@ async function runAgent(config, prompt, extractResult) {
|
|
|
715
877
|
if (errorClass === "fatal") fail(err);
|
|
716
878
|
const msg = err instanceof Error ? err.message : String(err);
|
|
717
879
|
if (errorClass === "timeout") {
|
|
718
|
-
console.log(` ${
|
|
880
|
+
console.log(` ${YELLOW3}[${config.id}] step timed out after ${stepTimeout / 1e3}s${RESET8}`);
|
|
719
881
|
} else {
|
|
720
|
-
console.log(` ${
|
|
882
|
+
console.log(` ${YELLOW3}[${config.id}] provider error: ${msg}${RESET8}`);
|
|
721
883
|
}
|
|
722
884
|
track("cli_agent_retryable_error", { agent: config.id, error_class: errorClass, retry });
|
|
723
885
|
if (retry < MAX_RETRIES - 1) {
|
|
724
|
-
console.log(` ${
|
|
886
|
+
console.log(` ${YELLOW3}[${config.id}] retrying (${retry + 1}/${MAX_RETRIES})...${RESET8}`);
|
|
725
887
|
if (errorClass === "transient") {
|
|
726
888
|
await sleep(Math.min(2e3 * 2 ** retry, 1e4));
|
|
727
889
|
}
|
|
@@ -749,8 +911,8 @@ var init_agent = __esm({
|
|
|
749
911
|
});
|
|
750
912
|
|
|
751
913
|
// src/core/gitignore.ts
|
|
752
|
-
import { readFile as
|
|
753
|
-
import { join as
|
|
914
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
915
|
+
import { join as join11, relative as relative2 } from "path";
|
|
754
916
|
import { glob as glob2 } from "glob";
|
|
755
917
|
async function loadGitignorePatterns(projectRoot) {
|
|
756
918
|
const patterns = [
|
|
@@ -770,10 +932,10 @@ async function loadGitignorePatterns(projectRoot) {
|
|
|
770
932
|
];
|
|
771
933
|
const matches = await glob2("**/.gitignore", { cwd: projectRoot, dot: true });
|
|
772
934
|
for (const match of matches) {
|
|
773
|
-
const fullPath =
|
|
935
|
+
const fullPath = join11(projectRoot, match);
|
|
774
936
|
try {
|
|
775
|
-
const content = await
|
|
776
|
-
const prefix = relative2(projectRoot,
|
|
937
|
+
const content = await readFile6(fullPath, "utf-8");
|
|
938
|
+
const prefix = relative2(projectRoot, join11(projectRoot, match, ".."));
|
|
777
939
|
const parsed = parseGitignore(content, prefix);
|
|
778
940
|
patterns.push(...parsed);
|
|
779
941
|
} catch (err) {
|
|
@@ -836,7 +998,7 @@ var init_exec_error = __esm({
|
|
|
836
998
|
import { execFile as execFile3 } from "child_process";
|
|
837
999
|
import { promisify as promisify2 } from "util";
|
|
838
1000
|
import { tool } from "ai";
|
|
839
|
-
import { z as
|
|
1001
|
+
import { z as z4 } from "zod";
|
|
840
1002
|
function validateCommand(command, allowed) {
|
|
841
1003
|
const trimmed = command.trim();
|
|
842
1004
|
if (trimmed.length === 0) return "Empty command";
|
|
@@ -902,8 +1064,8 @@ var init_bash = __esm({
|
|
|
902
1064
|
DEFAULT_ALLOWED = /* @__PURE__ */ new Set(["git", "wc", "sort", "head", "tail", "cat", "ls", "find", "diff", "echo"]);
|
|
903
1065
|
TIMEOUT_MS = 3e4;
|
|
904
1066
|
MAX_OUTPUT_BYTES = 1024 * 512;
|
|
905
|
-
inputSchema =
|
|
906
|
-
command:
|
|
1067
|
+
inputSchema = z4.object({
|
|
1068
|
+
command: z4.string().describe("Shell command to execute")
|
|
907
1069
|
});
|
|
908
1070
|
}
|
|
909
1071
|
});
|
|
@@ -911,7 +1073,7 @@ var init_bash = __esm({
|
|
|
911
1073
|
// src/tools/glob.ts
|
|
912
1074
|
import { tool as tool2 } from "ai";
|
|
913
1075
|
import { glob as glob3 } from "glob";
|
|
914
|
-
import { z as
|
|
1076
|
+
import { z as z5 } from "zod";
|
|
915
1077
|
async function executeGlob(pattern, cwd, ignorePatterns = DEFAULT_IGNORE) {
|
|
916
1078
|
try {
|
|
917
1079
|
const matches = await glob3(pattern, {
|
|
@@ -938,9 +1100,9 @@ var init_glob = __esm({
|
|
|
938
1100
|
"src/tools/glob.ts"() {
|
|
939
1101
|
"use strict";
|
|
940
1102
|
init_esm_shims();
|
|
941
|
-
inputSchema2 =
|
|
942
|
-
pattern:
|
|
943
|
-
cwd:
|
|
1103
|
+
inputSchema2 = z5.object({
|
|
1104
|
+
pattern: z5.string().describe("Glob pattern to match files (e.g. '**/*.ts', 'src/**/*.py')"),
|
|
1105
|
+
cwd: z5.string().optional().describe("Directory to search in. Defaults to working directory.")
|
|
944
1106
|
});
|
|
945
1107
|
DEFAULT_IGNORE = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
|
|
946
1108
|
}
|
|
@@ -950,7 +1112,7 @@ var init_glob = __esm({
|
|
|
950
1112
|
import { execFile as execFile4 } from "child_process";
|
|
951
1113
|
import { promisify as promisify3 } from "util";
|
|
952
1114
|
import { tool as tool3 } from "ai";
|
|
953
|
-
import { z as
|
|
1115
|
+
import { z as z6 } from "zod";
|
|
954
1116
|
function buildGrepTool(workingDirectory) {
|
|
955
1117
|
return tool3({
|
|
956
1118
|
description: "Search file contents with ripgrep. Returns matching lines with file paths and line numbers.",
|
|
@@ -994,10 +1156,10 @@ var init_grep = __esm({
|
|
|
994
1156
|
init_esm_shims();
|
|
995
1157
|
init_exec_error();
|
|
996
1158
|
execFileAsync3 = promisify3(execFile4);
|
|
997
|
-
inputSchema3 =
|
|
998
|
-
pattern:
|
|
999
|
-
glob:
|
|
1000
|
-
path:
|
|
1159
|
+
inputSchema3 = z6.object({
|
|
1160
|
+
pattern: z6.string().describe("Regex pattern to search for in file contents"),
|
|
1161
|
+
glob: z6.string().optional().describe("Glob to filter files (e.g. '*.ts')"),
|
|
1162
|
+
path: z6.string().optional().describe("File or directory to search in")
|
|
1001
1163
|
});
|
|
1002
1164
|
}
|
|
1003
1165
|
});
|
|
@@ -1005,10 +1167,10 @@ var init_grep = __esm({
|
|
|
1005
1167
|
// src/tools/list-directory.ts
|
|
1006
1168
|
import { readdir } from "fs/promises";
|
|
1007
1169
|
import { stat } from "fs/promises";
|
|
1008
|
-
import { join as
|
|
1170
|
+
import { join as join12, relative as relative3 } from "path";
|
|
1009
1171
|
import { tool as tool4 } from "ai";
|
|
1010
1172
|
import { minimatch } from "minimatch";
|
|
1011
|
-
import { z as
|
|
1173
|
+
import { z as z7 } from "zod";
|
|
1012
1174
|
function buildMatcher(patterns) {
|
|
1013
1175
|
const positive = patterns.filter((p10) => !p10.startsWith("!"));
|
|
1014
1176
|
const negative = patterns.filter((p10) => p10.startsWith("!")).map((p10) => p10.slice(1));
|
|
@@ -1030,7 +1192,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
|
|
|
1030
1192
|
const withTypes = [];
|
|
1031
1193
|
for (const name of rawEntries) {
|
|
1032
1194
|
try {
|
|
1033
|
-
const s = await stat(
|
|
1195
|
+
const s = await stat(join12(dirPath, name));
|
|
1034
1196
|
withTypes.push({ name, isDir: s.isDirectory() });
|
|
1035
1197
|
} catch {
|
|
1036
1198
|
withTypes.push({ name, isDir: false });
|
|
@@ -1050,7 +1212,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
|
|
|
1050
1212
|
}
|
|
1051
1213
|
if (entry.isDir) {
|
|
1052
1214
|
const children = await buildTree(
|
|
1053
|
-
|
|
1215
|
+
join12(dirPath, entry.name),
|
|
1054
1216
|
maxDepth,
|
|
1055
1217
|
currentDepth + 1,
|
|
1056
1218
|
isIgnored,
|
|
@@ -1088,10 +1250,10 @@ async function buildListDirectoryTool(workingDirectory) {
|
|
|
1088
1250
|
const isIgnored = buildMatcher(patterns);
|
|
1089
1251
|
return tool4({
|
|
1090
1252
|
description: "List directory structure as a tree. Use this for an overview of the project layout. Start at the root (path='.') with depth 3, then increase depth or narrow path if needed. Do NOT call this on every subdirectory - use glob to find specific files instead. Returns cached result if the same path+depth was already requested.",
|
|
1091
|
-
inputSchema:
|
|
1092
|
-
path:
|
|
1093
|
-
depth:
|
|
1094
|
-
gitignore:
|
|
1253
|
+
inputSchema: z7.object({
|
|
1254
|
+
path: z7.string().default(".").describe("Directory path relative to project root. Defaults to root."),
|
|
1255
|
+
depth: z7.number().min(1).max(15).default(10).describe("Max depth to traverse (1-15). Default 10."),
|
|
1256
|
+
gitignore: z7.boolean().describe(
|
|
1095
1257
|
"Whether to respect the gitignore or to ignore it. true will respect it. false will ignore it. Default true"
|
|
1096
1258
|
).default(true)
|
|
1097
1259
|
}),
|
|
@@ -1103,7 +1265,7 @@ async function buildListDirectoryTool(workingDirectory) {
|
|
|
1103
1265
|
};
|
|
1104
1266
|
}
|
|
1105
1267
|
seen.add(cacheKey);
|
|
1106
|
-
const targetDir = input.path === "." ? workingDirectory :
|
|
1268
|
+
const targetDir = input.path === "." ? workingDirectory : join12(workingDirectory, input.path);
|
|
1107
1269
|
try {
|
|
1108
1270
|
const s = await stat(targetDir);
|
|
1109
1271
|
if (!s.isDirectory()) {
|
|
@@ -1135,10 +1297,10 @@ var init_list_directory = __esm({
|
|
|
1135
1297
|
});
|
|
1136
1298
|
|
|
1137
1299
|
// src/tools/read-file.ts
|
|
1138
|
-
import { readFile as
|
|
1300
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1139
1301
|
import { relative as relative4, resolve as resolve2 } from "path";
|
|
1140
1302
|
import { tool as tool5 } from "ai";
|
|
1141
|
-
import { z as
|
|
1303
|
+
import { z as z8 } from "zod";
|
|
1142
1304
|
function resolveSandboxedPath(workingDirectory, filePath) {
|
|
1143
1305
|
const absolutePath = resolve2(workingDirectory, filePath);
|
|
1144
1306
|
const relativePath = relative4(workingDirectory, absolutePath);
|
|
@@ -1163,7 +1325,7 @@ async function executeReadFile(workingDirectory, filePath, offset, limit) {
|
|
|
1163
1325
|
const resolved = resolveSandboxedPath(workingDirectory, filePath);
|
|
1164
1326
|
if ("error" in resolved) return resolved;
|
|
1165
1327
|
try {
|
|
1166
|
-
const content = await
|
|
1328
|
+
const content = await readFile7(resolved.absolutePath, "utf-8");
|
|
1167
1329
|
const sliced = sliceLines(content, offset ?? 0, limit ?? MAX_LINES);
|
|
1168
1330
|
return {
|
|
1169
1331
|
path: resolved.relativePath,
|
|
@@ -1191,10 +1353,10 @@ var init_read_file = __esm({
|
|
|
1191
1353
|
"use strict";
|
|
1192
1354
|
init_esm_shims();
|
|
1193
1355
|
MAX_LINES = 2e3;
|
|
1194
|
-
inputSchema4 =
|
|
1195
|
-
filePath:
|
|
1196
|
-
offset:
|
|
1197
|
-
limit:
|
|
1356
|
+
inputSchema4 = z8.object({
|
|
1357
|
+
filePath: z8.string().describe("Path to the file (absolute or relative to working directory)"),
|
|
1358
|
+
offset: z8.number().int().min(0).optional().describe("Line number to start reading from (0-based)"),
|
|
1359
|
+
limit: z8.number().int().min(1).optional().describe("Maximum number of lines to read")
|
|
1198
1360
|
});
|
|
1199
1361
|
}
|
|
1200
1362
|
});
|
|
@@ -1217,10 +1379,10 @@ var init_pick_string = __esm({
|
|
|
1217
1379
|
|
|
1218
1380
|
// src/tools/subagent.ts
|
|
1219
1381
|
import { ToolLoopAgent as ToolLoopAgent2, hasToolCall as hasToolCall2, stepCountIs as stepCountIs2, tool as tool6 } from "ai";
|
|
1220
|
-
import { z as
|
|
1382
|
+
import { z as z9 } from "zod";
|
|
1221
1383
|
function buildSubagentTools(workingDirectory, onFileRead) {
|
|
1222
1384
|
const baseReadFile = buildReadFileTool(workingDirectory);
|
|
1223
|
-
const
|
|
1385
|
+
const readFile24 = onFileRead ? tool6({
|
|
1224
1386
|
description: baseReadFile.description,
|
|
1225
1387
|
inputSchema: baseReadFile.inputSchema,
|
|
1226
1388
|
execute: async (input, options) => {
|
|
@@ -1233,7 +1395,7 @@ function buildSubagentTools(workingDirectory, onFileRead) {
|
|
|
1233
1395
|
bash: buildBashTool(workingDirectory),
|
|
1234
1396
|
glob: buildGlobTool(workingDirectory),
|
|
1235
1397
|
grep: buildGrepTool(workingDirectory),
|
|
1236
|
-
read_file:
|
|
1398
|
+
read_file: readFile24
|
|
1237
1399
|
};
|
|
1238
1400
|
}
|
|
1239
1401
|
function buildSubagentTool(model, workingDirectory, onHeartbeat, onFileRead) {
|
|
@@ -1284,11 +1446,11 @@ var init_subagent = __esm({
|
|
|
1284
1446
|
init_glob();
|
|
1285
1447
|
init_grep();
|
|
1286
1448
|
init_read_file();
|
|
1287
|
-
inputSchema5 =
|
|
1288
|
-
instruction:
|
|
1449
|
+
inputSchema5 = z9.object({
|
|
1450
|
+
instruction: z9.string().describe("Focused task for the subagent. Be specific about files and patterns to investigate.")
|
|
1289
1451
|
});
|
|
1290
|
-
resultSchema =
|
|
1291
|
-
findings:
|
|
1452
|
+
resultSchema = z9.object({
|
|
1453
|
+
findings: z9.string().describe("Summary of what was found")
|
|
1292
1454
|
});
|
|
1293
1455
|
SYSTEM_PROMPT = `You are a code research assistant. You have tools to explore a codebase: bash (shell commands, mainly git), glob (find files), grep (search content), and read_file (read files).
|
|
1294
1456
|
|
|
@@ -1299,10 +1461,10 @@ Be thorough but focused - only investigate what's relevant to your instruction.`
|
|
|
1299
1461
|
});
|
|
1300
1462
|
|
|
1301
1463
|
// src/tools/write-file.ts
|
|
1302
|
-
import { writeFile as
|
|
1464
|
+
import { writeFile as writeFile5, mkdir as mkdir2 } from "fs/promises";
|
|
1303
1465
|
import { dirname as dirname2, relative as relative5, resolve as resolve3 } from "path";
|
|
1304
1466
|
import { tool as tool7 } from "ai";
|
|
1305
|
-
import { z as
|
|
1467
|
+
import { z as z10 } from "zod";
|
|
1306
1468
|
async function executeWriteFile(outputDirectory, filePath, content) {
|
|
1307
1469
|
const cleaned = filePath.replace(/^autonoma\//, "");
|
|
1308
1470
|
const absolutePath = resolve3(outputDirectory, cleaned);
|
|
@@ -1312,7 +1474,7 @@ async function executeWriteFile(outputDirectory, filePath, content) {
|
|
|
1312
1474
|
}
|
|
1313
1475
|
try {
|
|
1314
1476
|
await mkdir2(dirname2(absolutePath), { recursive: true });
|
|
1315
|
-
await
|
|
1477
|
+
await writeFile5(absolutePath, content, "utf-8");
|
|
1316
1478
|
return { path: relativePath, bytesWritten: content.length };
|
|
1317
1479
|
} catch (err) {
|
|
1318
1480
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1331,9 +1493,9 @@ var init_write_file = __esm({
|
|
|
1331
1493
|
"src/tools/write-file.ts"() {
|
|
1332
1494
|
"use strict";
|
|
1333
1495
|
init_esm_shims();
|
|
1334
|
-
inputSchema6 =
|
|
1335
|
-
filePath:
|
|
1336
|
-
content:
|
|
1496
|
+
inputSchema6 = z10.object({
|
|
1497
|
+
filePath: z10.string().describe("Path to write (absolute or relative to output directory)"),
|
|
1498
|
+
content: z10.string().describe("File content to write")
|
|
1337
1499
|
});
|
|
1338
1500
|
}
|
|
1339
1501
|
});
|
|
@@ -1341,16 +1503,21 @@ var init_write_file = __esm({
|
|
|
1341
1503
|
// src/tools/ask-user.ts
|
|
1342
1504
|
import * as p2 from "@clack/prompts";
|
|
1343
1505
|
import { tool as tool8 } from "ai";
|
|
1344
|
-
import { z as
|
|
1506
|
+
import { z as z11 } from "zod";
|
|
1345
1507
|
function buildAskUserTool() {
|
|
1346
1508
|
return tool8({
|
|
1347
1509
|
description: "Ask the user a question ONLY when the answer is truly unknowable from the codebase. Valid reasons: untyped JSON/JSONB field schemas, business rules not in code, config values not in source. NEVER ask about: field names (read the schema), field types (read the ORM model), enum values (read the code), relationships (read foreign keys), numeric values (read the seed data or defaults). If you can find it by reading a file, DO NOT ask - read the file instead.",
|
|
1348
|
-
inputSchema:
|
|
1349
|
-
question:
|
|
1510
|
+
inputSchema: z11.object({
|
|
1511
|
+
question: z11.string().describe(
|
|
1350
1512
|
"A clear, plain-language question. State exactly what you need to know and why you can't find it in code. BAD: 'What are the decimal values for checking_balance?' - GOOD: 'Your Account model has a metadata JSON column with no type definition. What fields go inside it?'"
|
|
1351
1513
|
)
|
|
1352
1514
|
}),
|
|
1353
1515
|
execute: async (input) => {
|
|
1516
|
+
if (!process.stdin.isTTY) {
|
|
1517
|
+
return {
|
|
1518
|
+
answer: "No interactive user is available (non-interactive run). Do not ask again - infer the answer by reading the relevant model/schema/service files in the codebase and proceed with your best judgment."
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1354
1521
|
const answer = await p2.text({ message: input.question });
|
|
1355
1522
|
if (p2.isCancel(answer)) return { answer: "User skipped this question" };
|
|
1356
1523
|
return { answer };
|
|
@@ -1394,6 +1561,58 @@ var init_tools = __esm({
|
|
|
1394
1561
|
}
|
|
1395
1562
|
});
|
|
1396
1563
|
|
|
1564
|
+
// src/agents/00-project-mapper/index.ts
|
|
1565
|
+
var project_mapper_exports = {};
|
|
1566
|
+
__export(project_mapper_exports, {
|
|
1567
|
+
runProjectMapper: () => runProjectMapper
|
|
1568
|
+
});
|
|
1569
|
+
import { tool as tool9 } from "ai";
|
|
1570
|
+
async function runProjectMapper(input) {
|
|
1571
|
+
const model = getModel(input.modelId);
|
|
1572
|
+
const { logger, onStepFinish } = buildDefaultStepLogger("project-map", MAX_STEPS);
|
|
1573
|
+
let captured;
|
|
1574
|
+
const prompt = `Map the codebase rooted at ${input.projectRoot}.
|
|
1575
|
+
|
|
1576
|
+
Explore the layout and dependency manifests, then enumerate EVERY candidate frontend and EVERY candidate backend/data-layer, and for each frontend record the backends it depends on. List everything genuinely irrelevant under ignore. Do not prune to a single app - a human picks the one to test afterward. Then call set_project_map and finish.`;
|
|
1577
|
+
const agentConfig = {
|
|
1578
|
+
id: "project-mapper",
|
|
1579
|
+
systemPrompt: SYSTEM_PROMPT2,
|
|
1580
|
+
model,
|
|
1581
|
+
maxSteps: MAX_STEPS,
|
|
1582
|
+
tools: async (heartbeat) => {
|
|
1583
|
+
const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
|
|
1584
|
+
return {
|
|
1585
|
+
...tools,
|
|
1586
|
+
set_project_map: tool9({
|
|
1587
|
+
description: "Record the final project map: the frontend app(s), the backend(s)/data layer(s), and the directories to ignore. Call this once you are confident in the partition, then call finish.",
|
|
1588
|
+
inputSchema: ProjectMapSchema,
|
|
1589
|
+
execute: (map) => {
|
|
1590
|
+
captured = map;
|
|
1591
|
+
return `Recorded: ${map.frontends.length} frontend(s), ${map.backends.length} backend(s), ${map.ignore.length} ignored. Now call finish.`;
|
|
1592
|
+
}
|
|
1593
|
+
})
|
|
1594
|
+
};
|
|
1595
|
+
},
|
|
1596
|
+
onStepFinish
|
|
1597
|
+
};
|
|
1598
|
+
await runAgent(agentConfig, prompt, () => captured);
|
|
1599
|
+
logger.summary();
|
|
1600
|
+
return captured;
|
|
1601
|
+
}
|
|
1602
|
+
var MAX_STEPS, SYSTEM_PROMPT2;
|
|
1603
|
+
var init_project_mapper = __esm({
|
|
1604
|
+
"src/agents/00-project-mapper/index.ts"() {
|
|
1605
|
+
"use strict";
|
|
1606
|
+
init_esm_shims();
|
|
1607
|
+
init_agent();
|
|
1608
|
+
init_model();
|
|
1609
|
+
init_project_map();
|
|
1610
|
+
init_tools();
|
|
1611
|
+
MAX_STEPS = 60;
|
|
1612
|
+
SYSTEM_PROMPT2 = "You map a codebase into three groups so the rest of the test-planning pipeline knows exactly what to look at and what to skip: FRONTENDS (the UI surfaces whose pages and flows could be tested), BACKENDS (the API/service and the data layer that owns the database models we would seed test data into), and IGNORE (everything irrelevant to testing this product).\n\nYou are a DISCOVERY step, not a decision step. The pipeline tests ONE frontend at a time, but YOU do not choose which - a human (or the agent driving you) picks afterward from what you found. So enumerate EVERY candidate frontend and EVERY candidate backend you can justify; do not prune down to a single app. Pruning happens at selection, using the dependency edges you record.\n\nDiscover the structure - never assume it. Read package/dependency manifests, config files, and the directory layout, then reason from evidence:\n- A FRONTEND renders a user interface (pages/routes/views, a UI framework or bundler, browser entry points).\n- A BACKEND serves an API and/or owns the data layer (a database schema, ORM models, migrations, server routes).\n- IGNORE is the rest: infra, build tooling, examples, generated code, and packages that are neither a UI nor a service/data-layer.\n\nFor EACH frontend, record `dependsOn`: the repo-relative paths (each must be one of the backends you list) that that frontend actually needs in order to work - the API/service(s) it calls and the data layer(s) that own the records it renders. Infer these from evidence: the frontend's dependency manifest, the API clients/SDKs it imports, GraphQL/REST endpoints or gateway URLs it points at, and shared data-layer packages it reads. When the user selects that frontend, its `dependsOn` backends are pre-checked, so be accurate: list the backends it truly needs, not every backend in the repo.\n\nHandle every shape without special-casing any framework:\n- A single fullstack app can be BOTH a frontend and a backend at the SAME path - list that path under both, and put its own path in its `dependsOn`.\n- A monorepo can hold many apps and packages - list every genuine frontend and backend; only truly irrelevant directories go in IGNORE.\n- There may be MANY backends (a main API, separate services, a shared data-layer package, a gateway plus the services behind it) - list each one, and wire each frontend's `dependsOn` to just the ones it uses.\n- The data layer a frontend uses may live in a sibling/shared package OUTSIDE the frontend's own folder - record that package as a backend, point its dataLayer.schemaPath at the schema, and include it in the frontend's `dependsOn`.\n- A codebase may ship only one half. If you find frontends but NO backend/data layer (or vice versa), still record what you found and leave the other group empty - the caller will ask the user to supply the missing half.\n\nBe thorough and evidence-based. When you have enumerated the candidates and their dependency edges, call set_project_map, then call finish. Keep each `why` to a single concrete sentence citing what you saw.";
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1397
1616
|
// src/agents/00-pages-finder/index.ts
|
|
1398
1617
|
var pages_finder_exports = {};
|
|
1399
1618
|
__export(pages_finder_exports, {
|
|
@@ -1401,8 +1620,8 @@ __export(pages_finder_exports, {
|
|
|
1401
1620
|
});
|
|
1402
1621
|
import { existsSync } from "fs";
|
|
1403
1622
|
import * as path2 from "path";
|
|
1404
|
-
import { tool as
|
|
1405
|
-
import { z as
|
|
1623
|
+
import { tool as tool10 } from "ai";
|
|
1624
|
+
import { z as z12 } from "zod";
|
|
1406
1625
|
async function runPageFinder(input) {
|
|
1407
1626
|
const model = getModel(input.modelId);
|
|
1408
1627
|
const pageCollector = new PageCollector();
|
|
@@ -1421,7 +1640,7 @@ ${input.extraMessage}`;
|
|
|
1421
1640
|
const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
|
|
1422
1641
|
return {
|
|
1423
1642
|
...tools,
|
|
1424
|
-
add_page:
|
|
1643
|
+
add_page: tool10({
|
|
1425
1644
|
description: "use this tool to add a page that you found",
|
|
1426
1645
|
inputSchema: Page,
|
|
1427
1646
|
execute: (input2) => {
|
|
@@ -1433,9 +1652,9 @@ ${input.extraMessage}`;
|
|
|
1433
1652
|
return `page ${JSON.stringify(input2)} added`;
|
|
1434
1653
|
}
|
|
1435
1654
|
}),
|
|
1436
|
-
view_pages:
|
|
1655
|
+
view_pages: tool10({
|
|
1437
1656
|
description: "use this tool to view all the pages that you already added",
|
|
1438
|
-
inputSchema:
|
|
1657
|
+
inputSchema: z12.object(),
|
|
1439
1658
|
execute: () => pageCollector.viewPages()
|
|
1440
1659
|
})
|
|
1441
1660
|
};
|
|
@@ -1454,10 +1673,10 @@ var init_pages_finder = __esm({
|
|
|
1454
1673
|
init_agent();
|
|
1455
1674
|
init_model();
|
|
1456
1675
|
init_tools();
|
|
1457
|
-
Page =
|
|
1458
|
-
route:
|
|
1459
|
-
path:
|
|
1460
|
-
description:
|
|
1676
|
+
Page = z12.object({
|
|
1677
|
+
route: z12.string().min(1),
|
|
1678
|
+
path: z12.string().min(1),
|
|
1679
|
+
description: z12.string().min(10)
|
|
1461
1680
|
});
|
|
1462
1681
|
PageCollector = class {
|
|
1463
1682
|
// the key is the path
|
|
@@ -1487,13 +1706,13 @@ var init_pages_finder = __esm({
|
|
|
1487
1706
|
|
|
1488
1707
|
// src/core/review.ts
|
|
1489
1708
|
import { access } from "fs/promises";
|
|
1490
|
-
import { join as
|
|
1709
|
+
import { join as join13, isAbsolute } from "path";
|
|
1491
1710
|
import * as p3 from "@clack/prompts";
|
|
1492
1711
|
import spawn from "cross-spawn";
|
|
1493
1712
|
import which from "which";
|
|
1494
1713
|
function resolvePath(artifact, outputDir) {
|
|
1495
1714
|
if (isAbsolute(artifact)) return artifact;
|
|
1496
|
-
return
|
|
1715
|
+
return join13(outputDir, artifact);
|
|
1497
1716
|
}
|
|
1498
1717
|
async function detectEditors() {
|
|
1499
1718
|
if (cachedEditors) return cachedEditors;
|
|
@@ -1508,17 +1727,17 @@ async function detectEditors() {
|
|
|
1508
1727
|
async function launchEditor(editor, files) {
|
|
1509
1728
|
const args = editor.args(files);
|
|
1510
1729
|
const isTerminalEditor = TERMINAL_EDITORS.has(editor.command);
|
|
1511
|
-
await new Promise((
|
|
1730
|
+
await new Promise((resolve6) => {
|
|
1512
1731
|
let settled = false;
|
|
1513
1732
|
const settle = () => {
|
|
1514
1733
|
if (settled) return;
|
|
1515
1734
|
settled = true;
|
|
1516
|
-
|
|
1735
|
+
resolve6();
|
|
1517
1736
|
};
|
|
1518
1737
|
const proc = spawn(editor.command, args, { stdio: "inherit" });
|
|
1519
1738
|
proc.on("error", (err) => {
|
|
1520
1739
|
p3.log.warn(`Couldn't open ${editor.label} (${err.message}). Review the files manually:`);
|
|
1521
|
-
for (const f of files) console.log(` ${
|
|
1740
|
+
for (const f of files) console.log(` ${CYAN}${f}${RESET3}`);
|
|
1522
1741
|
settle();
|
|
1523
1742
|
});
|
|
1524
1743
|
if (isTerminalEditor) {
|
|
@@ -1532,7 +1751,7 @@ async function openInEditor(files) {
|
|
|
1532
1751
|
const editors = await detectEditors();
|
|
1533
1752
|
if (editors.length === 0) {
|
|
1534
1753
|
p3.log.warn("No editors found. Review the files manually:");
|
|
1535
|
-
for (const f of files) console.log(` ${
|
|
1754
|
+
for (const f of files) console.log(` ${CYAN}${f}${RESET3}`);
|
|
1536
1755
|
return;
|
|
1537
1756
|
}
|
|
1538
1757
|
if (preferredEditor) {
|
|
@@ -1571,11 +1790,11 @@ async function openInEditor(files) {
|
|
|
1571
1790
|
}
|
|
1572
1791
|
async function showResults(result, options) {
|
|
1573
1792
|
console.log("");
|
|
1574
|
-
console.log(` ${
|
|
1793
|
+
console.log(` ${GREEN}[${options.agentId}] Step complete.${RESET3}`);
|
|
1575
1794
|
if (result.artifacts.length === 0) {
|
|
1576
1795
|
const knownFiles = ["AUTONOMA.md", "entity-audit.md", "scenarios.md"];
|
|
1577
1796
|
for (const f of knownFiles) {
|
|
1578
|
-
const fullPath =
|
|
1797
|
+
const fullPath = join13(options.outputDir, f);
|
|
1579
1798
|
try {
|
|
1580
1799
|
await access(fullPath);
|
|
1581
1800
|
result.artifacts.push(f);
|
|
@@ -1590,7 +1809,7 @@ async function showResults(result, options) {
|
|
|
1590
1809
|
for (const a of result.artifacts) {
|
|
1591
1810
|
const fullPath = resolvePath(a, options.outputDir);
|
|
1592
1811
|
resolvedPaths.push(fullPath);
|
|
1593
|
-
console.log(` ${
|
|
1812
|
+
console.log(` ${CYAN}${fullPath}${RESET3}`);
|
|
1594
1813
|
}
|
|
1595
1814
|
}
|
|
1596
1815
|
if (result.summary) {
|
|
@@ -1641,7 +1860,7 @@ async function reviewLoop(result, options) {
|
|
|
1641
1860
|
await showResults(result, options);
|
|
1642
1861
|
}
|
|
1643
1862
|
}
|
|
1644
|
-
var DIM3,
|
|
1863
|
+
var DIM3, CYAN, GREEN, RESET3, EDITORS, cachedEditors, preferredEditor, TERMINAL_EDITORS;
|
|
1645
1864
|
var init_review = __esm({
|
|
1646
1865
|
"src/core/review.ts"() {
|
|
1647
1866
|
"use strict";
|
|
@@ -1649,8 +1868,8 @@ var init_review = __esm({
|
|
|
1649
1868
|
init_debug();
|
|
1650
1869
|
init_notify();
|
|
1651
1870
|
DIM3 = "\x1B[2m";
|
|
1652
|
-
|
|
1653
|
-
|
|
1871
|
+
CYAN = "\x1B[36m";
|
|
1872
|
+
GREEN = "\x1B[32m";
|
|
1654
1873
|
RESET3 = "\x1B[0m";
|
|
1655
1874
|
EDITORS = [
|
|
1656
1875
|
{ command: "cursor", label: "Cursor", args: (f) => f },
|
|
@@ -1664,13 +1883,13 @@ var init_review = __esm({
|
|
|
1664
1883
|
});
|
|
1665
1884
|
|
|
1666
1885
|
// src/agents/01-kb-generator/flows.ts
|
|
1667
|
-
import { readFile as
|
|
1668
|
-
import { join as
|
|
1886
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
1887
|
+
import { join as join14 } from "path";
|
|
1669
1888
|
import matter from "gray-matter";
|
|
1670
1889
|
async function parseCoreFlows(outputDir) {
|
|
1671
1890
|
let raw;
|
|
1672
1891
|
try {
|
|
1673
|
-
raw = await
|
|
1892
|
+
raw = await readFile8(join14(outputDir, "AUTONOMA.md"), "utf-8");
|
|
1674
1893
|
} catch {
|
|
1675
1894
|
return [];
|
|
1676
1895
|
}
|
|
@@ -1714,7 +1933,7 @@ function renderFlowsTable(flows) {
|
|
|
1714
1933
|
const sep = `${DIM4}${"\u2500".repeat(numW + nameW + critW + DESC_MAX + 6)}${RESET4}`;
|
|
1715
1934
|
const body = rows.map((r) => {
|
|
1716
1935
|
const line = `${pad(r.num, numW)} ${pad(r.name, nameW)} ${pad(r.crit, critW)} ${r.desc}`;
|
|
1717
|
-
return r.crit === "core" ? `${
|
|
1936
|
+
return r.crit === "core" ? `${YELLOW}${line}${RESET4}` : line;
|
|
1718
1937
|
}).join("\n");
|
|
1719
1938
|
const caption = `${DIM4}${flows.length} flows \xB7 ${coreCount} marked core${RESET4}`;
|
|
1720
1939
|
return `${header}
|
|
@@ -1722,25 +1941,25 @@ ${sep}
|
|
|
1722
1941
|
${body}
|
|
1723
1942
|
${caption}`;
|
|
1724
1943
|
}
|
|
1725
|
-
var RESET4, DIM4,
|
|
1944
|
+
var RESET4, DIM4, YELLOW, BOLD2;
|
|
1726
1945
|
var init_flows = __esm({
|
|
1727
1946
|
"src/agents/01-kb-generator/flows.ts"() {
|
|
1728
1947
|
"use strict";
|
|
1729
1948
|
init_esm_shims();
|
|
1730
1949
|
RESET4 = "\x1B[0m";
|
|
1731
1950
|
DIM4 = "\x1B[2m";
|
|
1732
|
-
|
|
1951
|
+
YELLOW = "\x1B[33m";
|
|
1733
1952
|
BOLD2 = "\x1B[1m";
|
|
1734
1953
|
}
|
|
1735
1954
|
});
|
|
1736
1955
|
|
|
1737
1956
|
// src/agents/01-kb-generator/prompt.ts
|
|
1738
|
-
var
|
|
1957
|
+
var SYSTEM_PROMPT3;
|
|
1739
1958
|
var init_prompt = __esm({
|
|
1740
1959
|
"src/agents/01-kb-generator/prompt.ts"() {
|
|
1741
1960
|
"use strict";
|
|
1742
1961
|
init_esm_shims();
|
|
1743
|
-
|
|
1962
|
+
SYSTEM_PROMPT3 = `You are a knowledge base generator for E2E test planning. You analyze a frontend codebase and produce a structured guide to EVERY page, flow, and interaction. You must be EXHAUSTIVE - missing a page means missing test coverage.
|
|
1744
1963
|
|
|
1745
1964
|
## Your output
|
|
1746
1965
|
|
|
@@ -1883,15 +2102,15 @@ var kb_generator_exports = {};
|
|
|
1883
2102
|
__export(kb_generator_exports, {
|
|
1884
2103
|
runKBGenerator: () => runKBGenerator
|
|
1885
2104
|
});
|
|
1886
|
-
import { readFile as
|
|
1887
|
-
import { join as
|
|
1888
|
-
import { tool as
|
|
1889
|
-
import { z as
|
|
2105
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2106
|
+
import { join as join15, resolve as resolve5 } from "path";
|
|
2107
|
+
import { tool as tool11 } from "ai";
|
|
2108
|
+
import { z as z13 } from "zod";
|
|
1890
2109
|
function buildRegisterPagesTool(tracker) {
|
|
1891
|
-
return
|
|
2110
|
+
return tool11({
|
|
1892
2111
|
description: "Register ALL page/route files discovered via glob. Call this ONCE after globbing for page files. The system will track which ones you've read and block finish until all are covered.",
|
|
1893
|
-
inputSchema:
|
|
1894
|
-
pages:
|
|
2112
|
+
inputSchema: z13.object({
|
|
2113
|
+
pages: z13.array(z13.string()).describe("All page file paths found by glob")
|
|
1895
2114
|
}),
|
|
1896
2115
|
execute: async (input) => {
|
|
1897
2116
|
tracker.register(input.pages);
|
|
@@ -1903,25 +2122,33 @@ function buildRegisterPagesTool(tracker) {
|
|
|
1903
2122
|
});
|
|
1904
2123
|
}
|
|
1905
2124
|
function buildPageCoverageTool(tracker) {
|
|
1906
|
-
return
|
|
2125
|
+
return tool11({
|
|
1907
2126
|
description: "Check how many registered pages you've read vs how many remain.",
|
|
1908
|
-
inputSchema:
|
|
2127
|
+
inputSchema: z13.object({}),
|
|
1909
2128
|
execute: async () => tracker.coverage()
|
|
1910
2129
|
});
|
|
1911
2130
|
}
|
|
2131
|
+
function requiredReads(total) {
|
|
2132
|
+
if (total <= FULL_COVERAGE_MAX_ROUTES) return total;
|
|
2133
|
+
return Math.ceil(total * LARGE_APP_COVERAGE_FLOOR);
|
|
2134
|
+
}
|
|
1912
2135
|
function buildFinishTool(tracker, onFinish) {
|
|
1913
|
-
return
|
|
1914
|
-
description: "Call when you have finished generating the knowledge base. BLOCKED
|
|
1915
|
-
inputSchema:
|
|
1916
|
-
summary:
|
|
1917
|
-
artifacts:
|
|
2136
|
+
return tool11({
|
|
2137
|
+
description: "Call when you have finished generating the knowledge base. BLOCKED until you have read enough of the registered routes (every route on a small app; a strong majority on a large one) - call page_coverage first to check how many remain.",
|
|
2138
|
+
inputSchema: z13.object({
|
|
2139
|
+
summary: z13.string().describe("Summary of what was generated"),
|
|
2140
|
+
artifacts: z13.array(z13.string()).describe("List of files written")
|
|
1918
2141
|
}),
|
|
1919
2142
|
execute: async (input) => {
|
|
1920
2143
|
const cov = tracker.coverage();
|
|
1921
|
-
|
|
2144
|
+
const required = requiredReads(cov.total);
|
|
2145
|
+
if (cov.read < required) {
|
|
2146
|
+
const preview = cov.unread.slice(0, 40).join("\n");
|
|
2147
|
+
const more = cov.unread.length > 40 ? `
|
|
2148
|
+
...and ${cov.unread.length - 40} more` : "";
|
|
1922
2149
|
return {
|
|
1923
|
-
error: `Cannot finish: ${cov.
|
|
1924
|
-
${
|
|
2150
|
+
error: `Cannot finish: only ${cov.read}/${cov.total} routes read - read at least ${required - cov.read} more (target ${required} of ${cov.total}). Start with:
|
|
2151
|
+
${preview}${more}`
|
|
1925
2152
|
};
|
|
1926
2153
|
}
|
|
1927
2154
|
onFinish({
|
|
@@ -1934,7 +2161,7 @@ ${cov.unread.join("\n")}`
|
|
|
1934
2161
|
});
|
|
1935
2162
|
}
|
|
1936
2163
|
function buildTrackedReadTool(tracker, baseTool) {
|
|
1937
|
-
return
|
|
2164
|
+
return tool11({
|
|
1938
2165
|
description: baseTool.description,
|
|
1939
2166
|
inputSchema: baseTool.inputSchema,
|
|
1940
2167
|
execute: async (input, options) => {
|
|
@@ -1944,13 +2171,36 @@ function buildTrackedReadTool(tracker, baseTool) {
|
|
|
1944
2171
|
}
|
|
1945
2172
|
});
|
|
1946
2173
|
}
|
|
2174
|
+
function buildKbAgentConfig(tracker, model, input, onStepFinish, setResult) {
|
|
2175
|
+
return {
|
|
2176
|
+
id: "kb-generator",
|
|
2177
|
+
systemPrompt: SYSTEM_PROMPT3,
|
|
2178
|
+
model,
|
|
2179
|
+
maxSteps: 150,
|
|
2180
|
+
tools: async (heartbeat) => {
|
|
2181
|
+
const onFileRead = (path3) => tracker.markRead(path3);
|
|
2182
|
+
const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat, onFileRead);
|
|
2183
|
+
return {
|
|
2184
|
+
...tools,
|
|
2185
|
+
read_file: buildTrackedReadTool(tracker, tools.read_file),
|
|
2186
|
+
register_pages: buildRegisterPagesTool(tracker),
|
|
2187
|
+
page_coverage: buildPageCoverageTool(tracker),
|
|
2188
|
+
finish: buildFinishTool(tracker, setResult)
|
|
2189
|
+
};
|
|
2190
|
+
},
|
|
2191
|
+
onStepFinish
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
1947
2194
|
async function runKBGenerator(input) {
|
|
1948
2195
|
const model = getModel(input.modelId);
|
|
1949
2196
|
let result;
|
|
1950
|
-
const
|
|
2197
|
+
const setResult = (r) => {
|
|
2198
|
+
result = r;
|
|
2199
|
+
};
|
|
1951
2200
|
const { logger, onStepFinish } = buildDefaultStepLogger("kb", 150);
|
|
1952
2201
|
const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + formatRetryGuidance(input.retryGuidance);
|
|
1953
2202
|
const pages = input.projectContext?.pages;
|
|
2203
|
+
const tracker = new PageTracker(input.projectRoot);
|
|
1954
2204
|
if (pages?.length) {
|
|
1955
2205
|
tracker.register(pages.map((p10) => p10.path));
|
|
1956
2206
|
}
|
|
@@ -1962,7 +2212,7 @@ Pages have already been discovered (${pages.length} routes pre-registered). You
|
|
|
1962
2212
|
2. Read EVERY registered page file with read_file - the system tracks this
|
|
1963
2213
|
3. Write AUTONOMA.md progressively as you go (update it after each major area)
|
|
1964
2214
|
4. Call page_coverage to verify you've read all pages
|
|
1965
|
-
5. Call finish - it will REJECT if
|
|
2215
|
+
5. Call finish - it will REJECT if you have not read enough of the registered routes
|
|
1966
2216
|
|
|
1967
2217
|
Output files:
|
|
1968
2218
|
1. AUTONOMA.md - with YAML frontmatter (app_name, app_description, core_flows, feature_count)` : `Analyze the codebase at the working directory and generate a complete knowledge base.
|
|
@@ -1974,34 +2224,15 @@ MANDATORY PROCESS:
|
|
|
1974
2224
|
4. Read EVERY registered page file with read_file - the system tracks this
|
|
1975
2225
|
5. Write AUTONOMA.md progressively as you go (update it after each major area)
|
|
1976
2226
|
6. Call page_coverage to verify you've read all pages
|
|
1977
|
-
7. Call finish - it will REJECT if
|
|
2227
|
+
7. Call finish - it will REJECT if you have not read enough of the registered routes
|
|
1978
2228
|
|
|
1979
2229
|
Output files:
|
|
1980
2230
|
1. AUTONOMA.md - with YAML frontmatter (app_name, app_description, core_flows, feature_count)`;
|
|
1981
|
-
const agentConfig =
|
|
1982
|
-
id: "kb-generator",
|
|
1983
|
-
systemPrompt: SYSTEM_PROMPT2,
|
|
1984
|
-
model,
|
|
1985
|
-
maxSteps: 150,
|
|
1986
|
-
tools: async (heartbeat) => {
|
|
1987
|
-
const onFileRead = (path3) => tracker.markRead(path3);
|
|
1988
|
-
const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat, onFileRead);
|
|
1989
|
-
return {
|
|
1990
|
-
...tools,
|
|
1991
|
-
read_file: buildTrackedReadTool(tracker, tools.read_file),
|
|
1992
|
-
register_pages: buildRegisterPagesTool(tracker),
|
|
1993
|
-
page_coverage: buildPageCoverageTool(tracker),
|
|
1994
|
-
finish: buildFinishTool(tracker, (r) => {
|
|
1995
|
-
result = r;
|
|
1996
|
-
})
|
|
1997
|
-
};
|
|
1998
|
-
},
|
|
1999
|
-
onStepFinish
|
|
2000
|
-
};
|
|
2231
|
+
const agentConfig = buildKbAgentConfig(tracker, model, input, onStepFinish, setResult);
|
|
2001
2232
|
await runAgent(agentConfig, prompt, () => result);
|
|
2002
2233
|
logger.summary();
|
|
2003
|
-
const autonomaPath =
|
|
2004
|
-
const autonomaExists = await
|
|
2234
|
+
const autonomaPath = join15(input.outputDir, "AUTONOMA.md");
|
|
2235
|
+
const autonomaExists = await readFile9(autonomaPath, "utf-8").then(() => true).catch((err) => {
|
|
2005
2236
|
debugLog("AUTONOMA.md not found while checking step completion", { err });
|
|
2006
2237
|
return false;
|
|
2007
2238
|
});
|
|
@@ -2012,6 +2243,13 @@ Output files:
|
|
|
2012
2243
|
summary: "Knowledge base generated."
|
|
2013
2244
|
};
|
|
2014
2245
|
}
|
|
2246
|
+
const finalTracker = new PageTracker(input.projectRoot);
|
|
2247
|
+
if (pages?.length) {
|
|
2248
|
+
const paths = pages.map((p10) => p10.path);
|
|
2249
|
+
finalTracker.register(paths);
|
|
2250
|
+
for (const path3 of paths) finalTracker.markRead(path3);
|
|
2251
|
+
}
|
|
2252
|
+
const finalConfig = buildKbAgentConfig(finalTracker, model, input, onStepFinish, setResult);
|
|
2015
2253
|
const declaredCriticalFlows = input.projectContext?.criticalFlows?.trim();
|
|
2016
2254
|
if (result?.success && declaredCriticalFlows) {
|
|
2017
2255
|
const beforeSelfReview = result;
|
|
@@ -2028,7 +2266,7 @@ Read your AUTONOMA.md output. For EACH critical flow the user named:
|
|
|
2028
2266
|
If any declared critical flow is missing, mismatched, or left core: false, FIX AUTONOMA.md now - add the feature if it is genuinely absent, or flip core to true with a coreReason. Do not downgrade or drop anything the user declared critical.
|
|
2029
2267
|
|
|
2030
2268
|
When AUTONOMA.md correctly reflects every declared critical flow, call finish.`;
|
|
2031
|
-
await runAgent(
|
|
2269
|
+
await runAgent(finalConfig, selfReviewPrompt, () => result);
|
|
2032
2270
|
if (!result) result = beforeSelfReview;
|
|
2033
2271
|
}
|
|
2034
2272
|
const reviewed = await reviewLoop(result, {
|
|
@@ -2049,7 +2287,7 @@ When AUTONOMA.md correctly reflects every declared critical flow, call finish.`;
|
|
|
2049
2287
|
Read your previous output file (AUTONOMA.md) from the output directory to see what you produced.
|
|
2050
2288
|
Adjust based on the feedback. You can read source files again if needed.
|
|
2051
2289
|
Call page_coverage to see current state. When done with changes, call finish again.`;
|
|
2052
|
-
await runAgent(
|
|
2290
|
+
await runAgent(finalConfig, feedbackPrompt, () => result);
|
|
2053
2291
|
return result;
|
|
2054
2292
|
}
|
|
2055
2293
|
});
|
|
@@ -2059,7 +2297,7 @@ Call page_coverage to see current state. When done with changes, call finish aga
|
|
|
2059
2297
|
summary: "KB generator agent stopped without producing AUTONOMA.md"
|
|
2060
2298
|
};
|
|
2061
2299
|
}
|
|
2062
|
-
var PageTracker;
|
|
2300
|
+
var PageTracker, FULL_COVERAGE_MAX_ROUTES, LARGE_APP_COVERAGE_FLOOR;
|
|
2063
2301
|
var init_kb_generator = __esm({
|
|
2064
2302
|
"src/agents/01-kb-generator/index.ts"() {
|
|
2065
2303
|
"use strict";
|
|
@@ -2074,14 +2312,26 @@ var init_kb_generator = __esm({
|
|
|
2074
2312
|
init_flows();
|
|
2075
2313
|
init_prompt();
|
|
2076
2314
|
PageTracker = class {
|
|
2315
|
+
// Registered pages (from pages.json) are absolute paths, but the agent reads them
|
|
2316
|
+
// with paths relative to the working directory. Canonicalize both to an absolute
|
|
2317
|
+
// path against projectRoot so coverage matches regardless of how each side spelled
|
|
2318
|
+
// it - otherwise the finish gate never clears and the agent nudges out re-reading.
|
|
2319
|
+
constructor(projectRoot) {
|
|
2320
|
+
this.projectRoot = projectRoot;
|
|
2321
|
+
}
|
|
2322
|
+
projectRoot;
|
|
2077
2323
|
registered = /* @__PURE__ */ new Set();
|
|
2078
2324
|
read = /* @__PURE__ */ new Set();
|
|
2325
|
+
normalize(filePath) {
|
|
2326
|
+
return resolve5(this.projectRoot, filePath);
|
|
2327
|
+
}
|
|
2079
2328
|
register(pages) {
|
|
2080
|
-
for (const p10 of pages) this.registered.add(p10);
|
|
2329
|
+
for (const p10 of pages) this.registered.add(this.normalize(p10));
|
|
2081
2330
|
}
|
|
2082
2331
|
markRead(filePath) {
|
|
2083
|
-
|
|
2084
|
-
|
|
2332
|
+
const normalized = this.normalize(filePath);
|
|
2333
|
+
if (this.registered.has(normalized)) {
|
|
2334
|
+
this.read.add(normalized);
|
|
2085
2335
|
}
|
|
2086
2336
|
}
|
|
2087
2337
|
unread() {
|
|
@@ -2095,16 +2345,18 @@ var init_kb_generator = __esm({
|
|
|
2095
2345
|
};
|
|
2096
2346
|
}
|
|
2097
2347
|
};
|
|
2348
|
+
FULL_COVERAGE_MAX_ROUTES = 40;
|
|
2349
|
+
LARGE_APP_COVERAGE_FLOOR = 0.5;
|
|
2098
2350
|
}
|
|
2099
2351
|
});
|
|
2100
2352
|
|
|
2101
2353
|
// src/agents/04-recipe-builder/entity-order.ts
|
|
2102
|
-
import { readFile as
|
|
2103
|
-
import { join as
|
|
2354
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2355
|
+
import { join as join16 } from "path";
|
|
2104
2356
|
import matter2 from "gray-matter";
|
|
2105
|
-
import { z as
|
|
2357
|
+
import { z as z14 } from "zod";
|
|
2106
2358
|
async function parseEntityAudit(outputDir) {
|
|
2107
|
-
const raw = await
|
|
2359
|
+
const raw = await readFile10(join16(outputDir, "entity-audit.md"), "utf-8");
|
|
2108
2360
|
try {
|
|
2109
2361
|
const parsed = frontmatterSchema.safeParse(matter2(raw).data);
|
|
2110
2362
|
if (parsed.success && parsed.data.models.length > 0) {
|
|
@@ -2265,22 +2517,22 @@ var init_entity_order = __esm({
|
|
|
2265
2517
|
"src/agents/04-recipe-builder/entity-order.ts"() {
|
|
2266
2518
|
"use strict";
|
|
2267
2519
|
init_esm_shims();
|
|
2268
|
-
createdBySchema =
|
|
2269
|
-
owner:
|
|
2270
|
-
via:
|
|
2271
|
-
why:
|
|
2520
|
+
createdBySchema = z14.object({
|
|
2521
|
+
owner: z14.string(),
|
|
2522
|
+
via: z14.string().optional(),
|
|
2523
|
+
why: z14.string().optional()
|
|
2272
2524
|
});
|
|
2273
|
-
auditedModelSchema =
|
|
2274
|
-
name:
|
|
2275
|
-
independently_created:
|
|
2276
|
-
creation_file:
|
|
2277
|
-
creation_function:
|
|
2278
|
-
side_effects:
|
|
2525
|
+
auditedModelSchema = z14.object({
|
|
2526
|
+
name: z14.string(),
|
|
2527
|
+
independently_created: z14.coerce.boolean().default(false),
|
|
2528
|
+
creation_file: z14.string().optional(),
|
|
2529
|
+
creation_function: z14.string().optional(),
|
|
2530
|
+
side_effects: z14.array(z14.string()).optional(),
|
|
2279
2531
|
// Tolerate a stray `created_by:` with no entries (parsed as null by YAML).
|
|
2280
|
-
created_by:
|
|
2532
|
+
created_by: z14.array(createdBySchema).nullish().transform((v) => v ?? [])
|
|
2281
2533
|
});
|
|
2282
|
-
frontmatterSchema =
|
|
2283
|
-
models:
|
|
2534
|
+
frontmatterSchema = z14.object({
|
|
2535
|
+
models: z14.array(auditedModelSchema).nullish().transform((v) => v ?? [])
|
|
2284
2536
|
});
|
|
2285
2537
|
}
|
|
2286
2538
|
});
|
|
@@ -2325,7 +2577,7 @@ function renderEntityAuditTable(models) {
|
|
|
2325
2577
|
const sep = `${DIM5}${"\u2500".repeat(numW + nameW + critW + SRC_MAX + 6)}${RESET5}`;
|
|
2326
2578
|
const body = rows.map((r) => {
|
|
2327
2579
|
const line = `${pad2(r.num, numW)} ${pad2(r.name, nameW)} ${pad2(r.creation, critW)} ${r.source}`;
|
|
2328
|
-
return r.creation === "standalone" ? `${
|
|
2580
|
+
return r.creation === "standalone" ? `${YELLOW2}${line}${RESET5}` : line;
|
|
2329
2581
|
}).join("\n");
|
|
2330
2582
|
const caption = `${DIM5}${models.length} models \xB7 ${standalone} standalone \xB7 ${models.length - standalone} side-effect-only${RESET5}`;
|
|
2331
2583
|
return `${header}
|
|
@@ -2333,7 +2585,7 @@ ${sep}
|
|
|
2333
2585
|
${body}
|
|
2334
2586
|
${caption}`;
|
|
2335
2587
|
}
|
|
2336
|
-
var RESET5, DIM5,
|
|
2588
|
+
var RESET5, DIM5, YELLOW2, BOLD3;
|
|
2337
2589
|
var init_audit_table = __esm({
|
|
2338
2590
|
"src/agents/02-entity-audit/audit-table.ts"() {
|
|
2339
2591
|
"use strict";
|
|
@@ -2341,18 +2593,18 @@ var init_audit_table = __esm({
|
|
|
2341
2593
|
init_entity_order();
|
|
2342
2594
|
RESET5 = "\x1B[0m";
|
|
2343
2595
|
DIM5 = "\x1B[2m";
|
|
2344
|
-
|
|
2596
|
+
YELLOW2 = "\x1B[33m";
|
|
2345
2597
|
BOLD3 = "\x1B[1m";
|
|
2346
2598
|
}
|
|
2347
2599
|
});
|
|
2348
2600
|
|
|
2349
2601
|
// src/agents/02-entity-audit/prompt.ts
|
|
2350
|
-
var
|
|
2602
|
+
var SYSTEM_PROMPT4;
|
|
2351
2603
|
var init_prompt2 = __esm({
|
|
2352
2604
|
"src/agents/02-entity-audit/prompt.ts"() {
|
|
2353
2605
|
"use strict";
|
|
2354
2606
|
init_esm_shims();
|
|
2355
|
-
|
|
2607
|
+
SYSTEM_PROMPT4 = `You audit a codebase to discover EVERY database model and every way each is created. You must find ALL models - missing one means the test data layer has a gap. This audit drives factory generation and scenario planning.
|
|
2356
2608
|
|
|
2357
2609
|
## The two orthogonal questions
|
|
2358
2610
|
|
|
@@ -2490,17 +2742,17 @@ var entity_audit_exports = {};
|
|
|
2490
2742
|
__export(entity_audit_exports, {
|
|
2491
2743
|
runEntityAudit: () => runEntityAudit
|
|
2492
2744
|
});
|
|
2493
|
-
import { readFile as
|
|
2494
|
-
import { join as
|
|
2495
|
-
import { tool as
|
|
2745
|
+
import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
2746
|
+
import { join as join17 } from "path";
|
|
2747
|
+
import { tool as tool12 } from "ai";
|
|
2496
2748
|
import { glob as glob4 } from "glob";
|
|
2497
|
-
import { z as
|
|
2749
|
+
import { z as z15 } from "zod";
|
|
2498
2750
|
function buildRegisterModelsTool(tracker) {
|
|
2499
|
-
return
|
|
2751
|
+
return tool12({
|
|
2500
2752
|
description: "Register ALL database models discovered via grep. Call this ONCE after grepping for model definitions. After registering, use next_model to process them one at a time.",
|
|
2501
|
-
inputSchema:
|
|
2502
|
-
models:
|
|
2503
|
-
framework:
|
|
2753
|
+
inputSchema: z15.object({
|
|
2754
|
+
models: z15.array(z15.string()).describe("All model/table names found by grep"),
|
|
2755
|
+
framework: z15.string().describe("Database framework detected (e.g. 'sqlalchemy', 'prisma', 'drizzle')")
|
|
2504
2756
|
}),
|
|
2505
2757
|
execute: async (input) => {
|
|
2506
2758
|
tracker.register(input.models);
|
|
@@ -2514,9 +2766,9 @@ function buildRegisterModelsTool(tracker) {
|
|
|
2514
2766
|
});
|
|
2515
2767
|
}
|
|
2516
2768
|
function buildNextModelTool(tracker) {
|
|
2517
|
-
return
|
|
2769
|
+
return tool12({
|
|
2518
2770
|
description: "Get the next model to audit from the queue. If you called next_model before without calling mark_model_audited, the previous model is auto-skipped (marked as no creation path found). Returns done:true when all models are processed.",
|
|
2519
|
-
inputSchema:
|
|
2771
|
+
inputSchema: z15.object({}),
|
|
2520
2772
|
execute: async () => {
|
|
2521
2773
|
const next = tracker.nextModel();
|
|
2522
2774
|
if (!next) {
|
|
@@ -2534,19 +2786,19 @@ function buildNextModelTool(tracker) {
|
|
|
2534
2786
|
});
|
|
2535
2787
|
}
|
|
2536
2788
|
function buildMarkModelAuditedTool(tracker) {
|
|
2537
|
-
return
|
|
2789
|
+
return tool12({
|
|
2538
2790
|
description: "Mark a model as audited after you have determined its creation paths. Call this for EACH model after reading its creation code and determining independently_created + created_by. Include creation_function (e.g. 'UserService.create'), side_effects (list of things the creation does beyond the model itself), and for each created_by entry include owner, via (function name), and why (one sentence explaining the relationship).",
|
|
2539
|
-
inputSchema:
|
|
2540
|
-
model:
|
|
2541
|
-
independently_created:
|
|
2542
|
-
creation_file:
|
|
2543
|
-
creation_function:
|
|
2544
|
-
side_effects:
|
|
2545
|
-
created_by:
|
|
2546
|
-
|
|
2547
|
-
owner:
|
|
2548
|
-
via:
|
|
2549
|
-
why:
|
|
2791
|
+
inputSchema: z15.object({
|
|
2792
|
+
model: z15.string().describe("Model name"),
|
|
2793
|
+
independently_created: z15.boolean(),
|
|
2794
|
+
creation_file: z15.string().optional().describe("File containing the creation function"),
|
|
2795
|
+
creation_function: z15.string().optional().describe("Function/method name (e.g. 'UserService.create' or 'create_user')"),
|
|
2796
|
+
side_effects: z15.array(z15.string()).optional().describe("Side effects of creation (e.g. 'creates default Settings row', 'hashes password')"),
|
|
2797
|
+
created_by: z15.array(
|
|
2798
|
+
z15.object({
|
|
2799
|
+
owner: z15.string().describe("Owner model name"),
|
|
2800
|
+
via: z15.string().optional().describe("Function that creates this model (e.g. 'OrganizationService.create')"),
|
|
2801
|
+
why: z15.string().optional().describe("One sentence explaining why this model is created as a side effect")
|
|
2550
2802
|
})
|
|
2551
2803
|
).describe("List of owner models that create this as a side effect, empty array if none")
|
|
2552
2804
|
}),
|
|
@@ -2583,18 +2835,18 @@ function buildMarkModelAuditedTool(tracker) {
|
|
|
2583
2835
|
});
|
|
2584
2836
|
}
|
|
2585
2837
|
function buildModelCoverageTool(tracker) {
|
|
2586
|
-
return
|
|
2838
|
+
return tool12({
|
|
2587
2839
|
description: "Check how many registered models you've audited vs how many remain.",
|
|
2588
|
-
inputSchema:
|
|
2840
|
+
inputSchema: z15.object({}),
|
|
2589
2841
|
execute: async () => tracker.coverage()
|
|
2590
2842
|
});
|
|
2591
2843
|
}
|
|
2592
2844
|
function buildFinishTool2(tracker, onFinish) {
|
|
2593
|
-
return
|
|
2845
|
+
return tool12({
|
|
2594
2846
|
description: "Call when entity audit is complete. BLOCKED if there are unaudited models - call model_coverage first to check.",
|
|
2595
|
-
inputSchema:
|
|
2596
|
-
summary:
|
|
2597
|
-
artifacts:
|
|
2847
|
+
inputSchema: z15.object({
|
|
2848
|
+
summary: z15.string().describe("Summary of the audit"),
|
|
2849
|
+
artifacts: z15.array(z15.string()).describe("Files written")
|
|
2598
2850
|
}),
|
|
2599
2851
|
execute: async (input) => {
|
|
2600
2852
|
const cov = tracker.coverage();
|
|
@@ -2622,7 +2874,7 @@ async function findPrismaSchema(projectRoot) {
|
|
|
2622
2874
|
return candidates[0] ?? void 0;
|
|
2623
2875
|
}
|
|
2624
2876
|
async function extractPrismaModels(schemaPath) {
|
|
2625
|
-
const content = await
|
|
2877
|
+
const content = await readFile11(schemaPath, "utf-8");
|
|
2626
2878
|
return content.split("\n").filter((line) => line.startsWith("model ")).map((line) => line.split(/\s+/)[1]).filter((name) => name != null);
|
|
2627
2879
|
}
|
|
2628
2880
|
async function detectFrameworkAndModels(projectRoot) {
|
|
@@ -2645,8 +2897,11 @@ async function runEntityAudit(input) {
|
|
|
2645
2897
|
tracker.initQueue();
|
|
2646
2898
|
preRegisteredCount = detection.models.length;
|
|
2647
2899
|
}
|
|
2648
|
-
const { logger, onStepFinish } = buildDefaultStepLogger("entity-audit",
|
|
2649
|
-
const
|
|
2900
|
+
const { logger, onStepFinish } = buildDefaultStepLogger("entity-audit", MAX_STEPS2);
|
|
2901
|
+
const scopeBlock = input.scopeHint != null ? `
|
|
2902
|
+
${input.scopeHint}
|
|
2903
|
+
` : "";
|
|
2904
|
+
const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + scopeBlock + formatRetryGuidance(input.retryGuidance);
|
|
2650
2905
|
const preRegBlock = preRegisteredCount > 0 ? `
|
|
2651
2906
|
## Pre-registered models (${preRegisteredCount} found via ${detection.framework} schema at ${detection.schemaFile})
|
|
2652
2907
|
|
|
@@ -2676,9 +2931,9 @@ After every 10 mark_model_audited calls, use write_file to update entity-audit.m
|
|
|
2676
2931
|
write_file already targets the output directory - use just the filename.`;
|
|
2677
2932
|
const agentConfig = {
|
|
2678
2933
|
id: "entity-audit",
|
|
2679
|
-
systemPrompt:
|
|
2934
|
+
systemPrompt: SYSTEM_PROMPT4,
|
|
2680
2935
|
model,
|
|
2681
|
-
maxSteps:
|
|
2936
|
+
maxSteps: MAX_STEPS2,
|
|
2682
2937
|
tools: async (heartbeat) => {
|
|
2683
2938
|
const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
|
|
2684
2939
|
return {
|
|
@@ -2706,8 +2961,8 @@ ${formatException(err)}`);
|
|
|
2706
2961
|
logger.summary();
|
|
2707
2962
|
const writeCanonicalAudit = async () => {
|
|
2708
2963
|
if (tracker.auditedModels.size === 0) return void 0;
|
|
2709
|
-
const auditPath =
|
|
2710
|
-
await
|
|
2964
|
+
const auditPath = join17(input.outputDir, "entity-audit.md");
|
|
2965
|
+
await writeFile6(auditPath, tracker.generateAuditMarkdown(), "utf-8");
|
|
2711
2966
|
return auditPath;
|
|
2712
2967
|
};
|
|
2713
2968
|
const canonicalPath = await writeCanonicalAudit();
|
|
@@ -2744,9 +2999,9 @@ When done with changes, call finish again.`;
|
|
|
2744
2999
|
}
|
|
2745
3000
|
});
|
|
2746
3001
|
if (!reviewed) {
|
|
2747
|
-
const auditPath =
|
|
3002
|
+
const auditPath = join17(input.outputDir, "entity-audit.md");
|
|
2748
3003
|
try {
|
|
2749
|
-
await
|
|
3004
|
+
await readFile11(auditPath, "utf-8");
|
|
2750
3005
|
return {
|
|
2751
3006
|
success: true,
|
|
2752
3007
|
artifacts: ["entity-audit.md"],
|
|
@@ -2762,7 +3017,7 @@ When done with changes, call finish again.`;
|
|
|
2762
3017
|
summary: agentError ? `Entity audit failed: ${agentError}` : "Entity audit agent stopped without producing entity-audit.md"
|
|
2763
3018
|
};
|
|
2764
3019
|
}
|
|
2765
|
-
var ModelTracker;
|
|
3020
|
+
var MAX_STEPS2, ModelTracker;
|
|
2766
3021
|
var init_entity_audit = __esm({
|
|
2767
3022
|
"src/agents/02-entity-audit/index.ts"() {
|
|
2768
3023
|
"use strict";
|
|
@@ -2777,6 +3032,7 @@ var init_entity_audit = __esm({
|
|
|
2777
3032
|
init_ask_user();
|
|
2778
3033
|
init_audit_table();
|
|
2779
3034
|
init_prompt2();
|
|
3035
|
+
MAX_STEPS2 = 400;
|
|
2780
3036
|
ModelTracker = class {
|
|
2781
3037
|
registered = /* @__PURE__ */ new Set();
|
|
2782
3038
|
auditedModels = /* @__PURE__ */ new Map();
|
|
@@ -2887,11 +3143,11 @@ ${duals.length > 0 ? duals.map((m) => `- **${m.name}** - standalone: ${m.creatio
|
|
|
2887
3143
|
});
|
|
2888
3144
|
|
|
2889
3145
|
// src/core/parse-entity-audit.ts
|
|
2890
|
-
import { readFile as
|
|
2891
|
-
import { join as
|
|
3146
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3147
|
+
import { join as join18 } from "path";
|
|
2892
3148
|
async function parseEntityNames(outputDir) {
|
|
2893
3149
|
try {
|
|
2894
|
-
const content = await
|
|
3150
|
+
const content = await readFile12(join18(outputDir, "entity-audit.md"), "utf-8");
|
|
2895
3151
|
const names = [];
|
|
2896
3152
|
for (const line of content.split("\n")) {
|
|
2897
3153
|
const match = line.match(/^\s+-\s+name:\s+(.+)$/);
|
|
@@ -2964,13 +3220,13 @@ values; the recipe builder generates the exact records from what you write.`;
|
|
|
2964
3220
|
});
|
|
2965
3221
|
|
|
2966
3222
|
// src/agents/03-scenario-recipe/scenario-table.ts
|
|
2967
|
-
import { readFile as
|
|
2968
|
-
import { join as
|
|
3223
|
+
import { readFile as readFile13 } from "fs/promises";
|
|
3224
|
+
import { join as join19 } from "path";
|
|
2969
3225
|
import matter3 from "gray-matter";
|
|
2970
3226
|
async function parseScenario(outputDir) {
|
|
2971
3227
|
let raw;
|
|
2972
3228
|
try {
|
|
2973
|
-
raw = await
|
|
3229
|
+
raw = await readFile13(join19(outputDir, "scenarios.md"), "utf-8");
|
|
2974
3230
|
} catch {
|
|
2975
3231
|
return { scenarioNames: [], entityTypes: [] };
|
|
2976
3232
|
}
|
|
@@ -3051,22 +3307,22 @@ __export(scenario_recipe_exports, {
|
|
|
3051
3307
|
feedbackToScenario: () => feedbackToScenario,
|
|
3052
3308
|
runScenarioRecipe: () => runScenarioRecipe
|
|
3053
3309
|
});
|
|
3054
|
-
import { readFile as
|
|
3055
|
-
import { join as
|
|
3056
|
-
import { tool as
|
|
3057
|
-
import { z as
|
|
3310
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
3311
|
+
import { join as join20 } from "path";
|
|
3312
|
+
import { tool as tool13 } from "ai";
|
|
3313
|
+
import { z as z16 } from "zod";
|
|
3058
3314
|
function buildFinishTool3(requiredEntities, outputDir, onFinish) {
|
|
3059
|
-
return
|
|
3315
|
+
return tool13({
|
|
3060
3316
|
description: "Call when scenario design is complete and scenarios.md is written. BLOCKED if any required entities are missing from the scenario.",
|
|
3061
|
-
inputSchema:
|
|
3062
|
-
summary:
|
|
3063
|
-
entityCount:
|
|
3064
|
-
artifacts:
|
|
3317
|
+
inputSchema: z16.object({
|
|
3318
|
+
summary: z16.string().describe("Summary of the scenario"),
|
|
3319
|
+
entityCount: z16.number().describe("Number of entity types in the scenario"),
|
|
3320
|
+
artifacts: z16.array(z16.string()).describe("Files written")
|
|
3065
3321
|
}),
|
|
3066
3322
|
execute: async (input) => {
|
|
3067
3323
|
let content;
|
|
3068
3324
|
try {
|
|
3069
|
-
content = await
|
|
3325
|
+
content = await readFile14(join20(outputDir, "scenarios.md"), "utf-8");
|
|
3070
3326
|
} catch {
|
|
3071
3327
|
return { error: "Cannot finish: scenarios.md not found. Write it first." };
|
|
3072
3328
|
}
|
|
@@ -3101,7 +3357,10 @@ async function runScenarioRecipe(input) {
|
|
|
3101
3357
|
const model = getModel(input.modelId);
|
|
3102
3358
|
let result;
|
|
3103
3359
|
const { logger, onStepFinish } = buildDefaultStepLogger("scenario", 40);
|
|
3104
|
-
const
|
|
3360
|
+
const scopeBlock = input.scopeHint != null ? `
|
|
3361
|
+
${input.scopeHint}
|
|
3362
|
+
` : "";
|
|
3363
|
+
const contextBlock = (input.projectContext ? "\n" + formatContext(input.projectContext) + "\n" : "") + scopeBlock + formatRetryGuidance(input.retryGuidance);
|
|
3105
3364
|
const requiredEntities = await parseEntityNames(input.outputDir);
|
|
3106
3365
|
const entityListBlock = requiredEntities.length > 0 ? `
|
|
3107
3366
|
## Required entities (${requiredEntities.length} total - ALL must appear in the scenario)
|
|
@@ -3163,9 +3422,9 @@ When done with changes, call finish again.`;
|
|
|
3163
3422
|
}
|
|
3164
3423
|
});
|
|
3165
3424
|
if (!reviewed) {
|
|
3166
|
-
const scenariosPath =
|
|
3425
|
+
const scenariosPath = join20(input.outputDir, "scenarios.md");
|
|
3167
3426
|
try {
|
|
3168
|
-
await
|
|
3427
|
+
await readFile14(scenariosPath, "utf-8");
|
|
3169
3428
|
return {
|
|
3170
3429
|
success: true,
|
|
3171
3430
|
artifacts: ["scenarios.md"],
|
|
@@ -3226,7 +3485,7 @@ var init_scenario_recipe = __esm({
|
|
|
3226
3485
|
|
|
3227
3486
|
// src/agents/04-recipe-builder/entity-relevance.ts
|
|
3228
3487
|
import { Output, generateText } from "ai";
|
|
3229
|
-
import { z as
|
|
3488
|
+
import { z as z17 } from "zod";
|
|
3230
3489
|
async function callRanker(model, prompt) {
|
|
3231
3490
|
const result = await generateText({
|
|
3232
3491
|
model,
|
|
@@ -3314,19 +3573,19 @@ var init_entity_relevance = __esm({
|
|
|
3314
3573
|
init_esm_shims();
|
|
3315
3574
|
init_errors();
|
|
3316
3575
|
init_model();
|
|
3317
|
-
rankedSchema =
|
|
3318
|
-
ranked:
|
|
3576
|
+
rankedSchema = z17.object({
|
|
3577
|
+
ranked: z17.array(z17.string()).describe("Every entity name, ordered most-important first.")
|
|
3319
3578
|
});
|
|
3320
3579
|
}
|
|
3321
3580
|
});
|
|
3322
3581
|
|
|
3323
3582
|
// src/core/detect-pkg-manager.ts
|
|
3324
3583
|
import { existsSync as existsSync2 } from "fs";
|
|
3325
|
-
import { join as
|
|
3584
|
+
import { join as join21 } from "path";
|
|
3326
3585
|
function detectPackageManager(projectRoot) {
|
|
3327
|
-
if (existsSync2(
|
|
3328
|
-
if (existsSync2(
|
|
3329
|
-
if (existsSync2(
|
|
3586
|
+
if (existsSync2(join21(projectRoot, "bun.lock")) || existsSync2(join21(projectRoot, "bun.lockb"))) return "bun";
|
|
3587
|
+
if (existsSync2(join21(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
3588
|
+
if (existsSync2(join21(projectRoot, "yarn.lock"))) return "yarn";
|
|
3330
3589
|
return "npm";
|
|
3331
3590
|
}
|
|
3332
3591
|
function installCommand(pm, ...packages) {
|
|
@@ -3597,8 +3856,8 @@ var init_discover_schema = __esm({
|
|
|
3597
3856
|
});
|
|
3598
3857
|
|
|
3599
3858
|
// src/agents/04-recipe-builder/recipe.ts
|
|
3600
|
-
import { readFile as
|
|
3601
|
-
import { join as
|
|
3859
|
+
import { readFile as readFile15, writeFile as writeFile7 } from "fs/promises";
|
|
3860
|
+
import { join as join22 } from "path";
|
|
3602
3861
|
function collectRefs2(value, out) {
|
|
3603
3862
|
if (Array.isArray(value)) {
|
|
3604
3863
|
for (const v of value) collectRefs2(v, out);
|
|
@@ -3671,7 +3930,7 @@ function buildSubmittableRecipe(create, description) {
|
|
|
3671
3930
|
};
|
|
3672
3931
|
}
|
|
3673
3932
|
async function saveRecipe(outputDir, recipe) {
|
|
3674
|
-
await
|
|
3933
|
+
await writeFile7(join22(outputDir, RECIPE_FILE), JSON.stringify(recipe, null, 2), "utf-8");
|
|
3675
3934
|
}
|
|
3676
3935
|
var RECIPE_FILE;
|
|
3677
3936
|
var init_recipe = __esm({
|
|
@@ -3684,8 +3943,8 @@ var init_recipe = __esm({
|
|
|
3684
3943
|
});
|
|
3685
3944
|
|
|
3686
3945
|
// src/agents/04-recipe-builder/state.ts
|
|
3687
|
-
import { readFile as
|
|
3688
|
-
import { join as
|
|
3946
|
+
import { readFile as readFile16, writeFile as writeFile8 } from "fs/promises";
|
|
3947
|
+
import { join as join23 } from "path";
|
|
3689
3948
|
function adapterKey(a) {
|
|
3690
3949
|
return `${a.language}:${a.framework}`;
|
|
3691
3950
|
}
|
|
@@ -3707,7 +3966,7 @@ function initialRecipeState() {
|
|
|
3707
3966
|
}
|
|
3708
3967
|
async function loadRecipeState(outputDir) {
|
|
3709
3968
|
try {
|
|
3710
|
-
const raw = await
|
|
3969
|
+
const raw = await readFile16(join23(outputDir, STATE_FILE2), "utf-8");
|
|
3711
3970
|
const parsed = JSON.parse(raw);
|
|
3712
3971
|
return parsed;
|
|
3713
3972
|
} catch {
|
|
@@ -3715,7 +3974,7 @@ async function loadRecipeState(outputDir) {
|
|
|
3715
3974
|
}
|
|
3716
3975
|
}
|
|
3717
3976
|
async function saveRecipeState(outputDir, state) {
|
|
3718
|
-
await
|
|
3977
|
+
await writeFile8(join23(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
|
|
3719
3978
|
}
|
|
3720
3979
|
var ALL_ADAPTERS, ADAPTER_HINTS, STATE_FILE2;
|
|
3721
3980
|
var init_state = __esm({
|
|
@@ -3787,7 +4046,7 @@ var init_state = __esm({
|
|
|
3787
4046
|
|
|
3788
4047
|
// src/agents/04-recipe-builder/phases/failure-classifier.ts
|
|
3789
4048
|
import { Output as Output2, generateText as generateText2 } from "ai";
|
|
3790
|
-
import { z as
|
|
4049
|
+
import { z as z18 } from "zod";
|
|
3791
4050
|
function buildClassifierPrompt(args) {
|
|
3792
4051
|
const errorText = typeof args.error === "string" ? args.error : JSON.stringify(args.error, null, 2);
|
|
3793
4052
|
const phaseLine = args.phase === "teardown" ? `This was a DOWN (teardown) request. Teardown runs the developer's delete logic against data the create() step already accepted, so teardown failures are usually implementation-side (wrong delete order, foreign-key cleanup bugs) rather than caused by the recipe.` : `This was an UP (create) request - the factory tried to insert the recipe records.`;
|
|
@@ -3847,11 +4106,11 @@ var init_failure_classifier = __esm({
|
|
|
3847
4106
|
init_esm_shims();
|
|
3848
4107
|
init_errors();
|
|
3849
4108
|
init_model();
|
|
3850
|
-
classificationSchema =
|
|
3851
|
-
side:
|
|
4109
|
+
classificationSchema = z18.object({
|
|
4110
|
+
side: z18.enum(["recipe", "implementation", "unclear"]).describe(
|
|
3852
4111
|
"recipe = the test data we sent is wrong and regenerating it could fix the failure; implementation = the developer's handler/factory code is wrong and only a code change fixes it; unclear = cannot confidently attribute the failure to either side."
|
|
3853
4112
|
),
|
|
3854
|
-
reason:
|
|
4113
|
+
reason: z18.string().describe("One short, plain-language sentence explaining the verdict for the user. No code, no jargon dumps.")
|
|
3855
4114
|
});
|
|
3856
4115
|
PRIMER = `## Background - what you are looking at
|
|
3857
4116
|
|
|
@@ -3866,13 +4125,13 @@ So a failure has exactly two possible origins, and your only job is to tell them
|
|
|
3866
4125
|
});
|
|
3867
4126
|
|
|
3868
4127
|
// src/agents/04-recipe-builder/phases/entity-loop.ts
|
|
3869
|
-
import { writeFile as
|
|
4128
|
+
import { writeFile as writeFile9, readFile as readFile17 } from "fs/promises";
|
|
3870
4129
|
import { tmpdir } from "os";
|
|
3871
|
-
import { join as
|
|
4130
|
+
import { join as join24 } from "path";
|
|
3872
4131
|
import * as p4 from "@clack/prompts";
|
|
3873
|
-
import { tool as
|
|
4132
|
+
import { tool as tool14 } from "ai";
|
|
3874
4133
|
import spawn2 from "cross-spawn";
|
|
3875
|
-
import { z as
|
|
4134
|
+
import { z as z19 } from "zod";
|
|
3876
4135
|
function summarizeCompletedAliases(completedEntities, excludeName) {
|
|
3877
4136
|
return Object.entries(completedEntities).filter(([name, e]) => name !== excludeName && e.recipeData && e.recipeData.length > 0).map(([name, e]) => `${name}: aliases ${e.recipeData.map((r) => r._alias ?? "?").join(", ")}`).join("\n");
|
|
3878
4137
|
}
|
|
@@ -3887,10 +4146,10 @@ function summarizeEntityAudit(model) {
|
|
|
3887
4146
|
async function proposeRecipeData(entityName, entityIndex, totalEntities, model, outputDir, _projectRoot, completedEntities, schemaSpec) {
|
|
3888
4147
|
let result;
|
|
3889
4148
|
const { logger, onStepFinish } = buildDefaultStepLogger(`propose:${entityName}`, 20);
|
|
3890
|
-
const finishTool =
|
|
4149
|
+
const finishTool = tool14({
|
|
3891
4150
|
description: "Submit the proposed recipe data as a JSON array of records.",
|
|
3892
|
-
inputSchema:
|
|
3893
|
-
records:
|
|
4151
|
+
inputSchema: z19.object({
|
|
4152
|
+
records: z19.array(z19.record(z19.string(), z19.unknown())).describe("Array of record objects for this entity")
|
|
3894
4153
|
}),
|
|
3895
4154
|
execute: async (input) => {
|
|
3896
4155
|
result = input.records;
|
|
@@ -3933,10 +4192,10 @@ Call finish with the JSON array of records.`;
|
|
|
3933
4192
|
}
|
|
3934
4193
|
async function reviseRecipeData(entityName, entityIndex, totalEntities, current, feedback, model, outputDir, completedEntities, schemaSpec) {
|
|
3935
4194
|
let revised;
|
|
3936
|
-
const finishTool =
|
|
4195
|
+
const finishTool = tool14({
|
|
3937
4196
|
description: "Submit the fixed recipe data.",
|
|
3938
|
-
inputSchema:
|
|
3939
|
-
records:
|
|
4197
|
+
inputSchema: z19.object({
|
|
4198
|
+
records: z19.array(z19.record(z19.string(), z19.unknown()))
|
|
3940
4199
|
}),
|
|
3941
4200
|
execute: async (input) => {
|
|
3942
4201
|
revised = input.records;
|
|
@@ -3994,10 +4253,10 @@ Read scenarios.md and entity-audit.md to understand the correct aliases and sche
|
|
|
3994
4253
|
async function generateInstructions(entityName, entityIndex, totalEntities, isFirst, techStack, auditModel, recipeData, model, projectRoot, outputDir) {
|
|
3995
4254
|
let result;
|
|
3996
4255
|
const { logger, onStepFinish } = buildDefaultStepLogger(`instructions:${entityName}`, 15);
|
|
3997
|
-
const finishTool =
|
|
4256
|
+
const finishTool = tool14({
|
|
3998
4257
|
description: "Submit the implementation instructions.",
|
|
3999
|
-
inputSchema:
|
|
4000
|
-
instructions:
|
|
4258
|
+
inputSchema: z19.object({
|
|
4259
|
+
instructions: z19.string().describe("Complete, copy-pasteable implementation instructions")
|
|
4001
4260
|
}),
|
|
4002
4261
|
execute: async (input) => {
|
|
4003
4262
|
result = input.instructions;
|
|
@@ -4079,23 +4338,23 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
|
|
|
4079
4338
|
if (p4.isCancel(action)) throw new Error("Recipe review cancelled");
|
|
4080
4339
|
if (action === "keep") return proposed;
|
|
4081
4340
|
if (action === "edit") {
|
|
4082
|
-
const tmpPath =
|
|
4083
|
-
await
|
|
4341
|
+
const tmpPath = join24(tmpdir(), `autonoma-recipe-${entityName}.json`);
|
|
4342
|
+
await writeFile9(tmpPath, JSON.stringify(proposed, null, 2), "utf-8");
|
|
4084
4343
|
const env = readEnv();
|
|
4085
4344
|
const editor = env.EDITOR ?? env.VISUAL ?? "vi";
|
|
4086
4345
|
p4.log.info(`Opening ${editor}... Save and close when done.`);
|
|
4087
|
-
const launched = await new Promise((
|
|
4346
|
+
const launched = await new Promise((resolve6) => {
|
|
4088
4347
|
const proc = spawn2(editor, [tmpPath], { stdio: "inherit" });
|
|
4089
|
-
proc.on("close", () =>
|
|
4348
|
+
proc.on("close", () => resolve6(true));
|
|
4090
4349
|
proc.on("error", (err) => {
|
|
4091
4350
|
p4.log.error(
|
|
4092
4351
|
`Couldn't open ${editor} (${err.message}). Edit this file manually, then choose "edit" again: ${tmpPath}`
|
|
4093
4352
|
);
|
|
4094
|
-
|
|
4353
|
+
resolve6(false);
|
|
4095
4354
|
});
|
|
4096
4355
|
});
|
|
4097
4356
|
if (!launched) continue;
|
|
4098
|
-
const edited = await
|
|
4357
|
+
const edited = await readFile17(tmpPath, "utf-8");
|
|
4099
4358
|
try {
|
|
4100
4359
|
proposed = JSON.parse(edited);
|
|
4101
4360
|
p4.note(JSON.stringify(proposed, null, 2), `Updated data for ${entityName}`, { format: codeNoteFormat });
|
|
@@ -4387,8 +4646,8 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
|
|
|
4387
4646
|
const secret = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
4388
4647
|
state.sharedSecret = secret;
|
|
4389
4648
|
await saveRecipeState(outputDir, state);
|
|
4390
|
-
await
|
|
4391
|
-
|
|
4649
|
+
await writeFile9(
|
|
4650
|
+
join24(outputDir, "autonoma-config.json"),
|
|
4392
4651
|
JSON.stringify({ sharedSecret: secret, endpointUrl: state.sdkEndpointUrl }, null, 2),
|
|
4393
4652
|
"utf-8"
|
|
4394
4653
|
);
|
|
@@ -4399,7 +4658,7 @@ Add this to your server's .env file and restart it.
|
|
|
4399
4658
|
This is a 64-character hex key used for HMAC-SHA256 request signing.
|
|
4400
4659
|
The same value must be set in both your server and the Autonoma dashboard.
|
|
4401
4660
|
|
|
4402
|
-
Saved to: ${
|
|
4661
|
+
Saved to: ${join24(outputDir, "autonoma-config.json")}`,
|
|
4403
4662
|
"Shared secret generated"
|
|
4404
4663
|
);
|
|
4405
4664
|
const secretReady = await p4.confirm({
|
|
@@ -4420,8 +4679,8 @@ Saved to: ${join23(outputDir, "autonoma-config.json")}`,
|
|
|
4420
4679
|
if (p4.isCancel(url)) throw new Error("Entity loop cancelled");
|
|
4421
4680
|
state.sdkEndpointUrl = url.trim() || "http://localhost:3000/api/autonoma";
|
|
4422
4681
|
await saveRecipeState(outputDir, state);
|
|
4423
|
-
await
|
|
4424
|
-
|
|
4682
|
+
await writeFile9(
|
|
4683
|
+
join24(outputDir, "autonoma-config.json"),
|
|
4425
4684
|
JSON.stringify({ sharedSecret: state.sharedSecret, endpointUrl: state.sdkEndpointUrl }, null, 2),
|
|
4426
4685
|
"utf-8"
|
|
4427
4686
|
);
|
|
@@ -4537,14 +4796,14 @@ When done, call finish with the instructions text.`;
|
|
|
4537
4796
|
|
|
4538
4797
|
// src/agents/04-recipe-builder/phases/full-validation.ts
|
|
4539
4798
|
import * as p5 from "@clack/prompts";
|
|
4540
|
-
import { tool as
|
|
4541
|
-
import { z as
|
|
4799
|
+
import { tool as tool15 } from "ai";
|
|
4800
|
+
import { z as z20 } from "zod";
|
|
4542
4801
|
async function reviseFullRecipe(current, feedback, model, outputDir, entityOrder, schemaSpec) {
|
|
4543
4802
|
let revised;
|
|
4544
|
-
const finishTool =
|
|
4803
|
+
const finishTool = tool15({
|
|
4545
4804
|
description: "Submit the revised full recipe: an object mapping each entity name to its array of records.",
|
|
4546
|
-
inputSchema:
|
|
4547
|
-
recipe:
|
|
4805
|
+
inputSchema: z20.object({
|
|
4806
|
+
recipe: z20.record(z20.string(), z20.array(z20.record(z20.string(), z20.unknown())))
|
|
4548
4807
|
}),
|
|
4549
4808
|
execute: async (input) => {
|
|
4550
4809
|
revised = input.recipe;
|
|
@@ -4799,18 +5058,18 @@ var init_submit = __esm({
|
|
|
4799
5058
|
|
|
4800
5059
|
// src/agents/04-recipe-builder/phases/tech-detect.ts
|
|
4801
5060
|
import * as p7 from "@clack/prompts";
|
|
4802
|
-
import { tool as
|
|
4803
|
-
import { z as
|
|
5061
|
+
import { tool as tool16 } from "ai";
|
|
5062
|
+
import { z as z21 } from "zod";
|
|
4804
5063
|
async function detectTechStack(projectRoot, modelId, nonInteractive) {
|
|
4805
5064
|
const model = getModel(modelId);
|
|
4806
5065
|
const ignorePatterns = await loadGitignorePatterns(projectRoot);
|
|
4807
5066
|
let detected;
|
|
4808
5067
|
const { logger, onStepFinish } = buildDefaultStepLogger("tech-detect", 10);
|
|
4809
|
-
const finishTool =
|
|
5068
|
+
const finishTool = tool16({
|
|
4810
5069
|
description: "Report the detected backend technology stack.",
|
|
4811
|
-
inputSchema:
|
|
4812
|
-
language:
|
|
4813
|
-
framework:
|
|
5070
|
+
inputSchema: z21.object({
|
|
5071
|
+
language: z21.string().describe("Programming language: typescript, python, go, ruby, java, php, rust, elixir"),
|
|
5072
|
+
framework: z21.string().describe(
|
|
4814
5073
|
"Web framework: express, node, hono, web, flask, fastapi, django, gin, rails, rack, spring, laravel, axum, actix, plug"
|
|
4815
5074
|
)
|
|
4816
5075
|
}),
|
|
@@ -4821,7 +5080,7 @@ async function detectTechStack(projectRoot, modelId, nonInteractive) {
|
|
|
4821
5080
|
});
|
|
4822
5081
|
const agentConfig = {
|
|
4823
5082
|
id: "tech-detect",
|
|
4824
|
-
systemPrompt:
|
|
5083
|
+
systemPrompt: SYSTEM_PROMPT5,
|
|
4825
5084
|
model,
|
|
4826
5085
|
maxSteps: 10,
|
|
4827
5086
|
tools: (_heartbeat) => ({
|
|
@@ -4877,7 +5136,7 @@ async function detectTechStack(projectRoot, modelId, nonInteractive) {
|
|
|
4877
5136
|
p7.log.success(`Using ${adapterLabel(adapter)} - SDK: ${adapter.sdkPackage}, Adapter: ${adapter.adapterPackage}`);
|
|
4878
5137
|
return adapter;
|
|
4879
5138
|
}
|
|
4880
|
-
var DOCS_BASE,
|
|
5139
|
+
var DOCS_BASE, SYSTEM_PROMPT5;
|
|
4881
5140
|
var init_tech_detect = __esm({
|
|
4882
5141
|
"src/agents/04-recipe-builder/phases/tech-detect.ts"() {
|
|
4883
5142
|
"use strict";
|
|
@@ -4888,7 +5147,7 @@ var init_tech_detect = __esm({
|
|
|
4888
5147
|
init_tools();
|
|
4889
5148
|
init_state();
|
|
4890
5149
|
DOCS_BASE = "https://docs.autonoma.app";
|
|
4891
|
-
|
|
5150
|
+
SYSTEM_PROMPT5 = `You are a backend technology detector. Your job is to identify the programming language and web framework used by a project's backend/API server.
|
|
4892
5151
|
|
|
4893
5152
|
Explore the project files to detect:
|
|
4894
5153
|
1. The programming language (check package.json, requirements.txt, go.mod, Gemfile, pom.xml, composer.json, Cargo.toml, mix.exs)
|
|
@@ -4903,8 +5162,8 @@ var recipe_builder_exports = {};
|
|
|
4903
5162
|
__export(recipe_builder_exports, {
|
|
4904
5163
|
runRecipeBuilder: () => runRecipeBuilder
|
|
4905
5164
|
});
|
|
4906
|
-
import { readFile as
|
|
4907
|
-
import { join as
|
|
5165
|
+
import { readFile as readFile18 } from "fs/promises";
|
|
5166
|
+
import { join as join25 } from "path";
|
|
4908
5167
|
import * as p8 from "@clack/prompts";
|
|
4909
5168
|
async function runRecipeBuilder(input) {
|
|
4910
5169
|
const model = getModel(input.modelId);
|
|
@@ -4918,7 +5177,7 @@ async function runRecipeBuilder(input) {
|
|
|
4918
5177
|
state.techStack = await detectTechStack(input.projectRoot, input.modelId, input.nonInteractive);
|
|
4919
5178
|
let importanceRank;
|
|
4920
5179
|
try {
|
|
4921
|
-
const auditMarkdown = await
|
|
5180
|
+
const auditMarkdown = await readFile18(join25(input.outputDir, "entity-audit.md"), "utf-8");
|
|
4922
5181
|
importanceRank = await rankEntitiesByImportance(models, auditMarkdown, model);
|
|
4923
5182
|
} catch {
|
|
4924
5183
|
importanceRank = void 0;
|
|
@@ -5014,21 +5273,21 @@ var init_recipe_builder = __esm({
|
|
|
5014
5273
|
});
|
|
5015
5274
|
|
|
5016
5275
|
// src/agents/05-test-generator/rubrics.ts
|
|
5017
|
-
import { z as
|
|
5276
|
+
import { z as z22 } from "zod";
|
|
5018
5277
|
function reviewResultSchema(shape) {
|
|
5019
|
-
return
|
|
5278
|
+
return z22.object(shape);
|
|
5020
5279
|
}
|
|
5021
5280
|
var dimensionResultSchema, reviewResultRecordSchema, structuralIntentRubric, flowCompletenessRubric, uiTextRubric, dataAccuracyRubric, ALL_RUBRICS;
|
|
5022
5281
|
var init_rubrics = __esm({
|
|
5023
5282
|
"src/agents/05-test-generator/rubrics.ts"() {
|
|
5024
5283
|
"use strict";
|
|
5025
5284
|
init_esm_shims();
|
|
5026
|
-
dimensionResultSchema =
|
|
5027
|
-
pass:
|
|
5028
|
-
evidence:
|
|
5029
|
-
suggestion:
|
|
5285
|
+
dimensionResultSchema = z22.object({
|
|
5286
|
+
pass: z22.boolean(),
|
|
5287
|
+
evidence: z22.string().describe("What you checked and found - cite file paths, line content, or specific strings"),
|
|
5288
|
+
suggestion: z22.string().optional().describe("What the planner agent should fix, if failing")
|
|
5030
5289
|
});
|
|
5031
|
-
reviewResultRecordSchema =
|
|
5290
|
+
reviewResultRecordSchema = z22.record(z22.string(), dimensionResultSchema);
|
|
5032
5291
|
structuralIntentRubric = {
|
|
5033
5292
|
name: "structural-intent",
|
|
5034
5293
|
maxSteps: 8,
|
|
@@ -5202,12 +5461,12 @@ When done reviewing, call finish with your structured evaluation.`
|
|
|
5202
5461
|
// src/agents/05-test-generator/review-pass.ts
|
|
5203
5462
|
import { basename as basename2 } from "path";
|
|
5204
5463
|
import "ai";
|
|
5205
|
-
import { tool as
|
|
5464
|
+
import { tool as tool17 } from "ai";
|
|
5206
5465
|
async function runReviewPass(testContent, testPath, rubric, projectRoot, model, scenarioData) {
|
|
5207
5466
|
let result;
|
|
5208
5467
|
const agentLabel = `review:${rubric.name}:${basename2(testPath)}`;
|
|
5209
5468
|
const { onStepFinish } = buildDefaultStepLogger(agentLabel, rubric.maxSteps);
|
|
5210
|
-
const finishTool =
|
|
5469
|
+
const finishTool = tool17({
|
|
5211
5470
|
description: "Submit your structured review. Every dimension must have evidence from your investigation.",
|
|
5212
5471
|
inputSchema: rubric.resultSchema,
|
|
5213
5472
|
execute: async (input) => {
|
|
@@ -5265,8 +5524,8 @@ var init_review_pass = __esm({
|
|
|
5265
5524
|
});
|
|
5266
5525
|
|
|
5267
5526
|
// src/agents/05-test-generator/review.ts
|
|
5268
|
-
import { readFile as
|
|
5269
|
-
import { join as
|
|
5527
|
+
import { readFile as readFile19 } from "fs/promises";
|
|
5528
|
+
import { join as join26, relative as relative6, basename as basename3 } from "path";
|
|
5270
5529
|
import "ai";
|
|
5271
5530
|
import { glob as glob5 } from "glob";
|
|
5272
5531
|
async function reviewSingleTest(testContent, testPath, projectRoot, model, scenarioData) {
|
|
@@ -5295,19 +5554,19 @@ async function reviewSingleTest(testContent, testPath, projectRoot, model, scena
|
|
|
5295
5554
|
return merged;
|
|
5296
5555
|
}
|
|
5297
5556
|
async function runConsolidatedReview(outputDir, projectRoot, model) {
|
|
5298
|
-
const testsDir =
|
|
5557
|
+
const testsDir = join26(outputDir, "qa-tests");
|
|
5299
5558
|
const logger = createStepLogger("review", 5);
|
|
5300
5559
|
let scenarioData;
|
|
5301
5560
|
try {
|
|
5302
|
-
scenarioData = await
|
|
5561
|
+
scenarioData = await readFile19(join26(outputDir, "scenarios.md"), "utf-8");
|
|
5303
5562
|
} catch {
|
|
5304
5563
|
}
|
|
5305
|
-
const testFiles = await glob5(
|
|
5564
|
+
const testFiles = await glob5(join26(testsDir, "**/*.md"));
|
|
5306
5565
|
const tests = [];
|
|
5307
5566
|
for (const testPath of testFiles) {
|
|
5308
5567
|
if (basename3(testPath) === "INDEX.md") continue;
|
|
5309
5568
|
if (testPath.includes("/_invalid/")) continue;
|
|
5310
|
-
const content = await
|
|
5569
|
+
const content = await readFile19(testPath, "utf-8");
|
|
5311
5570
|
const flowMatch = content.match(/^---\n[\s\S]*?flow:\s*["']?([^"'\n]+)["']?\s*\n[\s\S]*?---/m);
|
|
5312
5571
|
tests.push({
|
|
5313
5572
|
path: testPath,
|
|
@@ -5390,17 +5649,17 @@ var init_review2 = __esm({
|
|
|
5390
5649
|
});
|
|
5391
5650
|
|
|
5392
5651
|
// src/agents/00b-feature-discovery/index.ts
|
|
5393
|
-
import { readFile as
|
|
5394
|
-
import { join as
|
|
5395
|
-
import { tool as
|
|
5396
|
-
import { z as
|
|
5652
|
+
import { readFile as readFile20, writeFile as writeFile10 } from "fs/promises";
|
|
5653
|
+
import { join as join27 } from "path";
|
|
5654
|
+
import { tool as tool18 } from "ai";
|
|
5655
|
+
import { z as z23 } from "zod";
|
|
5397
5656
|
async function saveFeatures(outputDir, features) {
|
|
5398
5657
|
const obj = Object.fromEntries(features);
|
|
5399
|
-
await
|
|
5658
|
+
await writeFile10(join27(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
|
|
5400
5659
|
}
|
|
5401
5660
|
async function loadFeatures(outputDir) {
|
|
5402
5661
|
try {
|
|
5403
|
-
const raw = await
|
|
5662
|
+
const raw = await readFile20(join27(outputDir, FEATURES_FILE), "utf-8");
|
|
5404
5663
|
const obj = JSON.parse(raw);
|
|
5405
5664
|
return new Map(Object.entries(obj));
|
|
5406
5665
|
} catch {
|
|
@@ -5424,17 +5683,17 @@ ${pagesDescription}
|
|
|
5424
5683
|
Process every page. Call add_feature for each sub-feature you discover. When done, call finish.`;
|
|
5425
5684
|
const agentConfig = {
|
|
5426
5685
|
id: "feature-discovery",
|
|
5427
|
-
systemPrompt:
|
|
5686
|
+
systemPrompt: SYSTEM_PROMPT6,
|
|
5428
5687
|
model,
|
|
5429
5688
|
maxSteps: 300,
|
|
5430
5689
|
tools: async (heartbeat) => {
|
|
5431
5690
|
const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
|
|
5432
5691
|
return {
|
|
5433
5692
|
...tools,
|
|
5434
|
-
add_feature:
|
|
5693
|
+
add_feature: tool18({
|
|
5435
5694
|
description: "Add a discovered sub-feature",
|
|
5436
5695
|
inputSchema: Feature.extend({
|
|
5437
|
-
id:
|
|
5696
|
+
id: z23.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
|
|
5438
5697
|
}),
|
|
5439
5698
|
execute: (featureInput) => {
|
|
5440
5699
|
const { id, ...rest } = featureInput;
|
|
@@ -5446,19 +5705,19 @@ Process every page. Call add_feature for each sub-feature you discover. When don
|
|
|
5446
5705
|
return `Feature "${id}" added (${collector.features.size} total)`;
|
|
5447
5706
|
}
|
|
5448
5707
|
}),
|
|
5449
|
-
view_features:
|
|
5708
|
+
view_features: tool18({
|
|
5450
5709
|
description: "View all discovered features so far",
|
|
5451
|
-
inputSchema:
|
|
5710
|
+
inputSchema: z23.object({}),
|
|
5452
5711
|
execute: () => collector.viewFeatures()
|
|
5453
5712
|
}),
|
|
5454
|
-
view_pages:
|
|
5713
|
+
view_pages: tool18({
|
|
5455
5714
|
description: "View the pages list to know what to analyze",
|
|
5456
|
-
inputSchema:
|
|
5715
|
+
inputSchema: z23.object({}),
|
|
5457
5716
|
execute: () => pagesDescription
|
|
5458
5717
|
}),
|
|
5459
|
-
finish:
|
|
5718
|
+
finish: tool18({
|
|
5460
5719
|
description: "Signal that feature discovery is complete",
|
|
5461
|
-
inputSchema:
|
|
5720
|
+
inputSchema: z23.object({ summary: z23.string() }),
|
|
5462
5721
|
execute: async (finishInput) => {
|
|
5463
5722
|
result = {
|
|
5464
5723
|
success: true,
|
|
@@ -5480,7 +5739,7 @@ Process every page. Call add_feature for each sub-feature you discover. When don
|
|
|
5480
5739
|
}
|
|
5481
5740
|
return collector.features;
|
|
5482
5741
|
}
|
|
5483
|
-
var FEATURES_FILE, Feature, FeatureCollector,
|
|
5742
|
+
var FEATURES_FILE, Feature, FeatureCollector, SYSTEM_PROMPT6;
|
|
5484
5743
|
var init_b_feature_discovery = __esm({
|
|
5485
5744
|
"src/agents/00b-feature-discovery/index.ts"() {
|
|
5486
5745
|
"use strict";
|
|
@@ -5489,13 +5748,13 @@ var init_b_feature_discovery = __esm({
|
|
|
5489
5748
|
init_model();
|
|
5490
5749
|
init_tools();
|
|
5491
5750
|
FEATURES_FILE = "features.json";
|
|
5492
|
-
Feature =
|
|
5493
|
-
name:
|
|
5494
|
-
type:
|
|
5495
|
-
parentPagePath:
|
|
5496
|
-
sourceFiles:
|
|
5497
|
-
interactiveElements:
|
|
5498
|
-
description:
|
|
5751
|
+
Feature = z23.object({
|
|
5752
|
+
name: z23.string().min(1).describe("Human-readable name (e.g. 'Settings > Notifications Tab', 'Create Project Modal')"),
|
|
5753
|
+
type: z23.enum(["tab", "modal", "form", "table", "wizard", "nested-route", "complex-component"]),
|
|
5754
|
+
parentPagePath: z23.string().min(1).describe("The page path this feature belongs to (from the pages list)"),
|
|
5755
|
+
sourceFiles: z23.array(z23.string()).min(1).describe("Relative paths to the source files for this sub-feature"),
|
|
5756
|
+
interactiveElements: z23.number().int().min(0).describe("Count of interactive elements found (buttons, inputs, toggles, etc.)"),
|
|
5757
|
+
description: z23.string().min(10).describe("What this sub-feature does")
|
|
5499
5758
|
});
|
|
5500
5759
|
FeatureCollector = class {
|
|
5501
5760
|
features = /* @__PURE__ */ new Map();
|
|
@@ -5526,7 +5785,7 @@ ${page}:`);
|
|
|
5526
5785
|
return lines.join("\n");
|
|
5527
5786
|
}
|
|
5528
5787
|
};
|
|
5529
|
-
|
|
5788
|
+
SYSTEM_PROMPT6 = `You are a feature discovery agent. Your job is to explore each page's source code and discover all sub-features that deserve their own test coverage.
|
|
5530
5789
|
|
|
5531
5790
|
You will be given a list of pages. For each page, you must:
|
|
5532
5791
|
1. Read the page's source file
|
|
@@ -5578,16 +5837,16 @@ Use kebab-case IDs that indicate the parent page and feature type:
|
|
|
5578
5837
|
});
|
|
5579
5838
|
|
|
5580
5839
|
// src/agents/05-test-generator/graph.ts
|
|
5581
|
-
import { readFile as
|
|
5582
|
-
import { join as
|
|
5840
|
+
import { readFile as readFile21, writeFile as writeFile11 } from "fs/promises";
|
|
5841
|
+
import { join as join28 } from "path";
|
|
5583
5842
|
async function saveBfsState(outputDir, state) {
|
|
5584
|
-
const path3 =
|
|
5585
|
-
await
|
|
5843
|
+
const path3 = join28(outputDir, STATE_FILE3);
|
|
5844
|
+
await writeFile11(path3, JSON.stringify(state.serialize(), null, 2), "utf-8");
|
|
5586
5845
|
}
|
|
5587
5846
|
async function loadBfsState(outputDir) {
|
|
5588
|
-
const path3 =
|
|
5847
|
+
const path3 = join28(outputDir, STATE_FILE3);
|
|
5589
5848
|
try {
|
|
5590
|
-
const raw = await
|
|
5849
|
+
const raw = await readFile21(path3, "utf-8");
|
|
5591
5850
|
return CoverageState.deserialize(JSON.parse(raw));
|
|
5592
5851
|
} catch {
|
|
5593
5852
|
return void 0;
|
|
@@ -5678,12 +5937,12 @@ var init_graph = __esm({
|
|
|
5678
5937
|
});
|
|
5679
5938
|
|
|
5680
5939
|
// src/agents/05-test-generator/prompt.ts
|
|
5681
|
-
var
|
|
5940
|
+
var SYSTEM_PROMPT7;
|
|
5682
5941
|
var init_prompt4 = __esm({
|
|
5683
5942
|
"src/agents/05-test-generator/prompt.ts"() {
|
|
5684
5943
|
"use strict";
|
|
5685
5944
|
init_esm_shims();
|
|
5686
|
-
|
|
5945
|
+
SYSTEM_PROMPT7 = `You are an E2E test generator that explores a frontend codebase as a BFS graph. You are ONE long-running agent that maintains all state about what's been explored and what hasn't.
|
|
5687
5946
|
|
|
5688
5947
|
## Your process
|
|
5689
5948
|
|
|
@@ -6114,11 +6373,11 @@ var init_validation = __esm({
|
|
|
6114
6373
|
});
|
|
6115
6374
|
|
|
6116
6375
|
// src/agents/05-test-generator/tools.ts
|
|
6117
|
-
import { mkdir as mkdir3, writeFile as
|
|
6118
|
-
import { dirname as dirname3, join as
|
|
6119
|
-
import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as
|
|
6376
|
+
import { mkdir as mkdir3, writeFile as writeFile12 } from "fs/promises";
|
|
6377
|
+
import { dirname as dirname3, join as join29 } from "path";
|
|
6378
|
+
import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as tool19, ToolLoopAgent as ToolLoopAgent3 } from "ai";
|
|
6120
6379
|
import matter5 from "gray-matter";
|
|
6121
|
-
import { z as
|
|
6380
|
+
import { z as z24 } from "zod";
|
|
6122
6381
|
function findForbiddenPlaceholder(stepsSection) {
|
|
6123
6382
|
const placeholderPatterns = [
|
|
6124
6383
|
{ pattern: /Dynamic:\s/gi, name: '"Dynamic:" placeholder' },
|
|
@@ -6136,13 +6395,13 @@ function findForbiddenPlaceholder(stepsSection) {
|
|
|
6136
6395
|
return void 0;
|
|
6137
6396
|
}
|
|
6138
6397
|
function buildWriteTestTool(state, outputDir) {
|
|
6139
|
-
return
|
|
6398
|
+
return tool19({
|
|
6140
6399
|
description: "Write a test file to qa-tests/{folder}/{filename}.md. Validates frontmatter before writing. Returns error if frontmatter is invalid.",
|
|
6141
|
-
inputSchema:
|
|
6142
|
-
folder:
|
|
6143
|
-
filename:
|
|
6144
|
-
content:
|
|
6145
|
-
nodeId:
|
|
6400
|
+
inputSchema: z24.object({
|
|
6401
|
+
folder: z24.string().describe("Subfolder name under qa-tests/"),
|
|
6402
|
+
filename: z24.string().describe("File name (e.g. login-valid-credentials.md)"),
|
|
6403
|
+
content: z24.string().describe("Full file content including YAML frontmatter"),
|
|
6404
|
+
nodeId: z24.string().describe("The FeatureNode ID this test belongs to")
|
|
6146
6405
|
}),
|
|
6147
6406
|
execute: async (input) => {
|
|
6148
6407
|
const frontmatter = extractFrontmatter(input.content);
|
|
@@ -6185,11 +6444,11 @@ function buildWriteTestTool(state, outputDir) {
|
|
|
6185
6444
|
error: `Test steps contain ${placeholder.name}: "${placeholder.match}". Use EXACT values from scenarios.md - not placeholders or examples.`
|
|
6186
6445
|
};
|
|
6187
6446
|
}
|
|
6188
|
-
const relPath =
|
|
6189
|
-
const absPath =
|
|
6447
|
+
const relPath = join29("qa-tests", input.folder, input.filename);
|
|
6448
|
+
const absPath = join29(outputDir, relPath);
|
|
6190
6449
|
try {
|
|
6191
6450
|
await mkdir3(dirname3(absPath), { recursive: true });
|
|
6192
|
-
await
|
|
6451
|
+
await writeFile12(absPath, input.content, "utf-8");
|
|
6193
6452
|
state.markTested(input.nodeId, [relPath]);
|
|
6194
6453
|
await saveBfsState(outputDir, state);
|
|
6195
6454
|
return { path: relPath, title: parsed.data.title };
|
|
@@ -6201,16 +6460,16 @@ function buildWriteTestTool(state, outputDir) {
|
|
|
6201
6460
|
});
|
|
6202
6461
|
}
|
|
6203
6462
|
function buildCreateFolderTool(outputDir) {
|
|
6204
|
-
return
|
|
6463
|
+
return tool19({
|
|
6205
6464
|
description: "Create a folder under qa-tests/ for organizing tests.",
|
|
6206
|
-
inputSchema:
|
|
6207
|
-
folder:
|
|
6465
|
+
inputSchema: z24.object({
|
|
6466
|
+
folder: z24.string().describe("Folder name (kebab-case)")
|
|
6208
6467
|
}),
|
|
6209
6468
|
execute: async (input) => {
|
|
6210
|
-
const absPath =
|
|
6469
|
+
const absPath = join29(outputDir, "qa-tests", input.folder);
|
|
6211
6470
|
try {
|
|
6212
6471
|
await mkdir3(absPath, { recursive: true });
|
|
6213
|
-
return { path:
|
|
6472
|
+
return { path: join29("qa-tests", input.folder) };
|
|
6214
6473
|
} catch (err) {
|
|
6215
6474
|
const message = err instanceof Error ? err.message : String(err);
|
|
6216
6475
|
return { error: `Failed to create folder: ${message}` };
|
|
@@ -6219,9 +6478,9 @@ function buildCreateFolderTool(outputDir) {
|
|
|
6219
6478
|
});
|
|
6220
6479
|
}
|
|
6221
6480
|
function buildNextNodeTool(state, outputDir) {
|
|
6222
|
-
return
|
|
6481
|
+
return tool19({
|
|
6223
6482
|
description: "Get the next node to write tests for. If you called next_node before without writing any tests (via write_test), the previous node is auto-skipped. Returns done:true when all nodes are processed.",
|
|
6224
|
-
inputSchema:
|
|
6483
|
+
inputSchema: z24.object({}),
|
|
6225
6484
|
execute: async () => {
|
|
6226
6485
|
const next = state.nextNode();
|
|
6227
6486
|
await saveBfsState(outputDir, state);
|
|
@@ -6248,9 +6507,9 @@ function buildNextNodeTool(state, outputDir) {
|
|
|
6248
6507
|
});
|
|
6249
6508
|
}
|
|
6250
6509
|
function buildGetProgressTool(state) {
|
|
6251
|
-
return
|
|
6510
|
+
return tool19({
|
|
6252
6511
|
description: "Check how many nodes have been tested vs how many remain.",
|
|
6253
|
-
inputSchema:
|
|
6512
|
+
inputSchema: z24.object({}),
|
|
6254
6513
|
execute: async () => {
|
|
6255
6514
|
const stats = state.summary();
|
|
6256
6515
|
const nodes = [...state.nodes.values()].map((n) => ({
|
|
@@ -6264,14 +6523,14 @@ function buildGetProgressTool(state) {
|
|
|
6264
6523
|
});
|
|
6265
6524
|
}
|
|
6266
6525
|
function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
|
|
6267
|
-
return
|
|
6526
|
+
return tool19({
|
|
6268
6527
|
description: "Spawn a research subagent to read and analyze source files without polluting your context. Use for complex sub-features where you don't want to read 20 files yourself.",
|
|
6269
|
-
inputSchema:
|
|
6270
|
-
instruction:
|
|
6528
|
+
inputSchema: z24.object({
|
|
6529
|
+
instruction: z24.string().describe("What to research - be specific about files and what to look for")
|
|
6271
6530
|
}),
|
|
6272
6531
|
execute: async (input) => {
|
|
6273
|
-
const resultSchema2 =
|
|
6274
|
-
findings:
|
|
6532
|
+
const resultSchema2 = z24.object({
|
|
6533
|
+
findings: z24.string().describe("Summary of what was found")
|
|
6275
6534
|
});
|
|
6276
6535
|
let result;
|
|
6277
6536
|
const subagent = new ToolLoopAgent3({
|
|
@@ -6283,7 +6542,7 @@ function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
|
|
|
6283
6542
|
glob: buildGlobTool(workingDirectory),
|
|
6284
6543
|
grep: buildGrepTool(workingDirectory),
|
|
6285
6544
|
read_file: buildReadFileTool(workingDirectory),
|
|
6286
|
-
finish:
|
|
6545
|
+
finish: tool19({
|
|
6287
6546
|
description: "Report your findings.",
|
|
6288
6547
|
inputSchema: resultSchema2,
|
|
6289
6548
|
execute: async (output) => {
|
|
@@ -6325,14 +6584,14 @@ var init_tools2 = __esm({
|
|
|
6325
6584
|
init_tools();
|
|
6326
6585
|
init_graph();
|
|
6327
6586
|
init_validation();
|
|
6328
|
-
testFrontmatterSchema =
|
|
6329
|
-
title:
|
|
6330
|
-
description:
|
|
6331
|
-
intent:
|
|
6332
|
-
criticality:
|
|
6333
|
-
scenario:
|
|
6334
|
-
flow:
|
|
6335
|
-
verification:
|
|
6587
|
+
testFrontmatterSchema = z24.object({
|
|
6588
|
+
title: z24.string().min(1),
|
|
6589
|
+
description: z24.string().min(1),
|
|
6590
|
+
intent: z24.string().min(30, "Intent must be at least 30 characters - describe the BEHAVIOR being tested, not the steps"),
|
|
6591
|
+
criticality: z24.enum(["critical", "high", "mid", "low"]),
|
|
6592
|
+
scenario: z24.string().min(1),
|
|
6593
|
+
flow: z24.string().min(1),
|
|
6594
|
+
verification: z24.string().min(
|
|
6336
6595
|
20,
|
|
6337
6596
|
"Verification must describe WHERE to navigate and WHAT to assert at the source of truth - not UI acknowledgments like toasts"
|
|
6338
6597
|
)
|
|
@@ -6345,10 +6604,10 @@ var test_generator_exports = {};
|
|
|
6345
6604
|
__export(test_generator_exports, {
|
|
6346
6605
|
runTestGenerator: () => runTestGenerator
|
|
6347
6606
|
});
|
|
6348
|
-
import { mkdir as mkdir4, readFile as
|
|
6349
|
-
import { basename as basename4, join as
|
|
6350
|
-
import { tool as
|
|
6351
|
-
import { z as
|
|
6607
|
+
import { mkdir as mkdir4, readFile as readFile22, rmdir, unlink, writeFile as writeFile13 } from "fs/promises";
|
|
6608
|
+
import { basename as basename4, join as join30 } from "path";
|
|
6609
|
+
import { tool as tool20 } from "ai";
|
|
6610
|
+
import { z as z25 } from "zod";
|
|
6352
6611
|
import { glob as glob6 } from "glob";
|
|
6353
6612
|
async function preseedQueue(state, projectRoot, pages, features) {
|
|
6354
6613
|
let seeded = 0;
|
|
@@ -6396,10 +6655,10 @@ async function runTestGenerator(input) {
|
|
|
6396
6655
|
const existingState = await loadBfsState(input.outputDir);
|
|
6397
6656
|
const state = existingState ?? new CoverageState();
|
|
6398
6657
|
let result;
|
|
6399
|
-
const finishTool =
|
|
6658
|
+
const finishTool = tool20({
|
|
6400
6659
|
description: "Call when the BFS queue is empty and all routes have been explored.",
|
|
6401
|
-
inputSchema:
|
|
6402
|
-
summary:
|
|
6660
|
+
inputSchema: z25.object({
|
|
6661
|
+
summary: z25.string().describe("Coverage summary")
|
|
6403
6662
|
}),
|
|
6404
6663
|
execute: async (finishInput) => {
|
|
6405
6664
|
const stats = state.summary();
|
|
@@ -6428,7 +6687,7 @@ async function runTestGenerator(input) {
|
|
|
6428
6687
|
});
|
|
6429
6688
|
let kbContext = "";
|
|
6430
6689
|
try {
|
|
6431
|
-
const autonomaMd = await
|
|
6690
|
+
const autonomaMd = await readFile22(join30(input.outputDir, "AUTONOMA.md"), "utf-8");
|
|
6432
6691
|
kbContext += `
|
|
6433
6692
|
## Knowledge Base (AUTONOMA.md)
|
|
6434
6693
|
|
|
@@ -6437,7 +6696,7 @@ ${autonomaMd}
|
|
|
6437
6696
|
} catch {
|
|
6438
6697
|
}
|
|
6439
6698
|
try {
|
|
6440
|
-
const scenariosMd = await
|
|
6699
|
+
const scenariosMd = await readFile22(join30(input.outputDir, "scenarios.md"), "utf-8");
|
|
6441
6700
|
kbContext += `
|
|
6442
6701
|
## Scenarios
|
|
6443
6702
|
|
|
@@ -6491,7 +6750,7 @@ Do NOT try to finish early. Process EVERY node via next_node until it returns do
|
|
|
6491
6750
|
const listDirectoryFn = await buildListDirectoryTool(input.projectRoot);
|
|
6492
6751
|
const agentConfig = {
|
|
6493
6752
|
id: "test-generator",
|
|
6494
|
-
systemPrompt:
|
|
6753
|
+
systemPrompt: SYSTEM_PROMPT7,
|
|
6495
6754
|
model,
|
|
6496
6755
|
maxSteps: CHUNK_STEPS,
|
|
6497
6756
|
temperature: 0.3,
|
|
@@ -6607,20 +6866,20 @@ IMPORTANT: Do NOT try to finish early. Process every node via next_node until it
|
|
|
6607
6866
|
}
|
|
6608
6867
|
console.log(` Fix pass complete`);
|
|
6609
6868
|
}
|
|
6610
|
-
const allTestFiles = await glob6(
|
|
6869
|
+
const allTestFiles = await glob6(join30(input.outputDir, "qa-tests", "**/*.md"));
|
|
6611
6870
|
let markedInvalid = 0;
|
|
6612
6871
|
for (const testPath of allTestFiles) {
|
|
6613
6872
|
if (basename4(testPath) === "INDEX.md") continue;
|
|
6614
6873
|
if (testPath.includes("/_invalid/")) continue;
|
|
6615
|
-
const content = await
|
|
6874
|
+
const content = await readFile22(testPath, "utf-8");
|
|
6616
6875
|
const validation = validateTestContent(content);
|
|
6617
6876
|
if (!validation.valid) {
|
|
6618
|
-
const invalidDir =
|
|
6877
|
+
const invalidDir = join30(input.outputDir, "qa-tests", "_invalid");
|
|
6619
6878
|
await mkdir4(invalidDir, { recursive: true });
|
|
6620
|
-
const dest =
|
|
6879
|
+
const dest = join30(invalidDir, basename4(testPath));
|
|
6621
6880
|
const annotated = `<!-- VALIDATION ERRORS: ${validation.errors.join("; ")} -->
|
|
6622
6881
|
${content}`;
|
|
6623
|
-
await
|
|
6882
|
+
await writeFile13(dest, annotated, "utf-8");
|
|
6624
6883
|
await unlink(testPath);
|
|
6625
6884
|
markedInvalid++;
|
|
6626
6885
|
}
|
|
@@ -6628,7 +6887,7 @@ ${content}`;
|
|
|
6628
6887
|
if (markedInvalid > 0) {
|
|
6629
6888
|
console.log(` ${markedInvalid} tests still invalid after review cycles - moved to _invalid/`);
|
|
6630
6889
|
}
|
|
6631
|
-
const dirs = await glob6(
|
|
6890
|
+
const dirs = await glob6(join30(input.outputDir, "qa-tests", "**/"), {
|
|
6632
6891
|
dot: false
|
|
6633
6892
|
});
|
|
6634
6893
|
for (const dir of dirs.sort((a, b) => b.length - a.length)) {
|
|
@@ -6720,7 +6979,7 @@ async function generateIndex(outputDir, state) {
|
|
|
6720
6979
|
for (const paths of state.testsWritten.values()) {
|
|
6721
6980
|
for (const p10 of paths) {
|
|
6722
6981
|
try {
|
|
6723
|
-
const content2 = await
|
|
6982
|
+
const content2 = await readFile22(join30(outputDir, p10), "utf-8");
|
|
6724
6983
|
const critMatch = content2.match(/criticality:\s*(\w+)/);
|
|
6725
6984
|
const critVal = critMatch?.[1] ?? "";
|
|
6726
6985
|
if (critCounts.has(critVal)) critCounts.set(critVal, (critCounts.get(critVal) ?? 0) + 1);
|
|
@@ -6765,28 +7024,28 @@ ${folders.map((f) => `| ${f.name} | ${f.test_count} |`).join("\n")}
|
|
|
6765
7024
|
|
|
6766
7025
|
${[...testsByFolder.entries()].flatMap(([_folder, tests]) => tests.map((t) => `- \`${t}\``)).join("\n")}
|
|
6767
7026
|
`;
|
|
6768
|
-
await
|
|
7027
|
+
await writeFile13(join30(outputDir, "qa-tests", "INDEX.md"), content, "utf-8");
|
|
6769
7028
|
}
|
|
6770
7029
|
async function generateJourneyTests(outputDir, model, projectRoot) {
|
|
6771
7030
|
const logger = createStepLogger("journeys", 50);
|
|
6772
7031
|
let autonomaMd = "";
|
|
6773
7032
|
let scenariosMd = "";
|
|
6774
7033
|
try {
|
|
6775
|
-
autonomaMd = await
|
|
7034
|
+
autonomaMd = await readFile22(join30(outputDir, "AUTONOMA.md"), "utf-8");
|
|
6776
7035
|
} catch (err) {
|
|
6777
7036
|
debugLog("AUTONOMA.md not present for journey generation", { err });
|
|
6778
7037
|
}
|
|
6779
7038
|
try {
|
|
6780
|
-
scenariosMd = await
|
|
7039
|
+
scenariosMd = await readFile22(join30(outputDir, "scenarios.md"), "utf-8");
|
|
6781
7040
|
} catch (err) {
|
|
6782
7041
|
debugLog("scenarios.md not present for journey generation", { err });
|
|
6783
7042
|
}
|
|
6784
7043
|
if (!autonomaMd) return 0;
|
|
6785
|
-
const existingTests = await glob6(
|
|
7044
|
+
const existingTests = await glob6(join30(outputDir, "qa-tests", "**/*.md"));
|
|
6786
7045
|
const existingTitles = [];
|
|
6787
7046
|
for (const t of existingTests) {
|
|
6788
7047
|
if (basename4(t) === "INDEX.md") continue;
|
|
6789
|
-
const content = await
|
|
7048
|
+
const content = await readFile22(t, "utf-8");
|
|
6790
7049
|
const titleMatch = content.match(/title:\s*"([^"]+)"/);
|
|
6791
7050
|
if (titleMatch) existingTitles.push(titleMatch[1]);
|
|
6792
7051
|
}
|
|
@@ -6829,9 +7088,9 @@ Write 5-8 journey tests using the write_test tool with folder "journeys". Then c
|
|
|
6829
7088
|
status: "queued"
|
|
6830
7089
|
});
|
|
6831
7090
|
let journeyResult;
|
|
6832
|
-
const journeyFinish =
|
|
7091
|
+
const journeyFinish = tool20({
|
|
6833
7092
|
description: "Signal journey generation is complete.",
|
|
6834
|
-
inputSchema:
|
|
7093
|
+
inputSchema: z25.object({ summary: z25.string() }),
|
|
6835
7094
|
execute: async (finishInput) => {
|
|
6836
7095
|
journeyResult = {
|
|
6837
7096
|
success: true,
|
|
@@ -6843,7 +7102,7 @@ Write 5-8 journey tests using the write_test tool with folder "journeys". Then c
|
|
|
6843
7102
|
});
|
|
6844
7103
|
const config = {
|
|
6845
7104
|
id: "journey-gen",
|
|
6846
|
-
systemPrompt:
|
|
7105
|
+
systemPrompt: SYSTEM_PROMPT7,
|
|
6847
7106
|
model,
|
|
6848
7107
|
maxSteps: 50,
|
|
6849
7108
|
temperature: 0.3,
|
|
@@ -6920,8 +7179,8 @@ function ensureSupportedNode() {
|
|
|
6920
7179
|
ensureSupportedNode();
|
|
6921
7180
|
|
|
6922
7181
|
// src/index.ts
|
|
6923
|
-
import { readFile as
|
|
6924
|
-
import { join as
|
|
7182
|
+
import { readFile as readFile23, writeFile as writeFile14 } from "fs/promises";
|
|
7183
|
+
import { join as join31 } from "path";
|
|
6925
7184
|
import * as p9 from "@clack/prompts";
|
|
6926
7185
|
|
|
6927
7186
|
// src/config.ts
|
|
@@ -7001,6 +7260,8 @@ function loadConfig(args) {
|
|
|
7001
7260
|
projectRoot,
|
|
7002
7261
|
projectSlug,
|
|
7003
7262
|
modelId: args.model ?? env.OPENROUTER_MODEL,
|
|
7263
|
+
frontend: args.frontend,
|
|
7264
|
+
backends: args.backends,
|
|
7004
7265
|
databaseUrl: env.DATABASE_URL,
|
|
7005
7266
|
sdkEndpointUrl: env.SDK_ENDPOINT_URL,
|
|
7006
7267
|
sharedSecret: env.AUTONOMA_SHARED_SECRET,
|
|
@@ -7015,6 +7276,7 @@ function loadConfig(args) {
|
|
|
7015
7276
|
|
|
7016
7277
|
// src/index.ts
|
|
7017
7278
|
init_analytics();
|
|
7279
|
+
init_colors();
|
|
7018
7280
|
init_context();
|
|
7019
7281
|
init_errors();
|
|
7020
7282
|
|
|
@@ -7023,10 +7285,10 @@ init_esm_shims();
|
|
|
7023
7285
|
init_debug();
|
|
7024
7286
|
import readline from "readline";
|
|
7025
7287
|
import { settings } from "@clack/core";
|
|
7026
|
-
var
|
|
7027
|
-
var
|
|
7288
|
+
var DIM2 = "\x1B[2m";
|
|
7289
|
+
var RESET2 = "\x1B[0m";
|
|
7028
7290
|
var SHOW_CURSOR = "\x1B[?25h";
|
|
7029
|
-
var EXIT_HINT = `${
|
|
7291
|
+
var EXIT_HINT = `${DIM2}(press Ctrl+C again to exit)${RESET2}`;
|
|
7030
7292
|
var ARM_WINDOW_MS = 3e3;
|
|
7031
7293
|
var FORCE_EXIT_MS = 2500;
|
|
7032
7294
|
var installed = false;
|
|
@@ -7074,6 +7336,50 @@ function installInterruptHandler(opts) {
|
|
|
7074
7336
|
return iface;
|
|
7075
7337
|
});
|
|
7076
7338
|
}
|
|
7339
|
+
function installTerminationDiagnostics() {
|
|
7340
|
+
const memSnapshot = () => {
|
|
7341
|
+
const m = process.memoryUsage();
|
|
7342
|
+
const mb = (n) => Math.round(n / 1024 / 1024);
|
|
7343
|
+
return `rss=${mb(m.rss)}MB heapUsed=${mb(m.heapUsed)}MB heapTotal=${mb(m.heapTotal)}MB`;
|
|
7344
|
+
};
|
|
7345
|
+
for (const signal of ["SIGTERM", "SIGHUP"]) {
|
|
7346
|
+
const forcedCode = signal === "SIGTERM" ? 143 : 129;
|
|
7347
|
+
process.on(signal, () => {
|
|
7348
|
+
process.stderr.write(`
|
|
7349
|
+
[diagnostics] received ${signal} - external termination (${memSnapshot()})
|
|
7350
|
+
`);
|
|
7351
|
+
terminateGracefully(forcedCode);
|
|
7352
|
+
});
|
|
7353
|
+
}
|
|
7354
|
+
process.on("uncaughtException", (err) => {
|
|
7355
|
+
const detail = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
7356
|
+
process.stderr.write(`
|
|
7357
|
+
[diagnostics] uncaughtException (${memSnapshot()}): ${detail}
|
|
7358
|
+
`);
|
|
7359
|
+
restoreTerminal();
|
|
7360
|
+
process.exit(1);
|
|
7361
|
+
});
|
|
7362
|
+
process.on("unhandledRejection", (reason) => {
|
|
7363
|
+
const detail = reason instanceof Error ? reason.stack ?? reason.message : String(reason);
|
|
7364
|
+
process.stderr.write(`
|
|
7365
|
+
[diagnostics] unhandledRejection (${memSnapshot()}): ${detail}
|
|
7366
|
+
`);
|
|
7367
|
+
restoreTerminal();
|
|
7368
|
+
process.exit(1);
|
|
7369
|
+
});
|
|
7370
|
+
}
|
|
7371
|
+
function terminateGracefully(forcedCode) {
|
|
7372
|
+
if (quitting || onExit == null) {
|
|
7373
|
+
restoreTerminal();
|
|
7374
|
+
process.exit(forcedCode);
|
|
7375
|
+
}
|
|
7376
|
+
quitting = true;
|
|
7377
|
+
setTimeout(() => {
|
|
7378
|
+
restoreTerminal();
|
|
7379
|
+
process.exit(forcedCode);
|
|
7380
|
+
}, FORCE_EXIT_MS).unref?.();
|
|
7381
|
+
onExit(forcedCode);
|
|
7382
|
+
}
|
|
7077
7383
|
function restoreTerminal() {
|
|
7078
7384
|
try {
|
|
7079
7385
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
@@ -7151,15 +7457,31 @@ async function loadGitInfo(outputDir) {
|
|
|
7151
7457
|
|
|
7152
7458
|
// src/index.ts
|
|
7153
7459
|
init_notify();
|
|
7460
|
+
init_project_map();
|
|
7154
7461
|
|
|
7155
7462
|
// src/core/state.ts
|
|
7156
7463
|
init_esm_shims();
|
|
7157
|
-
|
|
7158
|
-
import {
|
|
7464
|
+
init_debug();
|
|
7465
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
7466
|
+
import { join as join9 } from "path";
|
|
7467
|
+
import { z as z3 } from "zod";
|
|
7468
|
+
var StepStatusSchema = z3.enum(["pending", "running", "done", "failed", "paused"]);
|
|
7469
|
+
var PipelineStateSchema = z3.object({
|
|
7470
|
+
steps: z3.object({
|
|
7471
|
+
projectMapper: StepStatusSchema.default("done"),
|
|
7472
|
+
pagesFinder: StepStatusSchema.default("done"),
|
|
7473
|
+
kb: StepStatusSchema.default("done"),
|
|
7474
|
+
entityAudit: StepStatusSchema.default("done"),
|
|
7475
|
+
scenarioRecipe: StepStatusSchema.default("done"),
|
|
7476
|
+
recipeBuilder: StepStatusSchema.default("done"),
|
|
7477
|
+
testGenerator: StepStatusSchema.default("done")
|
|
7478
|
+
})
|
|
7479
|
+
});
|
|
7159
7480
|
var STATE_FILE = ".pipeline-state.json";
|
|
7160
7481
|
function initialState() {
|
|
7161
7482
|
return {
|
|
7162
7483
|
steps: {
|
|
7484
|
+
projectMapper: "pending",
|
|
7163
7485
|
pagesFinder: "pending",
|
|
7164
7486
|
kb: "pending",
|
|
7165
7487
|
entityAudit: "pending",
|
|
@@ -7170,18 +7492,19 @@ function initialState() {
|
|
|
7170
7492
|
};
|
|
7171
7493
|
}
|
|
7172
7494
|
async function loadState(outputDir) {
|
|
7173
|
-
const path3 =
|
|
7495
|
+
const path3 = join9(outputDir, STATE_FILE);
|
|
7174
7496
|
try {
|
|
7175
|
-
const raw = await
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7497
|
+
const raw = await readFile4(path3, "utf-8");
|
|
7498
|
+
return PipelineStateSchema.parse(JSON.parse(raw));
|
|
7499
|
+
} catch (err) {
|
|
7500
|
+
const isMissingFile = err instanceof Error && "code" in err && err.code === "ENOENT";
|
|
7501
|
+
if (!isMissingFile) debugLog("Failed to load pipeline state, starting fresh", { path: path3, err });
|
|
7179
7502
|
return initialState();
|
|
7180
7503
|
}
|
|
7181
7504
|
}
|
|
7182
7505
|
async function saveState(outputDir, state) {
|
|
7183
|
-
const path3 =
|
|
7184
|
-
await
|
|
7506
|
+
const path3 = join9(outputDir, STATE_FILE);
|
|
7507
|
+
await writeFile4(path3, JSON.stringify(state, null, 2), "utf-8");
|
|
7185
7508
|
}
|
|
7186
7509
|
async function markStep(outputDir, state, step, status) {
|
|
7187
7510
|
const updated = {
|
|
@@ -7192,15 +7515,23 @@ async function markStep(outputDir, state, step, status) {
|
|
|
7192
7515
|
return updated;
|
|
7193
7516
|
}
|
|
7194
7517
|
function nextPendingStep(state) {
|
|
7195
|
-
const order = [
|
|
7518
|
+
const order = [
|
|
7519
|
+
"projectMapper",
|
|
7520
|
+
"pagesFinder",
|
|
7521
|
+
"kb",
|
|
7522
|
+
"entityAudit",
|
|
7523
|
+
"scenarioRecipe",
|
|
7524
|
+
"recipeBuilder",
|
|
7525
|
+
"testGenerator"
|
|
7526
|
+
];
|
|
7196
7527
|
return order.find((s) => state.steps[s] !== "done") ?? void 0;
|
|
7197
7528
|
}
|
|
7198
7529
|
|
|
7199
7530
|
// src/core/upload.ts
|
|
7200
7531
|
init_esm_shims();
|
|
7201
7532
|
init_debug();
|
|
7202
|
-
import { readFile as
|
|
7203
|
-
import { basename, join as
|
|
7533
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
7534
|
+
import { basename, join as join10, relative } from "path";
|
|
7204
7535
|
import * as p from "@clack/prompts";
|
|
7205
7536
|
import { glob } from "glob";
|
|
7206
7537
|
var ARTIFACT_FILES = ["AUTONOMA.md", "scenarios.md", "entity-audit.md"];
|
|
@@ -7208,7 +7539,7 @@ async function readArtifacts(outputDir) {
|
|
|
7208
7539
|
const files = [];
|
|
7209
7540
|
for (const name of ARTIFACT_FILES) {
|
|
7210
7541
|
try {
|
|
7211
|
-
const content = await
|
|
7542
|
+
const content = await readFile5(join10(outputDir, name), "utf-8");
|
|
7212
7543
|
files.push({ name, content });
|
|
7213
7544
|
} catch (err) {
|
|
7214
7545
|
debugLog(`Artifact ${name} not on disk; skipping upload`, { err });
|
|
@@ -7217,13 +7548,13 @@ async function readArtifacts(outputDir) {
|
|
|
7217
7548
|
return files;
|
|
7218
7549
|
}
|
|
7219
7550
|
async function readTestCases(outputDir) {
|
|
7220
|
-
const testsDir =
|
|
7551
|
+
const testsDir = join10(outputDir, "qa-tests");
|
|
7221
7552
|
const matches = await glob("**/*.md", { cwd: testsDir, nodir: true });
|
|
7222
7553
|
const files = [];
|
|
7223
7554
|
for (const match of matches) {
|
|
7224
7555
|
const name = basename(match);
|
|
7225
7556
|
if (name === "INDEX.md") continue;
|
|
7226
|
-
const content = await
|
|
7557
|
+
const content = await readFile5(join10(testsDir, match), "utf-8");
|
|
7227
7558
|
const folderPath = relative(".", match).split("/").slice(0, -1).join("/");
|
|
7228
7559
|
files.push({ name, content, folder: folderPath.length > 0 ? folderPath : void 0 });
|
|
7229
7560
|
}
|
|
@@ -7285,11 +7616,11 @@ process.setSourceMapsEnabled(true);
|
|
|
7285
7616
|
var PAGES_FILE = "pages.json";
|
|
7286
7617
|
async function savePages(outputDir, pages) {
|
|
7287
7618
|
const obj = Object.fromEntries(pages);
|
|
7288
|
-
await
|
|
7619
|
+
await writeFile14(join31(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
|
|
7289
7620
|
}
|
|
7290
7621
|
async function loadPages(outputDir) {
|
|
7291
7622
|
try {
|
|
7292
|
-
const raw = await
|
|
7623
|
+
const raw = await readFile23(join31(outputDir, PAGES_FILE), "utf-8");
|
|
7293
7624
|
const obj = JSON.parse(raw);
|
|
7294
7625
|
return new Map(Object.entries(obj));
|
|
7295
7626
|
} catch {
|
|
@@ -7318,6 +7649,7 @@ function strArg(args, key) {
|
|
|
7318
7649
|
return typeof value === "string" ? value : void 0;
|
|
7319
7650
|
}
|
|
7320
7651
|
var STEP_LABELS = {
|
|
7652
|
+
projectMapper: "Map your project structure",
|
|
7321
7653
|
pagesFinder: "Find your pages",
|
|
7322
7654
|
kb: "Build a knowledge base",
|
|
7323
7655
|
entityAudit: "Map your data models",
|
|
@@ -7329,6 +7661,7 @@ function isStepName(value) {
|
|
|
7329
7661
|
return value in STEP_LABELS;
|
|
7330
7662
|
}
|
|
7331
7663
|
var STEP_SUMMARIES = {
|
|
7664
|
+
projectMapper: "Identify your frontend(s), backend(s), and which folders to ignore.",
|
|
7332
7665
|
pagesFinder: "Map every page and route in your app.",
|
|
7333
7666
|
kb: "Learn your app's features, flows, and UI patterns.",
|
|
7334
7667
|
entityAudit: "Find what your app stores (users, orgs, ...) and how each one is created.",
|
|
@@ -7337,6 +7670,7 @@ var STEP_SUMMARIES = {
|
|
|
7337
7670
|
testGenerator: "Write the end-to-end tests, covering every page and feature."
|
|
7338
7671
|
};
|
|
7339
7672
|
var STEP_INTROS = {
|
|
7673
|
+
projectMapper: "Looking at how your codebase is laid out - which folder(s) are the frontend, which are the backend/data layer, and which are unrelated - so every later step scans only what matters instead of the whole repo.",
|
|
7340
7674
|
pagesFinder: "Scanning your codebase to find every page and route, so we know the full surface area that needs test coverage.",
|
|
7341
7675
|
kb: "Reading those pages to learn your app's features, flows, and UI patterns - the context everything after this builds on.",
|
|
7342
7676
|
entityAudit: "Finding the things your app stores (users, organizations, orders, ...) and how each one gets created, so we can generate realistic test data for them.",
|
|
@@ -7344,6 +7678,32 @@ var STEP_INTROS = {
|
|
|
7344
7678
|
recipeBuilder: "Helping you wire up small helpers that create and clean up test data in your own database. We give you a copy-paste guide for each one and test it live against your app running locally - you deploy later, once everything passes.",
|
|
7345
7679
|
testGenerator: "Writing the actual end-to-end tests, covering every page and feature with depth proportional to its complexity."
|
|
7346
7680
|
};
|
|
7681
|
+
async function resolveScopeSelection(map, config, nonInteractive) {
|
|
7682
|
+
if (config.frontend != null) {
|
|
7683
|
+
const requestedFrontend = resolveSelection(map, { frontend: config.frontend, backends: [] }).frontend;
|
|
7684
|
+
const backends = config.backends ?? defaultBackendsFor(map, requestedFrontend);
|
|
7685
|
+
return resolveSelection(map, { frontend: config.frontend, backends });
|
|
7686
|
+
}
|
|
7687
|
+
if (!nonInteractive) return promptScopeSelection(map);
|
|
7688
|
+
return pickDefaultSelection(map);
|
|
7689
|
+
}
|
|
7690
|
+
async function promptScopeSelection(map) {
|
|
7691
|
+
const frontend = await p9.select({
|
|
7692
|
+
message: "Which frontend do you want to plan tests for?",
|
|
7693
|
+
options: map.frontends.map((f) => ({ value: f.path, label: `${f.path} [${f.framework}]`, hint: f.why }))
|
|
7694
|
+
});
|
|
7695
|
+
if (p9.isCancel(frontend)) throw new Error("Cancelled");
|
|
7696
|
+
if (map.backends.length === 0) return { frontend, backends: [] };
|
|
7697
|
+
const needed = defaultBackendsFor(map, frontend);
|
|
7698
|
+
const backends = await p9.multiselect({
|
|
7699
|
+
message: "Which backends does it need? (pre-checked: the ones it depends on)",
|
|
7700
|
+
options: map.backends.map((b) => ({ value: b.path, label: `${b.path} [${b.framework}]`, hint: b.why })),
|
|
7701
|
+
initialValues: needed,
|
|
7702
|
+
required: false
|
|
7703
|
+
});
|
|
7704
|
+
if (p9.isCancel(backends)) throw new Error("Cancelled");
|
|
7705
|
+
return { frontend, backends };
|
|
7706
|
+
}
|
|
7347
7707
|
async function runStep(step, outputDir, state, config, projectContext, nonInteractive, retryGuidance) {
|
|
7348
7708
|
const label = STEP_LABELS[step];
|
|
7349
7709
|
p9.note(STEP_INTROS[step], `Step: ${label}`);
|
|
@@ -7359,13 +7719,52 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
|
|
|
7359
7719
|
try {
|
|
7360
7720
|
let result;
|
|
7361
7721
|
switch (step) {
|
|
7722
|
+
case "projectMapper": {
|
|
7723
|
+
const { runProjectMapper: runProjectMapper2 } = await Promise.resolve().then(() => (init_project_mapper(), project_mapper_exports));
|
|
7724
|
+
const map = await runProjectMapper2({
|
|
7725
|
+
projectRoot: config.projectRoot,
|
|
7726
|
+
outputDir,
|
|
7727
|
+
modelId: config.modelId,
|
|
7728
|
+
nonInteractive
|
|
7729
|
+
});
|
|
7730
|
+
if (map == null) {
|
|
7731
|
+
result = { success: false, artifacts: [], summary: "Project mapper did not produce a map." };
|
|
7732
|
+
break;
|
|
7733
|
+
}
|
|
7734
|
+
if (map.frontends.length === 0) {
|
|
7735
|
+
result = {
|
|
7736
|
+
success: false,
|
|
7737
|
+
artifacts: [],
|
|
7738
|
+
summary: "Project mapper found no frontend to test. Point --project at a codebase with a UI."
|
|
7739
|
+
};
|
|
7740
|
+
break;
|
|
7741
|
+
}
|
|
7742
|
+
const selection = await resolveScopeSelection(map, config, nonInteractive);
|
|
7743
|
+
if (selection == null) {
|
|
7744
|
+
await saveProjectMap(outputDir, map);
|
|
7745
|
+
p9.note(renderProjectMap(map), "Project map - candidates (pick one frontend + its backends)");
|
|
7746
|
+
result = {
|
|
7747
|
+
success: false,
|
|
7748
|
+
paused: true,
|
|
7749
|
+
artifacts: [],
|
|
7750
|
+
summary: `Found ${map.frontends.length} candidate frontends. Choose one and its backends, then resume with --frontend <path> --backends <path,path>.`
|
|
7751
|
+
};
|
|
7752
|
+
break;
|
|
7753
|
+
}
|
|
7754
|
+
const scoped = applySelection(map, selection);
|
|
7755
|
+
await saveProjectMap(outputDir, scoped);
|
|
7756
|
+
p9.note(renderProjectMap(scoped), "Project map");
|
|
7757
|
+
break;
|
|
7758
|
+
}
|
|
7362
7759
|
case "pagesFinder": {
|
|
7363
7760
|
const { runPageFinder: runPageFinder2 } = await Promise.resolve().then(() => (init_pages_finder(), pages_finder_exports));
|
|
7761
|
+
const projectMap = await loadProjectMap(outputDir);
|
|
7364
7762
|
const pages = await runPageFinder2({
|
|
7365
7763
|
projectRoot: config.projectRoot,
|
|
7366
7764
|
outputDir,
|
|
7367
7765
|
modelId: config.modelId,
|
|
7368
|
-
nonInteractive
|
|
7766
|
+
nonInteractive,
|
|
7767
|
+
extraMessage: projectMap != null ? formatFrontendScope(projectMap) : void 0
|
|
7369
7768
|
});
|
|
7370
7769
|
await savePages(outputDir, pages);
|
|
7371
7770
|
break;
|
|
@@ -7384,18 +7783,21 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
|
|
|
7384
7783
|
}
|
|
7385
7784
|
case "entityAudit": {
|
|
7386
7785
|
const { runEntityAudit: runEntityAudit2 } = await Promise.resolve().then(() => (init_entity_audit(), entity_audit_exports));
|
|
7786
|
+
const auditMap = await loadProjectMap(outputDir);
|
|
7387
7787
|
result = await runEntityAudit2({
|
|
7388
7788
|
projectRoot: config.projectRoot,
|
|
7389
7789
|
outputDir,
|
|
7390
7790
|
modelId: config.modelId,
|
|
7391
7791
|
projectContext,
|
|
7392
7792
|
nonInteractive,
|
|
7393
|
-
retryGuidance
|
|
7793
|
+
retryGuidance,
|
|
7794
|
+
scopeHint: auditMap != null ? formatBackendScope(auditMap) : void 0
|
|
7394
7795
|
});
|
|
7395
7796
|
break;
|
|
7396
7797
|
}
|
|
7397
7798
|
case "scenarioRecipe": {
|
|
7398
7799
|
const { runScenarioRecipe: runScenarioRecipe2 } = await Promise.resolve().then(() => (init_scenario_recipe(), scenario_recipe_exports));
|
|
7800
|
+
const recipeMap = await loadProjectMap(outputDir);
|
|
7399
7801
|
result = await runScenarioRecipe2({
|
|
7400
7802
|
projectRoot: config.projectRoot,
|
|
7401
7803
|
outputDir,
|
|
@@ -7403,7 +7805,8 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
|
|
|
7403
7805
|
config,
|
|
7404
7806
|
projectContext,
|
|
7405
7807
|
nonInteractive,
|
|
7406
|
-
retryGuidance
|
|
7808
|
+
retryGuidance,
|
|
7809
|
+
scopeHint: recipeMap != null ? formatBackendScope(recipeMap) : void 0
|
|
7407
7810
|
});
|
|
7408
7811
|
break;
|
|
7409
7812
|
}
|
|
@@ -7517,14 +7920,14 @@ async function showStatus(outputDir) {
|
|
|
7517
7920
|
}
|
|
7518
7921
|
}
|
|
7519
7922
|
var BANNER = `
|
|
7520
|
-
|
|
7923
|
+
${PRIMARY}${BOLD} \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557
|
|
7521
7924
|
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
7522
7925
|
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
|
|
7523
7926
|
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551
|
|
7524
7927
|
\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
7525
7928
|
\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
|
|
7526
|
-
|
|
7527
|
-
|
|
7929
|
+
${RESET}
|
|
7930
|
+
${DIM} E2E Test Planner - Generate exhaustive test suites from your codebase${RESET}
|
|
7528
7931
|
`;
|
|
7529
7932
|
function ensureAutonomaAuth() {
|
|
7530
7933
|
if (readEnv().AUTONOMA_API_TOKEN?.trim()) return true;
|
|
@@ -7556,6 +7959,7 @@ async function gatherProjectContext() {
|
|
|
7556
7959
|
};
|
|
7557
7960
|
}
|
|
7558
7961
|
async function main() {
|
|
7962
|
+
installTerminationDiagnostics();
|
|
7559
7963
|
const args = parseArgs(process.argv.slice(2));
|
|
7560
7964
|
const command = process.argv[2];
|
|
7561
7965
|
if (command === "status") {
|
|
@@ -7573,7 +7977,7 @@ async function main() {
|
|
|
7573
7977
|
if (command === "help" || args.help) {
|
|
7574
7978
|
console.log("Usage:");
|
|
7575
7979
|
console.log(
|
|
7576
|
-
" test-planner [run] [--project <path>] [--model <id>] [--step <name>] [--resume] [--non-interactive]"
|
|
7980
|
+
" test-planner [run] [--project <path>] [--frontend <path>] [--backends <path,path>] [--model <id>] [--step <name>] [--resume] [--non-interactive]"
|
|
7577
7981
|
);
|
|
7578
7982
|
console.log(" test-planner status [--project <path>]");
|
|
7579
7983
|
console.log("");
|
|
@@ -7584,19 +7988,25 @@ async function main() {
|
|
|
7584
7988
|
p9.intro("Let's generate your test suite");
|
|
7585
7989
|
const resumeCommand = `autonoma-planner --resume` + (args.project ? ` --project ${args.project}` : "");
|
|
7586
7990
|
installInterruptHandler({
|
|
7587
|
-
|
|
7991
|
+
// exitCode defaults to 0 for a user-initiated Ctrl+C (progress saved, a clean stop);
|
|
7992
|
+
// an external SIGTERM/SIGHUP passes the conventional 143/129 so the flushed exit still
|
|
7993
|
+
// carries the signal code a reaper/CI reads, not a 0 that looks like normal completion.
|
|
7994
|
+
onExit: (exitCode = 0) => {
|
|
7588
7995
|
track("cli_run_exited");
|
|
7589
7996
|
restoreTerminal();
|
|
7590
7997
|
console.log("");
|
|
7591
7998
|
p9.log.warn(`Your progress is saved. To resume, run:
|
|
7592
7999
|
${resumeCommand}`);
|
|
7593
|
-
void flushAnalytics().finally(() => process.exit(
|
|
8000
|
+
void flushAnalytics().finally(() => process.exit(exitCode));
|
|
7594
8001
|
}
|
|
7595
8002
|
});
|
|
8003
|
+
const backendsArg = strArg(args, "backends");
|
|
7596
8004
|
const config = loadConfig({
|
|
7597
8005
|
project: strArg(args, "project"),
|
|
7598
8006
|
model: strArg(args, "model"),
|
|
7599
|
-
slug: strArg(args, "slug")
|
|
8007
|
+
slug: strArg(args, "slug"),
|
|
8008
|
+
frontend: strArg(args, "frontend"),
|
|
8009
|
+
backends: backendsArg != null ? backendsArg.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : void 0
|
|
7600
8010
|
});
|
|
7601
8011
|
if (!ensureAutonomaAuth()) {
|
|
7602
8012
|
return;
|
|
@@ -7687,12 +8097,20 @@ or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
|
|
|
7687
8097
|
p9.outro("Done");
|
|
7688
8098
|
return;
|
|
7689
8099
|
}
|
|
7690
|
-
const startStep = isResuming ? nextPendingStep(state) : "
|
|
8100
|
+
const startStep = isResuming ? nextPendingStep(state) : "projectMapper";
|
|
7691
8101
|
if (!startStep) {
|
|
7692
8102
|
p9.log.success("All steps complete.");
|
|
7693
8103
|
return;
|
|
7694
8104
|
}
|
|
7695
|
-
const steps = [
|
|
8105
|
+
const steps = [
|
|
8106
|
+
"projectMapper",
|
|
8107
|
+
"pagesFinder",
|
|
8108
|
+
"kb",
|
|
8109
|
+
"entityAudit",
|
|
8110
|
+
"scenarioRecipe",
|
|
8111
|
+
"recipeBuilder",
|
|
8112
|
+
"testGenerator"
|
|
8113
|
+
];
|
|
7696
8114
|
const startIdx = steps.indexOf(startStep);
|
|
7697
8115
|
p9.note(steps.map((s, idx) => `${idx + 1}. ${STEP_LABELS[s]} - ${STEP_SUMMARIES[s]}`).join("\n"), "Here's the plan");
|
|
7698
8116
|
try {
|