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