@autonoma-ai/planner 0.1.2 → 0.1.3-canary.0ce8174
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1017 -527
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -20,13 +20,13 @@ var init_esm_shims = __esm({
|
|
|
20
20
|
|
|
21
21
|
// src/core/context.ts
|
|
22
22
|
import { readFile, writeFile } from "fs/promises";
|
|
23
|
-
import { join as
|
|
23
|
+
import { join as join3 } from "path";
|
|
24
24
|
async function saveContext(outputDir, ctx) {
|
|
25
|
-
await writeFile(
|
|
25
|
+
await writeFile(join3(outputDir, CONTEXT_FILE), JSON.stringify(ctx, null, 2), "utf-8");
|
|
26
26
|
}
|
|
27
27
|
async function loadContext(outputDir) {
|
|
28
28
|
try {
|
|
29
|
-
const raw = await readFile(
|
|
29
|
+
const raw = await readFile(join3(outputDir, CONTEXT_FILE), "utf-8");
|
|
30
30
|
return JSON.parse(raw);
|
|
31
31
|
} catch {
|
|
32
32
|
return null;
|
|
@@ -39,9 +39,9 @@ function formatContext(ctx) {
|
|
|
39
39
|
|
|
40
40
|
**Why they want testing:** ${ctx.testingGoal}
|
|
41
41
|
|
|
42
|
-
**Critical flows
|
|
42
|
+
**Critical flows (user-declared \u2014 these MUST be covered):** ${ctx.criticalFlows}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
These are flows the user explicitly said cannot break. Treat them as authoritative: every one of them must be represented faithfully in your output \u2014 never drop or downplay them. Start with these, then expand to cover the rest of the application.`;
|
|
45
45
|
if (ctx.pages?.length) {
|
|
46
46
|
output += `
|
|
47
47
|
|
|
@@ -150,7 +150,7 @@ function createStepLogger(agentId, maxSteps) {
|
|
|
150
150
|
function writeSpinner(message) {
|
|
151
151
|
const frame = SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length];
|
|
152
152
|
frameIdx++;
|
|
153
|
-
process.stderr.write(`${CLEAR_LINE} ${
|
|
153
|
+
process.stderr.write(`${CLEAR_LINE} ${DIM2}${frame} ${message}${RESET2}`);
|
|
154
154
|
lastSpinnerLine = true;
|
|
155
155
|
}
|
|
156
156
|
function writePermanent(message) {
|
|
@@ -182,52 +182,52 @@ function createStepLogger(agentId, maxSteps) {
|
|
|
182
182
|
case "write_file": {
|
|
183
183
|
stats.filesWritten++;
|
|
184
184
|
const path3 = String(tc.input.path ?? tc.input.file_path ?? "");
|
|
185
|
-
writePermanent(` ${GREEN}\u270E write ${path3}${
|
|
185
|
+
writePermanent(` ${GREEN}\u270E write ${path3}${RESET2}`);
|
|
186
186
|
break;
|
|
187
187
|
}
|
|
188
188
|
case "write_test":
|
|
189
189
|
stats.filesWritten++;
|
|
190
|
-
writePermanent(` ${GREEN}\u270E test ${summary2}${
|
|
190
|
+
writePermanent(` ${GREEN}\u270E test ${summary2}${RESET2}`);
|
|
191
191
|
break;
|
|
192
192
|
case "finish":
|
|
193
|
-
writePermanent(` ${GREEN}${BOLD}\u2713
|
|
193
|
+
writePermanent(` ${GREEN}${BOLD}\u2713 done:${RESET2} ${GREEN}${agentId}${RESET2}`);
|
|
194
194
|
break;
|
|
195
195
|
case "subagent":
|
|
196
196
|
case "spawn_researcher":
|
|
197
|
-
writePermanent(` ${CYAN}\u2295 subagent: ${summary2}${
|
|
197
|
+
writePermanent(` ${CYAN}\u2295 subagent: ${summary2}${RESET2}`);
|
|
198
198
|
break;
|
|
199
199
|
default:
|
|
200
200
|
writeSpinner(`${stepPrefix} \u2014 ${tc.name}${summary2 ? " " + summary2 : ""}`);
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
for (const te of info.toolErrors) {
|
|
204
|
-
writePermanent(` ${RED}\u2717 ${te.name}: ${te.error}${
|
|
204
|
+
writePermanent(` ${RED}\u2717 ${te.name}: ${te.error}${RESET2}`);
|
|
205
205
|
}
|
|
206
206
|
for (const f of info.writtenFiles) {
|
|
207
|
-
writePermanent(` ${GREEN}\u{1F4C4} wrote: ${f}${
|
|
207
|
+
writePermanent(` ${GREEN}\u{1F4C4} wrote: ${f}${RESET2}`);
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
function checkpoint(message) {
|
|
211
|
-
writePermanent(` ${YELLOW}\u25B8 ${message}${
|
|
211
|
+
writePermanent(` ${YELLOW}\u25B8 ${message}${RESET2}`);
|
|
212
212
|
}
|
|
213
213
|
function summary() {
|
|
214
214
|
clearSpinner();
|
|
215
215
|
if (stats.filesRead > 0 || stats.filesWritten > 0) {
|
|
216
|
-
console.log(` ${
|
|
216
|
+
console.log(` ${DIM2}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500${RESET2}`);
|
|
217
217
|
console.log(
|
|
218
|
-
` ${
|
|
218
|
+
` ${DIM2}files read: ${stats.filesRead} | files written: ${stats.filesWritten}${RESET2}`
|
|
219
219
|
);
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
222
|
return { log: log8, checkpoint, summary, stats };
|
|
223
223
|
}
|
|
224
|
-
var
|
|
224
|
+
var DIM2, RESET2, CYAN, GREEN, RED, YELLOW, BOLD, SPINNER_FRAMES, CLEAR_LINE;
|
|
225
225
|
var init_display = __esm({
|
|
226
226
|
"src/core/display.ts"() {
|
|
227
227
|
"use strict";
|
|
228
228
|
init_esm_shims();
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
DIM2 = "\x1B[2m";
|
|
230
|
+
RESET2 = "\x1B[0m";
|
|
231
231
|
CYAN = "\x1B[36m";
|
|
232
232
|
GREEN = "\x1B[32m";
|
|
233
233
|
RED = "\x1B[31m";
|
|
@@ -286,8 +286,8 @@ function buildStepHandler(config) {
|
|
|
286
286
|
async function runAgent(config, prompt, extractResult) {
|
|
287
287
|
const stepTimeout = config.stepTimeoutMs ?? STEP_TIMEOUT_MS;
|
|
288
288
|
const modelsToTry = [config.model, ...FALLBACK_MODELS.map((id) => getModel(id))];
|
|
289
|
-
const
|
|
290
|
-
const
|
|
289
|
+
const YELLOW4 = "\x1B[33m";
|
|
290
|
+
const RESET7 = "\x1B[0m";
|
|
291
291
|
for (let modelIdx = 0; modelIdx < modelsToTry.length; modelIdx++) {
|
|
292
292
|
const model = modelsToTry[modelIdx];
|
|
293
293
|
for (let retry = 0; retry < RETRIES_BEFORE_FALLBACK; retry++) {
|
|
@@ -312,17 +312,17 @@ async function runAgent(config, prompt, extractResult) {
|
|
|
312
312
|
const msg = err instanceof Error ? err.message : String(err);
|
|
313
313
|
const isTimeout = msg.includes("timed out") || msg.includes("timeout") || msg.includes("abort");
|
|
314
314
|
if (!isTimeout) throw err;
|
|
315
|
-
console.log(` ${
|
|
315
|
+
console.log(` ${YELLOW4}[${config.id}] step timed out after ${stepTimeout / 1e3}s${RESET7}`);
|
|
316
316
|
if (retry < RETRIES_BEFORE_FALLBACK - 1) {
|
|
317
317
|
console.log(
|
|
318
|
-
` ${
|
|
318
|
+
` ${YELLOW4}[${config.id}] retrying (${retry + 1}/${RETRIES_BEFORE_FALLBACK})...${RESET7}`
|
|
319
319
|
);
|
|
320
320
|
continue;
|
|
321
321
|
}
|
|
322
322
|
if (modelIdx < modelsToTry.length - 1) {
|
|
323
323
|
const nextModel = FALLBACK_MODELS[modelIdx];
|
|
324
324
|
console.log(
|
|
325
|
-
` ${
|
|
325
|
+
` ${YELLOW4}[${config.id}] ${RETRIES_BEFORE_FALLBACK} timeouts, switching to ${nextModel}${RESET7}`
|
|
326
326
|
);
|
|
327
327
|
break;
|
|
328
328
|
}
|
|
@@ -351,7 +351,7 @@ var init_agent = __esm({
|
|
|
351
351
|
|
|
352
352
|
// src/core/gitignore.ts
|
|
353
353
|
import { readFile as readFile3 } from "fs/promises";
|
|
354
|
-
import { join as
|
|
354
|
+
import { join as join7, relative } from "path";
|
|
355
355
|
import { glob } from "glob";
|
|
356
356
|
async function loadGitignorePatterns(projectRoot) {
|
|
357
357
|
const patterns = [
|
|
@@ -371,10 +371,10 @@ async function loadGitignorePatterns(projectRoot) {
|
|
|
371
371
|
];
|
|
372
372
|
const matches = await glob("**/.gitignore", { cwd: projectRoot, dot: true });
|
|
373
373
|
for (const match of matches) {
|
|
374
|
-
const fullPath =
|
|
374
|
+
const fullPath = join7(projectRoot, match);
|
|
375
375
|
try {
|
|
376
376
|
const content = await readFile3(fullPath, "utf-8");
|
|
377
|
-
const prefix = relative(projectRoot,
|
|
377
|
+
const prefix = relative(projectRoot, join7(projectRoot, match, ".."));
|
|
378
378
|
const parsed = parseGitignore(content, prefix);
|
|
379
379
|
patterns.push(...parsed);
|
|
380
380
|
} catch (err) {
|
|
@@ -598,7 +598,7 @@ var init_grep = __esm({
|
|
|
598
598
|
// src/tools/list-directory.ts
|
|
599
599
|
import { readdir } from "fs/promises";
|
|
600
600
|
import { stat } from "fs/promises";
|
|
601
|
-
import { join as
|
|
601
|
+
import { join as join8, relative as relative2 } from "path";
|
|
602
602
|
import { tool as tool4 } from "ai";
|
|
603
603
|
import { z as z4 } from "zod";
|
|
604
604
|
import { minimatch } from "minimatch";
|
|
@@ -623,7 +623,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
|
|
|
623
623
|
const withTypes = [];
|
|
624
624
|
for (const name of rawEntries) {
|
|
625
625
|
try {
|
|
626
|
-
const s = await stat(
|
|
626
|
+
const s = await stat(join8(dirPath, name));
|
|
627
627
|
withTypes.push({ name, isDir: s.isDirectory() });
|
|
628
628
|
} catch {
|
|
629
629
|
withTypes.push({ name, isDir: false });
|
|
@@ -643,7 +643,7 @@ async function buildTree(dirPath, maxDepth, currentDepth, isIgnored, relativeBas
|
|
|
643
643
|
}
|
|
644
644
|
if (entry.isDir) {
|
|
645
645
|
const children = await buildTree(
|
|
646
|
-
|
|
646
|
+
join8(dirPath, entry.name),
|
|
647
647
|
maxDepth,
|
|
648
648
|
currentDepth + 1,
|
|
649
649
|
isIgnored,
|
|
@@ -694,7 +694,7 @@ async function buildListDirectoryTool(workingDirectory) {
|
|
|
694
694
|
};
|
|
695
695
|
}
|
|
696
696
|
seen.add(cacheKey);
|
|
697
|
-
const targetDir = input.path === "." ? workingDirectory :
|
|
697
|
+
const targetDir = input.path === "." ? workingDirectory : join8(workingDirectory, input.path);
|
|
698
698
|
try {
|
|
699
699
|
const s = await stat(targetDir);
|
|
700
700
|
if (!s.isDirectory()) {
|
|
@@ -800,7 +800,7 @@ import {
|
|
|
800
800
|
import { z as z6 } from "zod";
|
|
801
801
|
function buildSubagentTools(workingDirectory, onFileRead) {
|
|
802
802
|
const baseReadFile = buildReadFileTool(workingDirectory);
|
|
803
|
-
const
|
|
803
|
+
const readFile19 = onFileRead ? tool6({
|
|
804
804
|
description: baseReadFile.description,
|
|
805
805
|
inputSchema: baseReadFile.inputSchema,
|
|
806
806
|
execute: async (input, options) => {
|
|
@@ -813,7 +813,7 @@ function buildSubagentTools(workingDirectory, onFileRead) {
|
|
|
813
813
|
bash: buildBashTool(workingDirectory),
|
|
814
814
|
glob: buildGlobTool(workingDirectory),
|
|
815
815
|
grep: buildGrepTool(workingDirectory),
|
|
816
|
-
read_file:
|
|
816
|
+
read_file: readFile19
|
|
817
817
|
};
|
|
818
818
|
}
|
|
819
819
|
function buildSubagentTool(model, workingDirectory, onHeartbeat, onFileRead) {
|
|
@@ -1091,12 +1091,12 @@ var init_notify = __esm({
|
|
|
1091
1091
|
// src/core/review.ts
|
|
1092
1092
|
import * as p2 from "@clack/prompts";
|
|
1093
1093
|
import { access } from "fs/promises";
|
|
1094
|
-
import { join as
|
|
1094
|
+
import { join as join9, isAbsolute } from "path";
|
|
1095
1095
|
import { spawn } from "child_process";
|
|
1096
1096
|
import which from "which";
|
|
1097
1097
|
function resolvePath(artifact, outputDir) {
|
|
1098
1098
|
if (isAbsolute(artifact)) return artifact;
|
|
1099
|
-
return
|
|
1099
|
+
return join9(outputDir, artifact);
|
|
1100
1100
|
}
|
|
1101
1101
|
async function detectEditors() {
|
|
1102
1102
|
if (cachedEditors) return cachedEditors;
|
|
@@ -1122,7 +1122,7 @@ async function openInEditor(files) {
|
|
|
1122
1122
|
const editors = await detectEditors();
|
|
1123
1123
|
if (editors.length === 0) {
|
|
1124
1124
|
p2.log.warn("No editors found. Review the files manually:");
|
|
1125
|
-
for (const f of files) console.log(` ${CYAN2}${f}${
|
|
1125
|
+
for (const f of files) console.log(` ${CYAN2}${f}${RESET3}`);
|
|
1126
1126
|
return;
|
|
1127
1127
|
}
|
|
1128
1128
|
if (preferredEditor) {
|
|
@@ -1164,11 +1164,11 @@ async function openInEditor(files) {
|
|
|
1164
1164
|
}
|
|
1165
1165
|
async function showResults(result, options) {
|
|
1166
1166
|
console.log("");
|
|
1167
|
-
console.log(` ${GREEN2}[${options.agentId}] Step complete.${
|
|
1167
|
+
console.log(` ${GREEN2}[${options.agentId}] Step complete.${RESET3}`);
|
|
1168
1168
|
if (result.artifacts.length === 0) {
|
|
1169
1169
|
const knownFiles = ["AUTONOMA.md", "entity-audit.md", "scenarios.md"];
|
|
1170
1170
|
for (const f of knownFiles) {
|
|
1171
|
-
const fullPath =
|
|
1171
|
+
const fullPath = join9(options.outputDir, f);
|
|
1172
1172
|
try {
|
|
1173
1173
|
await access(fullPath);
|
|
1174
1174
|
result.artifacts.push(f);
|
|
@@ -1178,17 +1178,24 @@ async function showResults(result, options) {
|
|
|
1178
1178
|
}
|
|
1179
1179
|
const resolvedPaths = [];
|
|
1180
1180
|
if (result.artifacts.length > 0) {
|
|
1181
|
-
console.log(` ${
|
|
1181
|
+
console.log(` ${DIM3}Output files:${RESET3}`);
|
|
1182
1182
|
for (const a of result.artifacts) {
|
|
1183
1183
|
const fullPath = resolvePath(a, options.outputDir);
|
|
1184
1184
|
resolvedPaths.push(fullPath);
|
|
1185
|
-
console.log(` ${CYAN2}${fullPath}${
|
|
1185
|
+
console.log(` ${CYAN2}${fullPath}${RESET3}`);
|
|
1186
1186
|
}
|
|
1187
1187
|
}
|
|
1188
1188
|
if (result.summary) {
|
|
1189
1189
|
console.log(` ${result.summary}`);
|
|
1190
1190
|
}
|
|
1191
1191
|
console.log("");
|
|
1192
|
+
if (options.renderSummary) {
|
|
1193
|
+
const rendered = await options.renderSummary();
|
|
1194
|
+
if (rendered) {
|
|
1195
|
+
console.log(rendered);
|
|
1196
|
+
console.log("");
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1192
1199
|
if (options.reviewGuidance) {
|
|
1193
1200
|
p2.note(options.reviewGuidance, "What to check");
|
|
1194
1201
|
}
|
|
@@ -1226,16 +1233,16 @@ async function reviewLoop(result, options) {
|
|
|
1226
1233
|
await showResults(result, options);
|
|
1227
1234
|
}
|
|
1228
1235
|
}
|
|
1229
|
-
var
|
|
1236
|
+
var DIM3, CYAN2, GREEN2, RESET3, EDITORS, cachedEditors, preferredEditor;
|
|
1230
1237
|
var init_review = __esm({
|
|
1231
1238
|
"src/core/review.ts"() {
|
|
1232
1239
|
"use strict";
|
|
1233
1240
|
init_esm_shims();
|
|
1234
1241
|
init_notify();
|
|
1235
|
-
|
|
1242
|
+
DIM3 = "\x1B[2m";
|
|
1236
1243
|
CYAN2 = "\x1B[36m";
|
|
1237
1244
|
GREEN2 = "\x1B[32m";
|
|
1238
|
-
|
|
1245
|
+
RESET3 = "\x1B[0m";
|
|
1239
1246
|
EDITORS = [
|
|
1240
1247
|
{ command: "cursor", label: "Cursor", args: (f) => f },
|
|
1241
1248
|
{ command: "code", label: "VS Code", args: (f) => f },
|
|
@@ -1339,6 +1346,7 @@ pages:
|
|
|
1339
1346
|
BAD mission: "Shows analytics charts" (just restates the feature name)
|
|
1340
1347
|
- coreReason (required when core: true): WHY breakage of this feature makes the product unusable.
|
|
1341
1348
|
- At least one flow must have core: true
|
|
1349
|
+
- Any flow the user explicitly named as critical in the Project Context MUST appear as a feature in core_flows AND be marked core: true with a coreReason. Map the user's wording to the matching feature(s) \u2014 never drop a user-declared critical flow or leave it as core: false.
|
|
1342
1350
|
- feature_count: total features identified (positive integer)
|
|
1343
1351
|
- pages: a list of all pages discovered, with their path and brief description
|
|
1344
1352
|
|
|
@@ -1391,6 +1399,77 @@ After the frontmatter, include:
|
|
|
1391
1399
|
}
|
|
1392
1400
|
});
|
|
1393
1401
|
|
|
1402
|
+
// src/agents/01-kb-generator/flows.ts
|
|
1403
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1404
|
+
import { join as join10 } from "path";
|
|
1405
|
+
import matter from "gray-matter";
|
|
1406
|
+
async function parseCoreFlows(outputDir) {
|
|
1407
|
+
let raw;
|
|
1408
|
+
try {
|
|
1409
|
+
raw = await readFile5(join10(outputDir, "AUTONOMA.md"), "utf-8");
|
|
1410
|
+
} catch {
|
|
1411
|
+
return [];
|
|
1412
|
+
}
|
|
1413
|
+
try {
|
|
1414
|
+
const parsed = matter(raw);
|
|
1415
|
+
const flows = parsed.data.core_flows;
|
|
1416
|
+
if (!Array.isArray(flows)) return [];
|
|
1417
|
+
return flows.filter((f) => !!f && typeof f === "object").map((f) => ({
|
|
1418
|
+
feature: String(f.feature ?? "").trim(),
|
|
1419
|
+
description: f.description != null ? String(f.description) : void 0,
|
|
1420
|
+
mission: f.mission != null ? String(f.mission) : void 0,
|
|
1421
|
+
core: f.core === true,
|
|
1422
|
+
coreReason: f.coreReason != null ? String(f.coreReason) : void 0
|
|
1423
|
+
})).filter((f) => f.feature.length > 0);
|
|
1424
|
+
} catch {
|
|
1425
|
+
return [];
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
function truncate(s, max) {
|
|
1429
|
+
if (s.length <= max) return s;
|
|
1430
|
+
return s.slice(0, max - 1).trimEnd() + "\u2026";
|
|
1431
|
+
}
|
|
1432
|
+
function pad(s, width) {
|
|
1433
|
+
return s + " ".repeat(Math.max(0, width - s.length));
|
|
1434
|
+
}
|
|
1435
|
+
function renderFlowsTable(flows) {
|
|
1436
|
+
if (flows.length === 0) return "";
|
|
1437
|
+
const DESC_MAX = 60;
|
|
1438
|
+
const NAME_MAX = 32;
|
|
1439
|
+
const rows = flows.map((f, i) => ({
|
|
1440
|
+
num: String(i + 1),
|
|
1441
|
+
name: truncate(f.feature, NAME_MAX),
|
|
1442
|
+
crit: f.core ? "core" : "normal",
|
|
1443
|
+
desc: truncate((f.description ?? "").replace(/\s+/g, " ").trim(), DESC_MAX)
|
|
1444
|
+
}));
|
|
1445
|
+
const numW = Math.max(1, ...rows.map((r) => r.num.length));
|
|
1446
|
+
const nameW = Math.max("Flow".length, ...rows.map((r) => r.name.length));
|
|
1447
|
+
const critW = Math.max("Criticality".length, ...rows.map((r) => r.crit.length));
|
|
1448
|
+
const coreCount = flows.filter((f) => f.core).length;
|
|
1449
|
+
const header = `${BOLD2}${pad("#", numW)} ${pad("Flow", nameW)} ${pad("Criticality", critW)} Description${RESET4}`;
|
|
1450
|
+
const sep = `${DIM4}${"\u2500".repeat(numW + nameW + critW + DESC_MAX + 6)}${RESET4}`;
|
|
1451
|
+
const body = rows.map((r) => {
|
|
1452
|
+
const line = `${pad(r.num, numW)} ${pad(r.name, nameW)} ${pad(r.crit, critW)} ${r.desc}`;
|
|
1453
|
+
return r.crit === "core" ? `${YELLOW2}${line}${RESET4}` : line;
|
|
1454
|
+
}).join("\n");
|
|
1455
|
+
const caption = `${DIM4}${flows.length} flows \xB7 ${coreCount} marked core${RESET4}`;
|
|
1456
|
+
return `${header}
|
|
1457
|
+
${sep}
|
|
1458
|
+
${body}
|
|
1459
|
+
${caption}`;
|
|
1460
|
+
}
|
|
1461
|
+
var RESET4, DIM4, YELLOW2, BOLD2;
|
|
1462
|
+
var init_flows = __esm({
|
|
1463
|
+
"src/agents/01-kb-generator/flows.ts"() {
|
|
1464
|
+
"use strict";
|
|
1465
|
+
init_esm_shims();
|
|
1466
|
+
RESET4 = "\x1B[0m";
|
|
1467
|
+
DIM4 = "\x1B[2m";
|
|
1468
|
+
YELLOW2 = "\x1B[33m";
|
|
1469
|
+
BOLD2 = "\x1B[1m";
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1394
1473
|
// src/agents/01-kb-generator/index.ts
|
|
1395
1474
|
var kb_generator_exports = {};
|
|
1396
1475
|
__export(kb_generator_exports, {
|
|
@@ -1398,8 +1477,8 @@ __export(kb_generator_exports, {
|
|
|
1398
1477
|
});
|
|
1399
1478
|
import { tool as tool10 } from "ai";
|
|
1400
1479
|
import { z as z10 } from "zod";
|
|
1401
|
-
import { readFile as
|
|
1402
|
-
import { join as
|
|
1480
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1481
|
+
import { join as join11 } from "path";
|
|
1403
1482
|
function buildRegisterPagesTool(tracker) {
|
|
1404
1483
|
return tool10({
|
|
1405
1484
|
description: "Register ALL page/route files discovered via glob. Call this ONCE after globbing for page files. The system will track which ones you've read and block finish until all are covered.",
|
|
@@ -1513,11 +1592,43 @@ Output files:
|
|
|
1513
1592
|
};
|
|
1514
1593
|
await runAgent(agentConfig, prompt, () => result);
|
|
1515
1594
|
logger.summary();
|
|
1595
|
+
const autonomaPath = join11(input.outputDir, "AUTONOMA.md");
|
|
1596
|
+
const autonomaExists = await readFile6(autonomaPath, "utf-8").then(() => true).catch(() => false);
|
|
1597
|
+
if (!result?.success && autonomaExists) {
|
|
1598
|
+
result = {
|
|
1599
|
+
success: true,
|
|
1600
|
+
artifacts: ["AUTONOMA.md"],
|
|
1601
|
+
summary: "Knowledge base generated."
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
const declaredCriticalFlows = input.projectContext?.criticalFlows?.trim();
|
|
1605
|
+
if (result?.success && declaredCriticalFlows) {
|
|
1606
|
+
const beforeSelfReview = result;
|
|
1607
|
+
result = void 0;
|
|
1608
|
+
const selfReviewPrompt = `Before this knowledge base is shown to the user, verify it honors the critical flows they explicitly declared.
|
|
1609
|
+
|
|
1610
|
+
The user said these flows are critical and cannot break:
|
|
1611
|
+
"${declaredCriticalFlows}"
|
|
1612
|
+
|
|
1613
|
+
Read your AUTONOMA.md output. For EACH critical flow the user named:
|
|
1614
|
+
- Confirm it appears as a feature in core_flows (map the user's wording to the matching feature).
|
|
1615
|
+
- Confirm that feature is marked core: true with a coreReason.
|
|
1616
|
+
|
|
1617
|
+
If any declared critical flow is missing, mismatched, or left core: false, FIX AUTONOMA.md now \u2014 add the feature if it is genuinely absent, or flip core to true with a coreReason. Do not downgrade or drop anything the user declared critical.
|
|
1618
|
+
|
|
1619
|
+
When AUTONOMA.md correctly reflects every declared critical flow, call finish.`;
|
|
1620
|
+
await runAgent(agentConfig, selfReviewPrompt, () => result);
|
|
1621
|
+
if (!result) result = beforeSelfReview;
|
|
1622
|
+
}
|
|
1516
1623
|
const reviewed = await reviewLoop(result, {
|
|
1517
1624
|
agentId: "kb-generator",
|
|
1518
1625
|
outputDir: input.outputDir,
|
|
1519
1626
|
nonInteractive: input.nonInteractive,
|
|
1520
|
-
|
|
1627
|
+
renderSummary: async () => {
|
|
1628
|
+
const flows = await parseCoreFlows(input.outputDir);
|
|
1629
|
+
return flows.length ? renderFlowsTable(flows) : void 0;
|
|
1630
|
+
},
|
|
1631
|
+
reviewGuidance: "Check that every page/route in your app appears in core_flows.\nVerify that every flow the user named as critical in the Project Context appears in core_flows and is marked core: true with a coreReason.\nVerify the mission for each feature describes the ONE thing it must do correctly.\nLook for missing features or incorrectly grouped pages.\nA complex app should have 20-40 features \u2014 if you see fewer than 15, features are probably grouped too aggressively.",
|
|
1521
1632
|
onFeedback: async (feedback) => {
|
|
1522
1633
|
result = void 0;
|
|
1523
1634
|
const feedbackPrompt = `The user reviewed your knowledge base output and has this feedback:
|
|
@@ -1531,18 +1642,6 @@ Call page_coverage to see current state. When done with changes, call finish aga
|
|
|
1531
1642
|
return result;
|
|
1532
1643
|
}
|
|
1533
1644
|
});
|
|
1534
|
-
if (!reviewed) {
|
|
1535
|
-
const autonomaPath = join8(input.outputDir, "AUTONOMA.md");
|
|
1536
|
-
try {
|
|
1537
|
-
await readFile5(autonomaPath, "utf-8");
|
|
1538
|
-
return {
|
|
1539
|
-
success: true,
|
|
1540
|
-
artifacts: ["AUTONOMA.md"],
|
|
1541
|
-
summary: "Knowledge base generated (finish tool may not have captured the result, but AUTONOMA.md exists)."
|
|
1542
|
-
};
|
|
1543
|
-
} catch {
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
1645
|
return reviewed ?? {
|
|
1547
1646
|
success: false,
|
|
1548
1647
|
artifacts: [],
|
|
@@ -1560,6 +1659,7 @@ var init_kb_generator = __esm({
|
|
|
1560
1659
|
init_review();
|
|
1561
1660
|
init_tools();
|
|
1562
1661
|
init_prompt();
|
|
1662
|
+
init_flows();
|
|
1563
1663
|
PageTracker = class {
|
|
1564
1664
|
registered = /* @__PURE__ */ new Set();
|
|
1565
1665
|
read = /* @__PURE__ */ new Set();
|
|
@@ -1585,6 +1685,233 @@ var init_kb_generator = __esm({
|
|
|
1585
1685
|
}
|
|
1586
1686
|
});
|
|
1587
1687
|
|
|
1688
|
+
// src/agents/04-recipe-builder/entity-order.ts
|
|
1689
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
1690
|
+
import { join as join12 } from "path";
|
|
1691
|
+
async function parseEntityAudit(outputDir) {
|
|
1692
|
+
const raw = await readFile7(join12(outputDir, "entity-audit.md"), "utf-8");
|
|
1693
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
1694
|
+
if (!fmMatch) throw new Error("entity-audit.md has no YAML frontmatter");
|
|
1695
|
+
const yaml = fmMatch[1];
|
|
1696
|
+
const models = [];
|
|
1697
|
+
let current = null;
|
|
1698
|
+
let inCreatedBy = false;
|
|
1699
|
+
let currentCreatedBy = null;
|
|
1700
|
+
for (const line of yaml.split("\n")) {
|
|
1701
|
+
if (line.match(/^\s{2}- name:/)) {
|
|
1702
|
+
if (current?.name) {
|
|
1703
|
+
if (currentCreatedBy?.owner) {
|
|
1704
|
+
(current.created_by ??= []).push(currentCreatedBy);
|
|
1705
|
+
currentCreatedBy = null;
|
|
1706
|
+
}
|
|
1707
|
+
models.push({
|
|
1708
|
+
name: current.name,
|
|
1709
|
+
independently_created: current.independently_created ?? false,
|
|
1710
|
+
creation_file: current.creation_file,
|
|
1711
|
+
creation_function: current.creation_function,
|
|
1712
|
+
side_effects: current.side_effects,
|
|
1713
|
+
created_by: current.created_by ?? []
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
current = { name: line.match(/name:\s*"?([^"'\n]+)"?/)?.[1]?.trim() };
|
|
1717
|
+
inCreatedBy = false;
|
|
1718
|
+
currentCreatedBy = null;
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
if (!current) continue;
|
|
1722
|
+
const kvMatch = line.match(/^\s{4}(\w+):\s*(.+)/);
|
|
1723
|
+
if (kvMatch && !inCreatedBy) {
|
|
1724
|
+
const [, key, val] = kvMatch;
|
|
1725
|
+
const v = val.replace(/^["']|["']$/g, "").trim();
|
|
1726
|
+
switch (key) {
|
|
1727
|
+
case "independently_created":
|
|
1728
|
+
current.independently_created = v === "true";
|
|
1729
|
+
break;
|
|
1730
|
+
case "creation_file":
|
|
1731
|
+
current.creation_file = v;
|
|
1732
|
+
break;
|
|
1733
|
+
case "creation_function":
|
|
1734
|
+
current.creation_function = v;
|
|
1735
|
+
break;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if (line.match(/^\s{4}side_effects:/)) {
|
|
1739
|
+
current.side_effects = [];
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
if (current.side_effects !== void 0 && line.match(/^\s{6}-\s/)) {
|
|
1743
|
+
const val = line.replace(/^\s{6}-\s*/, "").replace(/^["']|["']$/g, "").trim();
|
|
1744
|
+
if (val) current.side_effects.push(val);
|
|
1745
|
+
continue;
|
|
1746
|
+
}
|
|
1747
|
+
if (line.match(/^\s{4}created_by:/)) {
|
|
1748
|
+
current.created_by = [];
|
|
1749
|
+
current.side_effects = void 0;
|
|
1750
|
+
inCreatedBy = true;
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
if (inCreatedBy) {
|
|
1754
|
+
if (line.match(/^\s{6}- owner:/)) {
|
|
1755
|
+
if (currentCreatedBy?.owner) {
|
|
1756
|
+
current.created_by.push(currentCreatedBy);
|
|
1757
|
+
}
|
|
1758
|
+
currentCreatedBy = { owner: line.match(/owner:\s*"?([^"'\n]+)"?/)?.[1]?.trim() ?? "" };
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
if (currentCreatedBy && line.match(/^\s{8}via:/)) {
|
|
1762
|
+
currentCreatedBy.via = line.match(/via:\s*"?([^"'\n]+)"?/)?.[1]?.trim();
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
if (currentCreatedBy && line.match(/^\s{8}why:/)) {
|
|
1766
|
+
currentCreatedBy.why = line.match(/why:\s*"?([^"'\n]+)"?/)?.[1]?.trim();
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
if (current?.name) {
|
|
1772
|
+
if (currentCreatedBy?.owner) {
|
|
1773
|
+
(current.created_by ??= []).push(currentCreatedBy);
|
|
1774
|
+
}
|
|
1775
|
+
models.push({
|
|
1776
|
+
name: current.name,
|
|
1777
|
+
independently_created: current.independently_created ?? false,
|
|
1778
|
+
creation_file: current.creation_file,
|
|
1779
|
+
creation_function: current.creation_function,
|
|
1780
|
+
side_effects: current.side_effects,
|
|
1781
|
+
created_by: current.created_by ?? []
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
return models;
|
|
1785
|
+
}
|
|
1786
|
+
function resolveEntityOrder(models) {
|
|
1787
|
+
const factoryModels = models.filter((m) => m.independently_created);
|
|
1788
|
+
const nameSet = new Set(factoryModels.map((m) => m.name));
|
|
1789
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
1790
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
1791
|
+
for (const m of factoryModels) {
|
|
1792
|
+
inDegree.set(m.name, 0);
|
|
1793
|
+
dependents.set(m.name, []);
|
|
1794
|
+
}
|
|
1795
|
+
for (const m of factoryModels) {
|
|
1796
|
+
for (const dep of m.created_by) {
|
|
1797
|
+
if (nameSet.has(dep.owner)) {
|
|
1798
|
+
inDegree.set(m.name, (inDegree.get(m.name) ?? 0) + 1);
|
|
1799
|
+
dependents.get(dep.owner).push(m.name);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
const queue = [];
|
|
1804
|
+
for (const [name, deg] of inDegree) {
|
|
1805
|
+
if (deg === 0) queue.push(name);
|
|
1806
|
+
}
|
|
1807
|
+
queue.sort();
|
|
1808
|
+
const result = [];
|
|
1809
|
+
while (queue.length > 0) {
|
|
1810
|
+
const name = queue.shift();
|
|
1811
|
+
result.push(name);
|
|
1812
|
+
for (const dep of dependents.get(name) ?? []) {
|
|
1813
|
+
const newDeg = (inDegree.get(dep) ?? 1) - 1;
|
|
1814
|
+
inDegree.set(dep, newDeg);
|
|
1815
|
+
if (newDeg === 0) {
|
|
1816
|
+
queue.push(dep);
|
|
1817
|
+
queue.sort();
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
if (result.length !== factoryModels.length) {
|
|
1822
|
+
const missing = factoryModels.filter((m) => !result.includes(m.name)).map((m) => m.name);
|
|
1823
|
+
console.warn(`[recipe-builder] Circular dependency detected for: ${missing.join(", ")}. Appending in original order.`);
|
|
1824
|
+
for (const m of missing) result.push(m);
|
|
1825
|
+
}
|
|
1826
|
+
return result;
|
|
1827
|
+
}
|
|
1828
|
+
function getEntityDependencyChain(entityName, models, entityOrder) {
|
|
1829
|
+
const modelMap = new Map(models.map((m) => [m.name, m]));
|
|
1830
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1831
|
+
const chain = [];
|
|
1832
|
+
function walk(name) {
|
|
1833
|
+
if (visited.has(name)) return;
|
|
1834
|
+
visited.add(name);
|
|
1835
|
+
const model = modelMap.get(name);
|
|
1836
|
+
if (!model) return;
|
|
1837
|
+
for (const dep of model.created_by) {
|
|
1838
|
+
if (entityOrder.includes(dep.owner)) {
|
|
1839
|
+
walk(dep.owner);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
chain.push(name);
|
|
1843
|
+
}
|
|
1844
|
+
walk(entityName);
|
|
1845
|
+
return chain;
|
|
1846
|
+
}
|
|
1847
|
+
var init_entity_order = __esm({
|
|
1848
|
+
"src/agents/04-recipe-builder/entity-order.ts"() {
|
|
1849
|
+
"use strict";
|
|
1850
|
+
init_esm_shims();
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
|
|
1854
|
+
// src/agents/02-entity-audit/audit-table.ts
|
|
1855
|
+
async function parseAuditedModels(outputDir) {
|
|
1856
|
+
try {
|
|
1857
|
+
return await parseEntityAudit(outputDir);
|
|
1858
|
+
} catch {
|
|
1859
|
+
return [];
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
function truncate2(s, max) {
|
|
1863
|
+
if (s.length <= max) return s;
|
|
1864
|
+
return s.slice(0, max - 1).trimEnd() + "\u2026";
|
|
1865
|
+
}
|
|
1866
|
+
function pad2(s, width) {
|
|
1867
|
+
return s + " ".repeat(Math.max(0, width - s.length));
|
|
1868
|
+
}
|
|
1869
|
+
function creationSource(m) {
|
|
1870
|
+
if (m.independently_created) {
|
|
1871
|
+
return m.creation_function ?? m.creation_file ?? "(own creation API)";
|
|
1872
|
+
}
|
|
1873
|
+
const parents = m.created_by.map((cb) => cb.owner).filter(Boolean);
|
|
1874
|
+
return parents.length ? `\u2190 ${parents.join(", ")}` : "(no known creator)";
|
|
1875
|
+
}
|
|
1876
|
+
function renderEntityAuditTable(models) {
|
|
1877
|
+
if (models.length === 0) return "";
|
|
1878
|
+
const NAME_MAX = 32;
|
|
1879
|
+
const SRC_MAX = 52;
|
|
1880
|
+
const rows = models.map((m, i) => ({
|
|
1881
|
+
num: String(i + 1),
|
|
1882
|
+
name: truncate2(m.name, NAME_MAX),
|
|
1883
|
+
creation: m.independently_created ? "standalone" : "side-effect",
|
|
1884
|
+
source: truncate2(creationSource(m).replace(/\s+/g, " ").trim(), SRC_MAX)
|
|
1885
|
+
}));
|
|
1886
|
+
const numW = Math.max(1, ...rows.map((r) => r.num.length));
|
|
1887
|
+
const nameW = Math.max("Entity".length, ...rows.map((r) => r.name.length));
|
|
1888
|
+
const critW = Math.max("Creation".length, ...rows.map((r) => r.creation.length));
|
|
1889
|
+
const standalone = models.filter((m) => m.independently_created).length;
|
|
1890
|
+
const header = `${BOLD3}${pad2("#", numW)} ${pad2("Entity", nameW)} ${pad2("Creation", critW)} Created from${RESET5}`;
|
|
1891
|
+
const sep = `${DIM5}${"\u2500".repeat(numW + nameW + critW + SRC_MAX + 6)}${RESET5}`;
|
|
1892
|
+
const body = rows.map((r) => {
|
|
1893
|
+
const line = `${pad2(r.num, numW)} ${pad2(r.name, nameW)} ${pad2(r.creation, critW)} ${r.source}`;
|
|
1894
|
+
return r.creation === "standalone" ? `${YELLOW3}${line}${RESET5}` : line;
|
|
1895
|
+
}).join("\n");
|
|
1896
|
+
const caption = `${DIM5}${models.length} models \xB7 ${standalone} standalone \xB7 ${models.length - standalone} side-effect-only${RESET5}`;
|
|
1897
|
+
return `${header}
|
|
1898
|
+
${sep}
|
|
1899
|
+
${body}
|
|
1900
|
+
${caption}`;
|
|
1901
|
+
}
|
|
1902
|
+
var RESET5, DIM5, YELLOW3, BOLD3;
|
|
1903
|
+
var init_audit_table = __esm({
|
|
1904
|
+
"src/agents/02-entity-audit/audit-table.ts"() {
|
|
1905
|
+
"use strict";
|
|
1906
|
+
init_esm_shims();
|
|
1907
|
+
init_entity_order();
|
|
1908
|
+
RESET5 = "\x1B[0m";
|
|
1909
|
+
DIM5 = "\x1B[2m";
|
|
1910
|
+
YELLOW3 = "\x1B[33m";
|
|
1911
|
+
BOLD3 = "\x1B[1m";
|
|
1912
|
+
}
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1588
1915
|
// src/agents/02-entity-audit/prompt.ts
|
|
1589
1916
|
var SYSTEM_PROMPT3;
|
|
1590
1917
|
var init_prompt2 = __esm({
|
|
@@ -1729,8 +2056,8 @@ var entity_audit_exports = {};
|
|
|
1729
2056
|
__export(entity_audit_exports, {
|
|
1730
2057
|
runEntityAudit: () => runEntityAudit
|
|
1731
2058
|
});
|
|
1732
|
-
import { readFile as
|
|
1733
|
-
import { join as
|
|
2059
|
+
import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
2060
|
+
import { join as join13 } from "path";
|
|
1734
2061
|
import { tool as tool11 } from "ai";
|
|
1735
2062
|
import { z as z11 } from "zod";
|
|
1736
2063
|
import { glob as glob3 } from "glob";
|
|
@@ -1859,7 +2186,7 @@ async function findPrismaSchema(projectRoot) {
|
|
|
1859
2186
|
return candidates[0] ?? null;
|
|
1860
2187
|
}
|
|
1861
2188
|
async function extractPrismaModels(schemaPath) {
|
|
1862
|
-
const content = await
|
|
2189
|
+
const content = await readFile8(schemaPath, "utf-8");
|
|
1863
2190
|
return content.split("\n").filter((line) => line.startsWith("model ")).map((line) => line.split(/\s+/)[1]).filter((name) => name != null);
|
|
1864
2191
|
}
|
|
1865
2192
|
async function detectFrameworkAndModels(projectRoot) {
|
|
@@ -1941,7 +2268,7 @@ write_file already targets the output directory \u2014 use just the filename.`;
|
|
|
1941
2268
|
logger.summary();
|
|
1942
2269
|
if (!result && tracker.auditedModels.size > 0) {
|
|
1943
2270
|
const markdown = tracker.generateAuditMarkdown();
|
|
1944
|
-
const auditPath =
|
|
2271
|
+
const auditPath = join13(input.outputDir, "entity-audit.md");
|
|
1945
2272
|
await writeFile4(auditPath, markdown, "utf-8");
|
|
1946
2273
|
const cov = tracker.coverage();
|
|
1947
2274
|
result = {
|
|
@@ -1954,6 +2281,10 @@ write_file already targets the output directory \u2014 use just the filename.`;
|
|
|
1954
2281
|
agentId: "entity-audit",
|
|
1955
2282
|
outputDir: input.outputDir,
|
|
1956
2283
|
nonInteractive: input.nonInteractive,
|
|
2284
|
+
renderSummary: async () => {
|
|
2285
|
+
const models = await parseAuditedModels(input.outputDir);
|
|
2286
|
+
return models.length ? renderEntityAuditTable(models) : void 0;
|
|
2287
|
+
},
|
|
1957
2288
|
reviewGuidance: "Each model entry has these key fields:\n independently_created \u2014 true if this entity has its own creation API/function, false if only created as a side effect\n creation_file / creation_function \u2014 where in YOUR code this entity gets created\n side_effects \u2014 other entities that get created automatically when this one is created\n created_by \u2014 which parent entity's creation triggers this one\n\nWhat to check:\n - Every database model should be listed\n - independently_created should be correct \u2014 models created only as side effects should be false\n - creation_file and creation_function should reference real code in your project\n - Side effects should be complete (e.g., creating a User also creates a Profile)",
|
|
1958
2289
|
onFeedback: async (feedback) => {
|
|
1959
2290
|
result = void 0;
|
|
@@ -1970,9 +2301,9 @@ When done with changes, call finish again.`;
|
|
|
1970
2301
|
}
|
|
1971
2302
|
});
|
|
1972
2303
|
if (!reviewed) {
|
|
1973
|
-
const auditPath =
|
|
2304
|
+
const auditPath = join13(input.outputDir, "entity-audit.md");
|
|
1974
2305
|
try {
|
|
1975
|
-
await
|
|
2306
|
+
await readFile8(auditPath, "utf-8");
|
|
1976
2307
|
return {
|
|
1977
2308
|
success: true,
|
|
1978
2309
|
artifacts: ["entity-audit.md"],
|
|
@@ -1996,6 +2327,7 @@ var init_entity_audit = __esm({
|
|
|
1996
2327
|
init_context();
|
|
1997
2328
|
init_model();
|
|
1998
2329
|
init_review();
|
|
2330
|
+
init_audit_table();
|
|
1999
2331
|
init_tools();
|
|
2000
2332
|
init_ask_user();
|
|
2001
2333
|
init_prompt2();
|
|
@@ -2109,11 +2441,11 @@ ${duals.length > 0 ? duals.map((m) => `- **${m.name}** \u2014 standalone: ${m.cr
|
|
|
2109
2441
|
});
|
|
2110
2442
|
|
|
2111
2443
|
// src/core/parse-entity-audit.ts
|
|
2112
|
-
import { readFile as
|
|
2113
|
-
import { join as
|
|
2444
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
2445
|
+
import { join as join14 } from "path";
|
|
2114
2446
|
async function parseEntityNames(outputDir) {
|
|
2115
2447
|
try {
|
|
2116
|
-
const content = await
|
|
2448
|
+
const content = await readFile9(join14(outputDir, "entity-audit.md"), "utf-8");
|
|
2117
2449
|
const names = [];
|
|
2118
2450
|
for (const line of content.split("\n")) {
|
|
2119
2451
|
const match = line.match(/^\s+-\s+name:\s+(.+)$/);
|
|
@@ -2196,8 +2528,8 @@ __export(scenario_recipe_exports, {
|
|
|
2196
2528
|
feedbackToScenario: () => feedbackToScenario,
|
|
2197
2529
|
runScenarioRecipe: () => runScenarioRecipe
|
|
2198
2530
|
});
|
|
2199
|
-
import { readFile as
|
|
2200
|
-
import { join as
|
|
2531
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
2532
|
+
import { join as join15 } from "path";
|
|
2201
2533
|
import { tool as tool12 } from "ai";
|
|
2202
2534
|
import { z as z12 } from "zod";
|
|
2203
2535
|
function buildFinishTool3(requiredEntities, outputDir, onFinish) {
|
|
@@ -2211,7 +2543,7 @@ function buildFinishTool3(requiredEntities, outputDir, onFinish) {
|
|
|
2211
2543
|
execute: async (input) => {
|
|
2212
2544
|
if (requiredEntities.length > 0) {
|
|
2213
2545
|
try {
|
|
2214
|
-
const content = await
|
|
2546
|
+
const content = await readFile10(join15(outputDir, "scenarios.md"), "utf-8");
|
|
2215
2547
|
const missing = requiredEntities.filter(
|
|
2216
2548
|
(e) => !content.includes(e)
|
|
2217
2549
|
);
|
|
@@ -2294,9 +2626,9 @@ When done with changes, call finish again.`;
|
|
|
2294
2626
|
}
|
|
2295
2627
|
});
|
|
2296
2628
|
if (!reviewed) {
|
|
2297
|
-
const scenariosPath =
|
|
2629
|
+
const scenariosPath = join15(input.outputDir, "scenarios.md");
|
|
2298
2630
|
try {
|
|
2299
|
-
await
|
|
2631
|
+
await readFile10(scenariosPath, "utf-8");
|
|
2300
2632
|
return {
|
|
2301
2633
|
success: true,
|
|
2302
2634
|
artifacts: ["scenarios.md"],
|
|
@@ -2349,249 +2681,83 @@ var init_scenario_recipe = __esm({
|
|
|
2349
2681
|
});
|
|
2350
2682
|
|
|
2351
2683
|
// src/agents/04-recipe-builder/state.ts
|
|
2352
|
-
import { readFile as
|
|
2353
|
-
import { join as
|
|
2684
|
+
import { readFile as readFile11, writeFile as writeFile5 } from "fs/promises";
|
|
2685
|
+
import { join as join16 } from "path";
|
|
2354
2686
|
function adapterKey(a) {
|
|
2355
2687
|
return `${a.language}:${a.framework}`;
|
|
2356
2688
|
}
|
|
2357
|
-
function adapterLabel(a) {
|
|
2358
|
-
const lang = a.language.charAt(0).toUpperCase() + a.language.slice(1);
|
|
2359
|
-
const fw = a.framework.charAt(0).toUpperCase() + a.framework.slice(1);
|
|
2360
|
-
return `${lang} + ${fw}`;
|
|
2361
|
-
}
|
|
2362
|
-
function findAdapter(language, framework) {
|
|
2363
|
-
return ALL_ADAPTERS.find(
|
|
2364
|
-
(a) => a.language === language.toLowerCase() && a.framework === framework.toLowerCase()
|
|
2365
|
-
);
|
|
2366
|
-
}
|
|
2367
|
-
function initialRecipeState() {
|
|
2368
|
-
return {
|
|
2369
|
-
phase: "tech-detect",
|
|
2370
|
-
techStack: null,
|
|
2371
|
-
entityOrder: [],
|
|
2372
|
-
entities: {},
|
|
2373
|
-
currentEntityIndex: 0,
|
|
2374
|
-
sdkEndpointUrl: null,
|
|
2375
|
-
sharedSecret: null
|
|
2376
|
-
};
|
|
2377
|
-
}
|
|
2378
|
-
async function loadRecipeState(outputDir) {
|
|
2379
|
-
try {
|
|
2380
|
-
const raw = await readFile9(join12(outputDir, STATE_FILE2), "utf-8");
|
|
2381
|
-
return JSON.parse(raw);
|
|
2382
|
-
} catch {
|
|
2383
|
-
return null;
|
|
2384
|
-
}
|
|
2385
|
-
}
|
|
2386
|
-
async function saveRecipeState(outputDir, state) {
|
|
2387
|
-
await writeFile5(join12(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
|
|
2388
|
-
}
|
|
2389
|
-
var ALL_ADAPTERS, ADAPTER_HINTS, STATE_FILE2;
|
|
2390
|
-
var init_state = __esm({
|
|
2391
|
-
"src/agents/04-recipe-builder/state.ts"() {
|
|
2392
|
-
"use strict";
|
|
2393
|
-
init_esm_shims();
|
|
2394
|
-
ALL_ADAPTERS = [
|
|
2395
|
-
{ language: "typescript", framework: "web", sdkPackage: "@autonoma-ai/sdk", adapterPackage: "@autonoma-ai/server-web" },
|
|
2396
|
-
{ language: "typescript", framework: "express", sdkPackage: "@autonoma-ai/sdk", adapterPackage: "@autonoma-ai/server-express" },
|
|
2397
|
-
{ language: "typescript", framework: "node", sdkPackage: "@autonoma-ai/sdk", adapterPackage: "@autonoma-ai/server-node" },
|
|
2398
|
-
{ language: "typescript", framework: "hono", sdkPackage: "@autonoma-ai/sdk", adapterPackage: "@autonoma-ai/server-hono" },
|
|
2399
|
-
{ language: "python", framework: "flask", sdkPackage: "autonoma-ai", adapterPackage: "autonoma_flask" },
|
|
2400
|
-
{ language: "python", framework: "fastapi", sdkPackage: "autonoma-ai", adapterPackage: "autonoma_fastapi" },
|
|
2401
|
-
{ language: "python", framework: "django", sdkPackage: "autonoma-ai", adapterPackage: "autonoma_django" },
|
|
2402
|
-
{ language: "go", framework: "gin", sdkPackage: "github.com/autonoma-ai/sdk-go", adapterPackage: "github.com/autonoma-ai/sdk-go" },
|
|
2403
|
-
{ language: "ruby", framework: "rails", sdkPackage: "autonoma-ai", adapterPackage: "autonoma-ai" },
|
|
2404
|
-
{ language: "ruby", framework: "rack", sdkPackage: "autonoma-ai", adapterPackage: "autonoma-ai" },
|
|
2405
|
-
{ language: "java", framework: "spring", sdkPackage: "autonoma-sdk", adapterPackage: "autonoma-spring" },
|
|
2406
|
-
{ language: "php", framework: "laravel", sdkPackage: "autonoma-ai/sdk", adapterPackage: "autonoma-ai/sdk" },
|
|
2407
|
-
{ language: "rust", framework: "axum", sdkPackage: "autonoma-sdk", adapterPackage: "autonoma-sdk" },
|
|
2408
|
-
{ language: "rust", framework: "actix", sdkPackage: "autonoma-sdk", adapterPackage: "autonoma-sdk" },
|
|
2409
|
-
{ language: "elixir", framework: "plug", sdkPackage: "autonoma", adapterPackage: "autonoma" }
|
|
2410
|
-
];
|
|
2411
|
-
ADAPTER_HINTS = {
|
|
2412
|
-
"typescript:express": "Express.js and Express-compatible frameworks",
|
|
2413
|
-
"typescript:node": "Plain Node.js HTTP server (built-in http module)",
|
|
2414
|
-
"typescript:hono": "Hono framework (works with Bun, Deno, Node)",
|
|
2415
|
-
"typescript:web": "Web Standard Request/Response \u2014 covers Next.js, Remix, Nuxt, Bun, Deno, SvelteKit, Astro",
|
|
2416
|
-
"python:flask": "Flask framework",
|
|
2417
|
-
"python:fastapi": "FastAPI framework",
|
|
2418
|
-
"python:django": "Django framework",
|
|
2419
|
-
"go:gin": "Gin framework for Go",
|
|
2420
|
-
"ruby:rails": "Ruby on Rails",
|
|
2421
|
-
"ruby:rack": "Rack-based Ruby frameworks (Sinatra, Hanami, etc.)",
|
|
2422
|
-
"java:spring": "Spring Boot / Spring Framework",
|
|
2423
|
-
"php:laravel": "Laravel framework",
|
|
2424
|
-
"rust:axum": "Axum framework for Rust",
|
|
2425
|
-
"rust:actix": "Actix Web framework for Rust",
|
|
2426
|
-
"elixir:plug": "Plug / Phoenix framework for Elixir"
|
|
2427
|
-
};
|
|
2428
|
-
STATE_FILE2 = ".recipe-builder-state.json";
|
|
2429
|
-
}
|
|
2430
|
-
});
|
|
2431
|
-
|
|
2432
|
-
// src/agents/04-recipe-builder/entity-order.ts
|
|
2433
|
-
import { readFile as readFile10 } from "fs/promises";
|
|
2434
|
-
import { join as join13 } from "path";
|
|
2435
|
-
async function parseEntityAudit(outputDir) {
|
|
2436
|
-
const raw = await readFile10(join13(outputDir, "entity-audit.md"), "utf-8");
|
|
2437
|
-
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
2438
|
-
if (!fmMatch) throw new Error("entity-audit.md has no YAML frontmatter");
|
|
2439
|
-
const yaml = fmMatch[1];
|
|
2440
|
-
const models = [];
|
|
2441
|
-
let current = null;
|
|
2442
|
-
let inCreatedBy = false;
|
|
2443
|
-
let currentCreatedBy = null;
|
|
2444
|
-
for (const line of yaml.split("\n")) {
|
|
2445
|
-
if (line.match(/^\s{2}- name:/)) {
|
|
2446
|
-
if (current?.name) {
|
|
2447
|
-
if (currentCreatedBy?.owner) {
|
|
2448
|
-
(current.created_by ??= []).push(currentCreatedBy);
|
|
2449
|
-
currentCreatedBy = null;
|
|
2450
|
-
}
|
|
2451
|
-
models.push({
|
|
2452
|
-
name: current.name,
|
|
2453
|
-
independently_created: current.independently_created ?? false,
|
|
2454
|
-
creation_file: current.creation_file,
|
|
2455
|
-
creation_function: current.creation_function,
|
|
2456
|
-
side_effects: current.side_effects,
|
|
2457
|
-
created_by: current.created_by ?? []
|
|
2458
|
-
});
|
|
2459
|
-
}
|
|
2460
|
-
current = { name: line.match(/name:\s*"?([^"'\n]+)"?/)?.[1]?.trim() };
|
|
2461
|
-
inCreatedBy = false;
|
|
2462
|
-
currentCreatedBy = null;
|
|
2463
|
-
continue;
|
|
2464
|
-
}
|
|
2465
|
-
if (!current) continue;
|
|
2466
|
-
const kvMatch = line.match(/^\s{4}(\w+):\s*(.+)/);
|
|
2467
|
-
if (kvMatch && !inCreatedBy) {
|
|
2468
|
-
const [, key, val] = kvMatch;
|
|
2469
|
-
const v = val.replace(/^["']|["']$/g, "").trim();
|
|
2470
|
-
switch (key) {
|
|
2471
|
-
case "independently_created":
|
|
2472
|
-
current.independently_created = v === "true";
|
|
2473
|
-
break;
|
|
2474
|
-
case "creation_file":
|
|
2475
|
-
current.creation_file = v;
|
|
2476
|
-
break;
|
|
2477
|
-
case "creation_function":
|
|
2478
|
-
current.creation_function = v;
|
|
2479
|
-
break;
|
|
2480
|
-
}
|
|
2481
|
-
}
|
|
2482
|
-
if (line.match(/^\s{4}side_effects:/)) {
|
|
2483
|
-
current.side_effects = [];
|
|
2484
|
-
continue;
|
|
2485
|
-
}
|
|
2486
|
-
if (current.side_effects !== void 0 && line.match(/^\s{6}-\s/)) {
|
|
2487
|
-
const val = line.replace(/^\s{6}-\s*/, "").replace(/^["']|["']$/g, "").trim();
|
|
2488
|
-
if (val) current.side_effects.push(val);
|
|
2489
|
-
continue;
|
|
2490
|
-
}
|
|
2491
|
-
if (line.match(/^\s{4}created_by:/)) {
|
|
2492
|
-
current.created_by = [];
|
|
2493
|
-
current.side_effects = void 0;
|
|
2494
|
-
inCreatedBy = true;
|
|
2495
|
-
continue;
|
|
2496
|
-
}
|
|
2497
|
-
if (inCreatedBy) {
|
|
2498
|
-
if (line.match(/^\s{6}- owner:/)) {
|
|
2499
|
-
if (currentCreatedBy?.owner) {
|
|
2500
|
-
current.created_by.push(currentCreatedBy);
|
|
2501
|
-
}
|
|
2502
|
-
currentCreatedBy = { owner: line.match(/owner:\s*"?([^"'\n]+)"?/)?.[1]?.trim() ?? "" };
|
|
2503
|
-
continue;
|
|
2504
|
-
}
|
|
2505
|
-
if (currentCreatedBy && line.match(/^\s{8}via:/)) {
|
|
2506
|
-
currentCreatedBy.via = line.match(/via:\s*"?([^"'\n]+)"?/)?.[1]?.trim();
|
|
2507
|
-
continue;
|
|
2508
|
-
}
|
|
2509
|
-
if (currentCreatedBy && line.match(/^\s{8}why:/)) {
|
|
2510
|
-
currentCreatedBy.why = line.match(/why:\s*"?([^"'\n]+)"?/)?.[1]?.trim();
|
|
2511
|
-
continue;
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
}
|
|
2515
|
-
if (current?.name) {
|
|
2516
|
-
if (currentCreatedBy?.owner) {
|
|
2517
|
-
(current.created_by ??= []).push(currentCreatedBy);
|
|
2518
|
-
}
|
|
2519
|
-
models.push({
|
|
2520
|
-
name: current.name,
|
|
2521
|
-
independently_created: current.independently_created ?? false,
|
|
2522
|
-
creation_file: current.creation_file,
|
|
2523
|
-
creation_function: current.creation_function,
|
|
2524
|
-
side_effects: current.side_effects,
|
|
2525
|
-
created_by: current.created_by ?? []
|
|
2526
|
-
});
|
|
2527
|
-
}
|
|
2528
|
-
return models;
|
|
2529
|
-
}
|
|
2530
|
-
function resolveEntityOrder(models) {
|
|
2531
|
-
const factoryModels = models.filter((m) => m.independently_created);
|
|
2532
|
-
const nameSet = new Set(factoryModels.map((m) => m.name));
|
|
2533
|
-
const inDegree = /* @__PURE__ */ new Map();
|
|
2534
|
-
const dependents = /* @__PURE__ */ new Map();
|
|
2535
|
-
for (const m of factoryModels) {
|
|
2536
|
-
inDegree.set(m.name, 0);
|
|
2537
|
-
dependents.set(m.name, []);
|
|
2538
|
-
}
|
|
2539
|
-
for (const m of factoryModels) {
|
|
2540
|
-
for (const dep of m.created_by) {
|
|
2541
|
-
if (nameSet.has(dep.owner)) {
|
|
2542
|
-
inDegree.set(m.name, (inDegree.get(m.name) ?? 0) + 1);
|
|
2543
|
-
dependents.get(dep.owner).push(m.name);
|
|
2544
|
-
}
|
|
2545
|
-
}
|
|
2546
|
-
}
|
|
2547
|
-
const queue = [];
|
|
2548
|
-
for (const [name, deg] of inDegree) {
|
|
2549
|
-
if (deg === 0) queue.push(name);
|
|
2550
|
-
}
|
|
2551
|
-
queue.sort();
|
|
2552
|
-
const result = [];
|
|
2553
|
-
while (queue.length > 0) {
|
|
2554
|
-
const name = queue.shift();
|
|
2555
|
-
result.push(name);
|
|
2556
|
-
for (const dep of dependents.get(name) ?? []) {
|
|
2557
|
-
const newDeg = (inDegree.get(dep) ?? 1) - 1;
|
|
2558
|
-
inDegree.set(dep, newDeg);
|
|
2559
|
-
if (newDeg === 0) {
|
|
2560
|
-
queue.push(dep);
|
|
2561
|
-
queue.sort();
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
}
|
|
2565
|
-
if (result.length !== factoryModels.length) {
|
|
2566
|
-
const missing = factoryModels.filter((m) => !result.includes(m.name)).map((m) => m.name);
|
|
2567
|
-
console.warn(`[recipe-builder] Circular dependency detected for: ${missing.join(", ")}. Appending in original order.`);
|
|
2568
|
-
for (const m of missing) result.push(m);
|
|
2569
|
-
}
|
|
2570
|
-
return result;
|
|
2689
|
+
function adapterLabel(a) {
|
|
2690
|
+
const lang = a.language.charAt(0).toUpperCase() + a.language.slice(1);
|
|
2691
|
+
const fw = a.framework.charAt(0).toUpperCase() + a.framework.slice(1);
|
|
2692
|
+
return `${lang} + ${fw}`;
|
|
2571
2693
|
}
|
|
2572
|
-
function
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2694
|
+
function findAdapter(language, framework) {
|
|
2695
|
+
return ALL_ADAPTERS.find(
|
|
2696
|
+
(a) => a.language === language.toLowerCase() && a.framework === framework.toLowerCase()
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
function initialRecipeState() {
|
|
2700
|
+
return {
|
|
2701
|
+
phase: "tech-detect",
|
|
2702
|
+
techStack: null,
|
|
2703
|
+
entityOrder: [],
|
|
2704
|
+
entities: {},
|
|
2705
|
+
currentEntityIndex: 0,
|
|
2706
|
+
sdkEndpointUrl: null,
|
|
2707
|
+
sharedSecret: null
|
|
2708
|
+
};
|
|
2709
|
+
}
|
|
2710
|
+
async function loadRecipeState(outputDir) {
|
|
2711
|
+
try {
|
|
2712
|
+
const raw = await readFile11(join16(outputDir, STATE_FILE2), "utf-8");
|
|
2713
|
+
return JSON.parse(raw);
|
|
2714
|
+
} catch {
|
|
2715
|
+
return null;
|
|
2587
2716
|
}
|
|
2588
|
-
walk(entityName);
|
|
2589
|
-
return chain;
|
|
2590
2717
|
}
|
|
2591
|
-
|
|
2592
|
-
"
|
|
2718
|
+
async function saveRecipeState(outputDir, state) {
|
|
2719
|
+
await writeFile5(join16(outputDir, STATE_FILE2), JSON.stringify(state, null, 2), "utf-8");
|
|
2720
|
+
}
|
|
2721
|
+
var ALL_ADAPTERS, ADAPTER_HINTS, STATE_FILE2;
|
|
2722
|
+
var init_state = __esm({
|
|
2723
|
+
"src/agents/04-recipe-builder/state.ts"() {
|
|
2593
2724
|
"use strict";
|
|
2594
2725
|
init_esm_shims();
|
|
2726
|
+
ALL_ADAPTERS = [
|
|
2727
|
+
{ language: "typescript", framework: "web", sdkPackage: "@autonoma-ai/sdk", adapterPackage: "@autonoma-ai/server-web" },
|
|
2728
|
+
{ language: "typescript", framework: "express", sdkPackage: "@autonoma-ai/sdk", adapterPackage: "@autonoma-ai/server-express" },
|
|
2729
|
+
{ language: "typescript", framework: "node", sdkPackage: "@autonoma-ai/sdk", adapterPackage: "@autonoma-ai/server-node" },
|
|
2730
|
+
{ language: "typescript", framework: "hono", sdkPackage: "@autonoma-ai/sdk", adapterPackage: "@autonoma-ai/server-hono" },
|
|
2731
|
+
{ language: "python", framework: "flask", sdkPackage: "autonoma-ai", adapterPackage: "autonoma_flask" },
|
|
2732
|
+
{ language: "python", framework: "fastapi", sdkPackage: "autonoma-ai", adapterPackage: "autonoma_fastapi" },
|
|
2733
|
+
{ language: "python", framework: "django", sdkPackage: "autonoma-ai", adapterPackage: "autonoma_django" },
|
|
2734
|
+
{ language: "go", framework: "gin", sdkPackage: "github.com/autonoma-ai/sdk-go", adapterPackage: "github.com/autonoma-ai/sdk-go" },
|
|
2735
|
+
{ language: "ruby", framework: "rails", sdkPackage: "autonoma-ai", adapterPackage: "autonoma-ai" },
|
|
2736
|
+
{ language: "ruby", framework: "rack", sdkPackage: "autonoma-ai", adapterPackage: "autonoma-ai" },
|
|
2737
|
+
{ language: "java", framework: "spring", sdkPackage: "autonoma-sdk", adapterPackage: "autonoma-spring" },
|
|
2738
|
+
{ language: "php", framework: "laravel", sdkPackage: "autonoma-ai/sdk", adapterPackage: "autonoma-ai/sdk" },
|
|
2739
|
+
{ language: "rust", framework: "axum", sdkPackage: "autonoma-sdk", adapterPackage: "autonoma-sdk" },
|
|
2740
|
+
{ language: "rust", framework: "actix", sdkPackage: "autonoma-sdk", adapterPackage: "autonoma-sdk" },
|
|
2741
|
+
{ language: "elixir", framework: "plug", sdkPackage: "autonoma", adapterPackage: "autonoma" }
|
|
2742
|
+
];
|
|
2743
|
+
ADAPTER_HINTS = {
|
|
2744
|
+
"typescript:express": "Express.js and Express-compatible frameworks",
|
|
2745
|
+
"typescript:node": "Plain Node.js HTTP server (built-in http module)",
|
|
2746
|
+
"typescript:hono": "Hono framework (works with Bun, Deno, Node)",
|
|
2747
|
+
"typescript:web": "Web Standard Request/Response \u2014 covers Next.js, Remix, Nuxt, Bun, Deno, SvelteKit, Astro",
|
|
2748
|
+
"python:flask": "Flask framework",
|
|
2749
|
+
"python:fastapi": "FastAPI framework",
|
|
2750
|
+
"python:django": "Django framework",
|
|
2751
|
+
"go:gin": "Gin framework for Go",
|
|
2752
|
+
"ruby:rails": "Ruby on Rails",
|
|
2753
|
+
"ruby:rack": "Rack-based Ruby frameworks (Sinatra, Hanami, etc.)",
|
|
2754
|
+
"java:spring": "Spring Boot / Spring Framework",
|
|
2755
|
+
"php:laravel": "Laravel framework",
|
|
2756
|
+
"rust:axum": "Axum framework for Rust",
|
|
2757
|
+
"rust:actix": "Actix Web framework for Rust",
|
|
2758
|
+
"elixir:plug": "Plug / Phoenix framework for Elixir"
|
|
2759
|
+
};
|
|
2760
|
+
STATE_FILE2 = ".recipe-builder-state.json";
|
|
2595
2761
|
}
|
|
2596
2762
|
});
|
|
2597
2763
|
|
|
@@ -2690,11 +2856,11 @@ When done, call finish with your findings.`;
|
|
|
2690
2856
|
|
|
2691
2857
|
// src/core/detect-pkg-manager.ts
|
|
2692
2858
|
import { existsSync as existsSync2 } from "fs";
|
|
2693
|
-
import { join as
|
|
2859
|
+
import { join as join17 } from "path";
|
|
2694
2860
|
function detectPackageManager(projectRoot) {
|
|
2695
|
-
if (existsSync2(
|
|
2696
|
-
if (existsSync2(
|
|
2697
|
-
if (existsSync2(
|
|
2861
|
+
if (existsSync2(join17(projectRoot, "bun.lock")) || existsSync2(join17(projectRoot, "bun.lockb"))) return "bun";
|
|
2862
|
+
if (existsSync2(join17(projectRoot, "pnpm-lock.yaml"))) return "pnpm";
|
|
2863
|
+
if (existsSync2(join17(projectRoot, "yarn.lock"))) return "yarn";
|
|
2698
2864
|
return "npm";
|
|
2699
2865
|
}
|
|
2700
2866
|
function installCommand(pm, ...packages) {
|
|
@@ -2724,7 +2890,7 @@ function spanReplacer(_match, cls) {
|
|
|
2724
2890
|
return ANSI[mainCls] ?? "";
|
|
2725
2891
|
}
|
|
2726
2892
|
function htmlToAnsi(html) {
|
|
2727
|
-
return html.replace(/<span class="hljs-([^"]+)">/g, spanReplacer).replace(/<\/span>/g,
|
|
2893
|
+
return html.replace(/<span class="hljs-([^"]+)">/g, spanReplacer).replace(/<\/span>/g, RESET6).replace(/'/g, "'").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"');
|
|
2728
2894
|
}
|
|
2729
2895
|
function highlightCode(code, language = "typescript") {
|
|
2730
2896
|
try {
|
|
@@ -2738,14 +2904,14 @@ function codeNoteFormat(line) {
|
|
|
2738
2904
|
if (line.includes("\x1B[")) return line;
|
|
2739
2905
|
return highlightCode(line);
|
|
2740
2906
|
}
|
|
2741
|
-
var
|
|
2907
|
+
var RESET6, ANSI;
|
|
2742
2908
|
var init_highlight = __esm({
|
|
2743
2909
|
"src/core/highlight.ts"() {
|
|
2744
2910
|
"use strict";
|
|
2745
2911
|
init_esm_shims();
|
|
2746
|
-
|
|
2912
|
+
RESET6 = "\x1B[0m";
|
|
2747
2913
|
ANSI = {
|
|
2748
|
-
reset:
|
|
2914
|
+
reset: RESET6,
|
|
2749
2915
|
keyword: "\x1B[35m",
|
|
2750
2916
|
string: "\x1B[32m",
|
|
2751
2917
|
number: "\x1B[33m",
|
|
@@ -2770,8 +2936,8 @@ var init_highlight = __esm({
|
|
|
2770
2936
|
});
|
|
2771
2937
|
|
|
2772
2938
|
// src/agents/04-recipe-builder/recipe.ts
|
|
2773
|
-
import { readFile as
|
|
2774
|
-
import { join as
|
|
2939
|
+
import { readFile as readFile12, writeFile as writeFile6 } from "fs/promises";
|
|
2940
|
+
import { join as join18 } from "path";
|
|
2775
2941
|
function buildSingleEntityRecipe(entityName, models, entityOrder, allEntities) {
|
|
2776
2942
|
const chain = getEntityDependencyChain(entityName, models, entityOrder);
|
|
2777
2943
|
const recipe = {};
|
|
@@ -2815,7 +2981,7 @@ function buildSubmittableRecipe(create, description) {
|
|
|
2815
2981
|
};
|
|
2816
2982
|
}
|
|
2817
2983
|
async function saveRecipe(outputDir, recipe) {
|
|
2818
|
-
await writeFile6(
|
|
2984
|
+
await writeFile6(join18(outputDir, RECIPE_FILE), JSON.stringify(recipe, null, 2), "utf-8");
|
|
2819
2985
|
}
|
|
2820
2986
|
var RECIPE_FILE;
|
|
2821
2987
|
var init_recipe = __esm({
|
|
@@ -2875,12 +3041,15 @@ var init_http_client = __esm({
|
|
|
2875
3041
|
|
|
2876
3042
|
// src/agents/04-recipe-builder/phases/entity-loop.ts
|
|
2877
3043
|
import * as p4 from "@clack/prompts";
|
|
2878
|
-
import { writeFile as writeFile7, readFile as
|
|
2879
|
-
import { join as
|
|
3044
|
+
import { writeFile as writeFile7, readFile as readFile13 } from "fs/promises";
|
|
3045
|
+
import { join as join19 } from "path";
|
|
2880
3046
|
import { tmpdir } from "os";
|
|
2881
3047
|
import { spawn as spawn2 } from "child_process";
|
|
2882
3048
|
import { tool as tool14 } from "ai";
|
|
2883
3049
|
import { z as z14 } from "zod";
|
|
3050
|
+
function summarizeCompletedAliases(completedEntities, excludeName) {
|
|
3051
|
+
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");
|
|
3052
|
+
}
|
|
2884
3053
|
async function proposeRecipeData(entityName, entityIndex, totalEntities, model, outputDir, _projectRoot, completedEntities) {
|
|
2885
3054
|
let result;
|
|
2886
3055
|
const { logger, onStepFinish } = buildDefaultStepLogger(`propose:${entityName}`, 20);
|
|
@@ -2894,7 +3063,7 @@ async function proposeRecipeData(entityName, entityIndex, totalEntities, model,
|
|
|
2894
3063
|
return { accepted: true };
|
|
2895
3064
|
}
|
|
2896
3065
|
});
|
|
2897
|
-
const completedAliases =
|
|
3066
|
+
const completedAliases = summarizeCompletedAliases(completedEntities, entityName);
|
|
2898
3067
|
const prompt = `[${entityIndex + 1}/${totalEntities}] Propose recipe data for entity "${entityName}".
|
|
2899
3068
|
|
|
2900
3069
|
Read scenarios.md and entity-audit.md from the output directory. Design records that match the scenario data.
|
|
@@ -2924,7 +3093,7 @@ Call finish with the JSON array of records.`;
|
|
|
2924
3093
|
logger.summary();
|
|
2925
3094
|
return result ?? [];
|
|
2926
3095
|
}
|
|
2927
|
-
async function reviseRecipeData(entityName, entityIndex, totalEntities, current, feedback, model, outputDir) {
|
|
3096
|
+
async function reviseRecipeData(entityName, entityIndex, totalEntities, current, feedback, model, outputDir, completedEntities) {
|
|
2928
3097
|
let revised;
|
|
2929
3098
|
const finishTool = tool14({
|
|
2930
3099
|
description: "Submit the fixed recipe data.",
|
|
@@ -2937,14 +3106,19 @@ async function reviseRecipeData(entityName, entityIndex, totalEntities, current,
|
|
|
2937
3106
|
}
|
|
2938
3107
|
});
|
|
2939
3108
|
const { logger, onStepFinish } = buildDefaultStepLogger(`fix:${entityName}`, 15);
|
|
3109
|
+
const completedAliases = summarizeCompletedAliases(completedEntities, entityName);
|
|
3110
|
+
const aliasBlock = completedAliases ? `Aliases declared by already-created parent entities (these are the ONLY valid _ref targets):
|
|
3111
|
+
${completedAliases}
|
|
3112
|
+
` : `This is a root entity \u2014 it has no parent entities to _ref.
|
|
3113
|
+
`;
|
|
2940
3114
|
await runAgent(
|
|
2941
3115
|
{
|
|
2942
3116
|
id: `fix-${entityName}`,
|
|
2943
|
-
systemPrompt: `You are fixing recipe data
|
|
3117
|
+
systemPrompt: `You are fixing recipe data based on user feedback (or a validation failure). Read the error, the current data, and the user's feedback. Read scenarios.md and entity-audit.md if needed. Fix the data and call finish.
|
|
2944
3118
|
|
|
2945
3119
|
Rules:
|
|
2946
3120
|
- _alias fields must be unique identifiers (e.g., "card_1", "transaction_1")
|
|
2947
|
-
- _ref fields reference
|
|
3121
|
+
- _ref fields must reference an alias that ALREADY EXISTS on a parent entity \u2014 see the list of valid targets below. Never invent a _ref to an alias that isn't listed.
|
|
2948
3122
|
- Read scenarios.md to verify you're using correct alias names from parent entities
|
|
2949
3123
|
- Field names must match the entity's schema from entity-audit.md`,
|
|
2950
3124
|
model,
|
|
@@ -2957,13 +3131,14 @@ Rules:
|
|
|
2957
3131
|
},
|
|
2958
3132
|
`[${entityIndex + 1}/${totalEntities}] Fix recipe data for "${entityName}".
|
|
2959
3133
|
|
|
2960
|
-
Current data
|
|
3134
|
+
Current data:
|
|
2961
3135
|
${JSON.stringify(current, null, 2)}
|
|
2962
3136
|
|
|
2963
|
-
|
|
3137
|
+
What's wrong / what to change:
|
|
2964
3138
|
${feedback}
|
|
2965
3139
|
|
|
2966
|
-
|
|
3140
|
+
${aliasBlock}
|
|
3141
|
+
Read scenarios.md and entity-audit.md to understand the correct aliases and schema. Apply the change and call finish.`,
|
|
2967
3142
|
() => revised
|
|
2968
3143
|
);
|
|
2969
3144
|
logger.summary();
|
|
@@ -3037,7 +3212,7 @@ Read the creation file from the project to understand the existing service/funct
|
|
|
3037
3212
|
logger.summary();
|
|
3038
3213
|
return result ?? "No instructions generated. Check the entity audit for creation_file and creation_function.";
|
|
3039
3214
|
}
|
|
3040
|
-
async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed, model, outputDir) {
|
|
3215
|
+
async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed, model, outputDir, completedEntities) {
|
|
3041
3216
|
p4.log.info(
|
|
3042
3217
|
`Legend for recipe fields:
|
|
3043
3218
|
_alias \u2014 Internal ID used to reference this record from other entities (e.g., { "_ref": "org_1" })
|
|
@@ -3060,7 +3235,7 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
|
|
|
3060
3235
|
if (p4.isCancel(action)) throw new Error("Recipe review cancelled");
|
|
3061
3236
|
if (action === "keep") return proposed;
|
|
3062
3237
|
if (action === "edit") {
|
|
3063
|
-
const tmpPath =
|
|
3238
|
+
const tmpPath = join19(tmpdir(), `autonoma-recipe-${entityName}.json`);
|
|
3064
3239
|
await writeFile7(tmpPath, JSON.stringify(proposed, null, 2), "utf-8");
|
|
3065
3240
|
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
3066
3241
|
p4.log.info(`Opening ${editor}... Save and close when done.`);
|
|
@@ -3069,7 +3244,7 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
|
|
|
3069
3244
|
proc.on("close", () => resolve5());
|
|
3070
3245
|
proc.on("error", reject);
|
|
3071
3246
|
});
|
|
3072
|
-
const edited = await
|
|
3247
|
+
const edited = await readFile13(tmpPath, "utf-8");
|
|
3073
3248
|
try {
|
|
3074
3249
|
proposed = JSON.parse(edited);
|
|
3075
3250
|
p4.note(JSON.stringify(proposed, null, 2), `Updated data for ${entityName}`, { format: codeNoteFormat });
|
|
@@ -3084,43 +3259,16 @@ async function reviewRecipeData(entityName, entityIndex, totalEntities, proposed
|
|
|
3084
3259
|
placeholder: "e.g., add more records, change field values, fix references..."
|
|
3085
3260
|
});
|
|
3086
3261
|
if (p4.isCancel(feedback) || !feedback.trim()) continue;
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
}
|
|
3097
|
-
});
|
|
3098
|
-
const { logger, onStepFinish } = buildDefaultStepLogger(`revise:${entityName}`, 10);
|
|
3099
|
-
await runAgent(
|
|
3100
|
-
{
|
|
3101
|
-
id: `revise-${entityName}`,
|
|
3102
|
-
systemPrompt: "You are revising recipe data based on user feedback. Read the current data, apply the feedback, and call finish with the updated records.",
|
|
3103
|
-
model,
|
|
3104
|
-
maxSteps: 10,
|
|
3105
|
-
tools: (_heartbeat) => ({
|
|
3106
|
-
read_output: buildReadFileTool(outputDir),
|
|
3107
|
-
finish: finishTool
|
|
3108
|
-
}),
|
|
3109
|
-
onStepFinish
|
|
3110
|
-
},
|
|
3111
|
-
`Current data for ${entityName}:
|
|
3112
|
-
${JSON.stringify(proposed, null, 2)}
|
|
3113
|
-
|
|
3114
|
-
User feedback: "${feedback}"
|
|
3115
|
-
|
|
3116
|
-
Revise the data and call finish.`,
|
|
3117
|
-
() => revised
|
|
3262
|
+
proposed = await reviseRecipeData(
|
|
3263
|
+
entityName,
|
|
3264
|
+
entityIndex,
|
|
3265
|
+
totalEntities,
|
|
3266
|
+
proposed,
|
|
3267
|
+
feedback.trim(),
|
|
3268
|
+
model,
|
|
3269
|
+
outputDir,
|
|
3270
|
+
completedEntities
|
|
3118
3271
|
);
|
|
3119
|
-
logger.summary();
|
|
3120
|
-
if (revised) {
|
|
3121
|
-
proposed = revised;
|
|
3122
|
-
p4.note(JSON.stringify(proposed, null, 2), `Revised data for ${entityName}`, { format: codeNoteFormat });
|
|
3123
|
-
}
|
|
3124
3272
|
}
|
|
3125
3273
|
}
|
|
3126
3274
|
}
|
|
@@ -3247,7 +3395,7 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
|
|
|
3247
3395
|
);
|
|
3248
3396
|
}
|
|
3249
3397
|
if (!nonInteractive) {
|
|
3250
|
-
recipeData = await reviewRecipeData(entityName, i, total, recipeData, model, outputDir);
|
|
3398
|
+
recipeData = await reviewRecipeData(entityName, i, total, recipeData, model, outputDir, state.entities);
|
|
3251
3399
|
}
|
|
3252
3400
|
state.entities[entityName] = {
|
|
3253
3401
|
entityName,
|
|
@@ -3270,20 +3418,21 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
|
|
|
3270
3418
|
projectRoot,
|
|
3271
3419
|
outputDir
|
|
3272
3420
|
);
|
|
3273
|
-
p4.note(instructions, `Implementation guide for ${entityName}`, { format: codeNoteFormat });
|
|
3274
3421
|
const DOCS_BASE2 = "https://docs.agent.autonoma.app";
|
|
3275
3422
|
p4.log.info(
|
|
3276
|
-
`
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3423
|
+
`Next: implement the ${entityName} factory. The block below is a copy-paste guide \u2014
|
|
3424
|
+
paste it into Claude Code (or your AI assistant) and it will write the factory in your codebase.
|
|
3425
|
+
A factory teaches the Autonoma SDK how to create and tear down ${entityName} records using your app's own code.
|
|
3426
|
+
Keep it local for now: implement it, run your app on localhost, and we'll test it live here. You deploy later.`
|
|
3280
3427
|
);
|
|
3428
|
+
p4.note(instructions, `Implementation guide for ${entityName} (paste into your AI assistant)`, { format: codeNoteFormat });
|
|
3429
|
+
p4.log.info(`Autonoma SDK docs: ${DOCS_BASE2}/sdk/environment-factory`);
|
|
3281
3430
|
if (i === 0) {
|
|
3282
|
-
p4.log.info("This is your first factory \u2014 the
|
|
3431
|
+
p4.log.info("This is your first factory \u2014 the guide includes one-time SDK setup. Later entities only need the factory function.");
|
|
3283
3432
|
}
|
|
3284
3433
|
notify("Autonoma", `${entityName} \u2014 implementation ready, waiting for you`);
|
|
3285
3434
|
const ready = await p4.confirm({
|
|
3286
|
-
message: `[${i + 1}/${total}] Is your
|
|
3435
|
+
message: `[${i + 1}/${total}] Is your app running locally with the ${entityName} factory wired up?`
|
|
3287
3436
|
});
|
|
3288
3437
|
if (p4.isCancel(ready)) throw new Error("Entity loop cancelled");
|
|
3289
3438
|
if (!ready) {
|
|
@@ -3299,7 +3448,7 @@ async function runEntityLoop(state, models, model, projectRoot, outputDir, nonIn
|
|
|
3299
3448
|
state.sharedSecret = secret;
|
|
3300
3449
|
await saveRecipeState(outputDir, state);
|
|
3301
3450
|
await writeFile7(
|
|
3302
|
-
|
|
3451
|
+
join19(outputDir, "autonoma-config.json"),
|
|
3303
3452
|
JSON.stringify({ sharedSecret: secret, endpointUrl: state.sdkEndpointUrl }, null, 2),
|
|
3304
3453
|
"utf-8"
|
|
3305
3454
|
);
|
|
@@ -3310,7 +3459,7 @@ Add this to your server's .env file and restart it.
|
|
|
3310
3459
|
This is a 64-character hex key used for HMAC-SHA256 request signing.
|
|
3311
3460
|
The same value must be set in both your server and the Autonoma dashboard.
|
|
3312
3461
|
|
|
3313
|
-
Saved to: ${
|
|
3462
|
+
Saved to: ${join19(outputDir, "autonoma-config.json")}`,
|
|
3314
3463
|
"Shared secret generated"
|
|
3315
3464
|
);
|
|
3316
3465
|
const secretReady = await p4.confirm({
|
|
@@ -3332,7 +3481,7 @@ Saved to: ${join16(outputDir, "autonoma-config.json")}`,
|
|
|
3332
3481
|
state.sdkEndpointUrl = url.trim() || "http://localhost:3000/api/autonoma";
|
|
3333
3482
|
await saveRecipeState(outputDir, state);
|
|
3334
3483
|
await writeFile7(
|
|
3335
|
-
|
|
3484
|
+
join19(outputDir, "autonoma-config.json"),
|
|
3336
3485
|
JSON.stringify({ sharedSecret: state.sharedSecret, endpointUrl: state.sdkEndpointUrl }, null, 2),
|
|
3337
3486
|
"utf-8"
|
|
3338
3487
|
);
|
|
@@ -3363,7 +3512,8 @@ Saved to: ${join16(outputDir, "autonoma-config.json")}`,
|
|
|
3363
3512
|
state.entities[entityName].recipeData,
|
|
3364
3513
|
testResult.feedback,
|
|
3365
3514
|
model,
|
|
3366
|
-
outputDir
|
|
3515
|
+
outputDir,
|
|
3516
|
+
state.entities
|
|
3367
3517
|
);
|
|
3368
3518
|
state.entities[entityName].recipeData = revised;
|
|
3369
3519
|
await saveRecipeState(outputDir, state);
|
|
@@ -3423,7 +3573,79 @@ When done, call finish with the instructions text.`;
|
|
|
3423
3573
|
|
|
3424
3574
|
// src/agents/04-recipe-builder/phases/full-validation.ts
|
|
3425
3575
|
import * as p5 from "@clack/prompts";
|
|
3426
|
-
|
|
3576
|
+
import { tool as tool15 } from "ai";
|
|
3577
|
+
import { z as z15 } from "zod";
|
|
3578
|
+
async function reviseFullRecipe(current, feedback, model, outputDir, entityOrder) {
|
|
3579
|
+
let revised;
|
|
3580
|
+
const finishTool = tool15({
|
|
3581
|
+
description: "Submit the revised full recipe: an object mapping each entity name to its array of records.",
|
|
3582
|
+
inputSchema: z15.object({
|
|
3583
|
+
recipe: z15.record(z15.string(), z15.array(z15.record(z15.string(), z15.unknown())))
|
|
3584
|
+
}),
|
|
3585
|
+
execute: async (input) => {
|
|
3586
|
+
revised = input.recipe;
|
|
3587
|
+
return { done: true };
|
|
3588
|
+
}
|
|
3589
|
+
});
|
|
3590
|
+
const { logger, onStepFinish } = buildDefaultStepLogger("revise:full-recipe", 20);
|
|
3591
|
+
await runAgent(
|
|
3592
|
+
{
|
|
3593
|
+
id: "revise-full-recipe",
|
|
3594
|
+
systemPrompt: `You are revising a full test-data recipe based on user feedback after they reviewed the app populated with this data.
|
|
3595
|
+
|
|
3596
|
+
The recipe is an object mapping entity names to arrays of records. Records use:
|
|
3597
|
+
- _alias: a unique id for a record so other records can point to it
|
|
3598
|
+
- _ref: { "_ref": "alias" } points to a parent record's _alias
|
|
3599
|
+
|
|
3600
|
+
Rules:
|
|
3601
|
+
- Apply the user's feedback across whatever entities it touches.
|
|
3602
|
+
- Keep _ref values pointing to aliases that actually exist in the recipe. Never invent a _ref to a missing alias.
|
|
3603
|
+
- Entities are created in this order (parents first): ${entityOrder.join(" \u2192 ")}. A record may only _ref an alias declared by an entity earlier in that order.
|
|
3604
|
+
- Field names/types must match the schema in entity-audit.md.
|
|
3605
|
+
- Read scenarios.md and entity-audit.md from the output directory as needed.
|
|
3606
|
+
|
|
3607
|
+
Return the COMPLETE revised recipe (all entities, not just the changed ones) via finish.`,
|
|
3608
|
+
model,
|
|
3609
|
+
maxSteps: 20,
|
|
3610
|
+
tools: (_heartbeat) => ({
|
|
3611
|
+
read_output: buildReadFileTool(outputDir),
|
|
3612
|
+
finish: finishTool
|
|
3613
|
+
}),
|
|
3614
|
+
onStepFinish
|
|
3615
|
+
},
|
|
3616
|
+
`The user reviewed the app with this test data and said it doesn't look right.
|
|
3617
|
+
|
|
3618
|
+
Current full recipe:
|
|
3619
|
+
${JSON.stringify(current, null, 2)}
|
|
3620
|
+
|
|
3621
|
+
User feedback:
|
|
3622
|
+
"${feedback}"
|
|
3623
|
+
|
|
3624
|
+
Revise the recipe to address the feedback, then call finish with the complete updated recipe.`,
|
|
3625
|
+
() => revised
|
|
3626
|
+
);
|
|
3627
|
+
logger.summary();
|
|
3628
|
+
return revised;
|
|
3629
|
+
}
|
|
3630
|
+
async function teardown(sdkConfig, refsToken, successMessage) {
|
|
3631
|
+
if (!refsToken) return true;
|
|
3632
|
+
p5.log.step("[Full validation] Tearing down all entities...");
|
|
3633
|
+
let downResult;
|
|
3634
|
+
try {
|
|
3635
|
+
downResult = await down(sdkConfig, refsToken);
|
|
3636
|
+
} catch (err) {
|
|
3637
|
+
p5.log.error(`Full DOWN request failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3638
|
+
return false;
|
|
3639
|
+
}
|
|
3640
|
+
if (!downResult.ok) {
|
|
3641
|
+
p5.log.error(`Full DOWN failed (HTTP ${downResult.status}):`);
|
|
3642
|
+
console.log(JSON.stringify(downResult.body, null, 2));
|
|
3643
|
+
return false;
|
|
3644
|
+
}
|
|
3645
|
+
p5.log.success(successMessage);
|
|
3646
|
+
return true;
|
|
3647
|
+
}
|
|
3648
|
+
async function runFullValidation(state, _models, outputDir, model) {
|
|
3427
3649
|
const total = state.entityOrder.length;
|
|
3428
3650
|
p5.log.info(
|
|
3429
3651
|
`All individual factories work. Now let's create EVERYTHING together and verify the app looks right with a full dataset. This is the recipe that will run before every test execution.`
|
|
@@ -3442,7 +3664,7 @@ async function runFullValidation(state, _models, outputDir) {
|
|
|
3442
3664
|
endpointUrl: state.sdkEndpointUrl,
|
|
3443
3665
|
sharedSecret: state.sharedSecret ?? ""
|
|
3444
3666
|
};
|
|
3445
|
-
|
|
3667
|
+
let fullRecipe = buildFullRecipe(state.entityOrder, state.entities);
|
|
3446
3668
|
while (true) {
|
|
3447
3669
|
const testRunId = `full-${Date.now()}`;
|
|
3448
3670
|
p5.log.step(`[Full validation] Creating all ${total} entities...`);
|
|
@@ -3500,26 +3722,35 @@ async function runFullValidation(state, _models, outputDir) {
|
|
|
3500
3722
|
message: "Does the app look right with the test data?"
|
|
3501
3723
|
});
|
|
3502
3724
|
if (p5.isCancel(looksGood)) throw new Error("Cancelled");
|
|
3503
|
-
|
|
3504
|
-
|
|
3725
|
+
const torndown = await teardown(
|
|
3726
|
+
sdkConfig,
|
|
3727
|
+
refsToken,
|
|
3728
|
+
looksGood ? "Full lifecycle works. All data was created and torn down cleanly." : "Tore down the test data so we can regenerate it."
|
|
3729
|
+
);
|
|
3730
|
+
if (!torndown) return false;
|
|
3731
|
+
if (looksGood) return true;
|
|
3732
|
+
const feedback = await p5.text({
|
|
3733
|
+
message: "What's wrong with the test data? Describe what to change.",
|
|
3734
|
+
placeholder: "e.g. accounts need realistic balances, transactions should reference the right account..."
|
|
3735
|
+
});
|
|
3736
|
+
if (p5.isCancel(feedback) || !feedback.trim()) {
|
|
3737
|
+
p5.log.info("No feedback given. You can edit recipe.json manually and re-run with --resume.");
|
|
3738
|
+
return false;
|
|
3505
3739
|
}
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
if (!downResult.ok) {
|
|
3516
|
-
p5.log.error(`Full DOWN failed (HTTP ${downResult.status}):`);
|
|
3517
|
-
console.log(JSON.stringify(downResult.body, null, 2));
|
|
3518
|
-
return false;
|
|
3740
|
+
p5.log.info("Revising the full recipe based on your feedback...");
|
|
3741
|
+
const revised = await reviseFullRecipe(fullRecipe, feedback.trim(), model, outputDir, state.entityOrder);
|
|
3742
|
+
if (!revised) {
|
|
3743
|
+
p5.log.warn("Couldn't revise automatically. Edit recipe.json manually and re-run with --resume.");
|
|
3744
|
+
return false;
|
|
3745
|
+
}
|
|
3746
|
+
for (const [name, records] of Object.entries(revised)) {
|
|
3747
|
+
if (state.entities[name]) {
|
|
3748
|
+
state.entities[name].recipeData = records;
|
|
3519
3749
|
}
|
|
3520
|
-
p5.log.success("Full lifecycle works. All data was created and torn down cleanly.");
|
|
3521
3750
|
}
|
|
3522
|
-
|
|
3751
|
+
await saveRecipeState(outputDir, state);
|
|
3752
|
+
fullRecipe = buildFullRecipe(state.entityOrder, state.entities);
|
|
3753
|
+
p5.note(JSON.stringify(fullRecipe, null, 2), "Revised recipe \u2014 re-running full validation", { format: codeNoteFormat });
|
|
3523
3754
|
}
|
|
3524
3755
|
}
|
|
3525
3756
|
var init_full_validation = __esm({
|
|
@@ -3527,6 +3758,9 @@ var init_full_validation = __esm({
|
|
|
3527
3758
|
"use strict";
|
|
3528
3759
|
init_esm_shims();
|
|
3529
3760
|
init_notify();
|
|
3761
|
+
init_agent();
|
|
3762
|
+
init_tools();
|
|
3763
|
+
init_highlight();
|
|
3530
3764
|
init_state();
|
|
3531
3765
|
init_recipe();
|
|
3532
3766
|
init_http_client();
|
|
@@ -3632,7 +3866,7 @@ async function runRecipeBuilder(input) {
|
|
|
3632
3866
|
}
|
|
3633
3867
|
}
|
|
3634
3868
|
if (state.phase === "full-validation") {
|
|
3635
|
-
const success = await runFullValidation(state, models, input.outputDir);
|
|
3869
|
+
const success = await runFullValidation(state, models, input.outputDir, model);
|
|
3636
3870
|
if (success) {
|
|
3637
3871
|
state.phase = "submit";
|
|
3638
3872
|
await saveRecipeState(input.outputDir, state);
|
|
@@ -3682,22 +3916,22 @@ var init_recipe_builder = __esm({
|
|
|
3682
3916
|
});
|
|
3683
3917
|
|
|
3684
3918
|
// src/agents/05-test-generator/rubrics.ts
|
|
3685
|
-
import { z as
|
|
3919
|
+
import { z as z16 } from "zod";
|
|
3686
3920
|
var dimensionResultSchema, structuralIntentRubric, flowCompletenessRubric, uiTextRubric, dataAccuracyRubric, ALL_RUBRICS;
|
|
3687
3921
|
var init_rubrics = __esm({
|
|
3688
3922
|
"src/agents/05-test-generator/rubrics.ts"() {
|
|
3689
3923
|
"use strict";
|
|
3690
3924
|
init_esm_shims();
|
|
3691
|
-
dimensionResultSchema =
|
|
3692
|
-
pass:
|
|
3693
|
-
evidence:
|
|
3694
|
-
suggestion:
|
|
3925
|
+
dimensionResultSchema = z16.object({
|
|
3926
|
+
pass: z16.boolean(),
|
|
3927
|
+
evidence: z16.string().describe("What you checked and found \u2014 cite file paths, line content, or specific strings"),
|
|
3928
|
+
suggestion: z16.string().optional().describe("What the planner agent should fix, if failing")
|
|
3695
3929
|
});
|
|
3696
3930
|
structuralIntentRubric = {
|
|
3697
3931
|
name: "structural-intent",
|
|
3698
3932
|
maxSteps: 8,
|
|
3699
3933
|
dimensions: ["structuralValidity", "intentQuality", "missionAlignment"],
|
|
3700
|
-
resultSchema:
|
|
3934
|
+
resultSchema: z16.object({
|
|
3701
3935
|
structuralValidity: dimensionResultSchema.describe(
|
|
3702
3936
|
"Are all step verbs valid (click/type/scroll/assert/hover/drag/read/refresh)? Are asserts visual-only (no URLs, network, console)? No code selectors? No login steps?"
|
|
3703
3937
|
),
|
|
@@ -3738,7 +3972,7 @@ When done reviewing, call finish with your structured evaluation.`
|
|
|
3738
3972
|
name: "flow-completeness",
|
|
3739
3973
|
maxSteps: 12,
|
|
3740
3974
|
dimensions: ["actionCompletion", "mutationVerification"],
|
|
3741
|
-
resultSchema:
|
|
3975
|
+
resultSchema: z16.object({
|
|
3742
3976
|
actionCompletion: dimensionResultSchema.describe(
|
|
3743
3977
|
"Does the test complete a core action and reach an OUTCOME? Not just opening a modal or clicking a tab."
|
|
3744
3978
|
),
|
|
@@ -3774,7 +4008,7 @@ When done reviewing, call finish with your structured evaluation.`
|
|
|
3774
4008
|
name: "ui-text",
|
|
3775
4009
|
maxSteps: 20,
|
|
3776
4010
|
dimensions: ["uiTextAuthenticity"],
|
|
3777
|
-
resultSchema:
|
|
4011
|
+
resultSchema: z16.object({
|
|
3778
4012
|
uiTextAuthenticity: dimensionResultSchema.describe(
|
|
3779
4013
|
"Do all quoted strings in steps reference text a human would actually see on screen? Not translation keys, config paths, component names, enum identifiers, or CSS classes."
|
|
3780
4014
|
)
|
|
@@ -3813,7 +4047,7 @@ When done reviewing, call finish with your structured evaluation.`
|
|
|
3813
4047
|
name: "data-accuracy",
|
|
3814
4048
|
maxSteps: 20,
|
|
3815
4049
|
dimensions: ["dataAccuracy"],
|
|
3816
|
-
resultSchema:
|
|
4050
|
+
resultSchema: z16.object({
|
|
3817
4051
|
dataAccuracy: dimensionResultSchema.describe(
|
|
3818
4052
|
"Do the referenced UI elements (buttons, labels, fields, headings, toasts) actually exist in the source code for this page? Are default states correct? Does all test data (names, values, entities) come from the scenario data \u2014 NOT from other tests?"
|
|
3819
4053
|
)
|
|
@@ -3866,12 +4100,12 @@ When done reviewing, call finish with your structured evaluation.`
|
|
|
3866
4100
|
// src/agents/05-test-generator/review-pass.ts
|
|
3867
4101
|
import { basename } from "path";
|
|
3868
4102
|
import "ai";
|
|
3869
|
-
import { tool as
|
|
4103
|
+
import { tool as tool16 } from "ai";
|
|
3870
4104
|
async function runReviewPass(testContent, testPath, rubric, projectRoot, model, scenarioData) {
|
|
3871
4105
|
let result;
|
|
3872
4106
|
const agentLabel = `review:${rubric.name}:${basename(testPath)}`;
|
|
3873
4107
|
const { onStepFinish } = buildDefaultStepLogger(agentLabel, rubric.maxSteps);
|
|
3874
|
-
const finishTool =
|
|
4108
|
+
const finishTool = tool16({
|
|
3875
4109
|
description: "Submit your structured review. Every dimension must have evidence from your investigation.",
|
|
3876
4110
|
inputSchema: rubric.resultSchema,
|
|
3877
4111
|
execute: async (input) => {
|
|
@@ -3928,8 +4162,8 @@ var init_review_pass = __esm({
|
|
|
3928
4162
|
});
|
|
3929
4163
|
|
|
3930
4164
|
// src/agents/05-test-generator/review.ts
|
|
3931
|
-
import { readFile as
|
|
3932
|
-
import { join as
|
|
4165
|
+
import { readFile as readFile14 } from "fs/promises";
|
|
4166
|
+
import { join as join20, relative as relative5, basename as basename2 } from "path";
|
|
3933
4167
|
import { glob as glob4 } from "glob";
|
|
3934
4168
|
import "ai";
|
|
3935
4169
|
async function reviewSingleTest(testContent, testPath, projectRoot, model, scenarioData) {
|
|
@@ -3956,19 +4190,19 @@ async function reviewSingleTest(testContent, testPath, projectRoot, model, scena
|
|
|
3956
4190
|
return merged;
|
|
3957
4191
|
}
|
|
3958
4192
|
async function runConsolidatedReview(outputDir, projectRoot, model) {
|
|
3959
|
-
const testsDir =
|
|
4193
|
+
const testsDir = join20(outputDir, "qa-tests");
|
|
3960
4194
|
const logger = createStepLogger("review", 5);
|
|
3961
4195
|
let scenarioData;
|
|
3962
4196
|
try {
|
|
3963
|
-
scenarioData = await
|
|
4197
|
+
scenarioData = await readFile14(join20(outputDir, "scenarios.md"), "utf-8");
|
|
3964
4198
|
} catch {
|
|
3965
4199
|
}
|
|
3966
|
-
const testFiles = await glob4(
|
|
4200
|
+
const testFiles = await glob4(join20(testsDir, "**/*.md"));
|
|
3967
4201
|
const tests = [];
|
|
3968
4202
|
for (const testPath of testFiles) {
|
|
3969
4203
|
if (basename2(testPath) === "INDEX.md") continue;
|
|
3970
4204
|
if (testPath.includes("/_invalid/")) continue;
|
|
3971
|
-
const content = await
|
|
4205
|
+
const content = await readFile14(testPath, "utf-8");
|
|
3972
4206
|
const flowMatch = content.match(/^---\n[\s\S]*?flow:\s*["']?([^"'\n]+)["']?\s*\n[\s\S]*?---/m);
|
|
3973
4207
|
tests.push({
|
|
3974
4208
|
path: testPath,
|
|
@@ -4045,16 +4279,16 @@ var init_review2 = __esm({
|
|
|
4045
4279
|
});
|
|
4046
4280
|
|
|
4047
4281
|
// src/agents/05-test-generator/graph.ts
|
|
4048
|
-
import { readFile as
|
|
4049
|
-
import { join as
|
|
4282
|
+
import { readFile as readFile15, writeFile as writeFile8 } from "fs/promises";
|
|
4283
|
+
import { join as join21 } from "path";
|
|
4050
4284
|
async function saveBfsState(outputDir, state) {
|
|
4051
|
-
const path3 =
|
|
4285
|
+
const path3 = join21(outputDir, STATE_FILE3);
|
|
4052
4286
|
await writeFile8(path3, JSON.stringify(state.serialize(), null, 2), "utf-8");
|
|
4053
4287
|
}
|
|
4054
4288
|
async function loadBfsState(outputDir) {
|
|
4055
|
-
const path3 =
|
|
4289
|
+
const path3 = join21(outputDir, STATE_FILE3);
|
|
4056
4290
|
try {
|
|
4057
|
-
const raw = await
|
|
4291
|
+
const raw = await readFile15(path3, "utf-8");
|
|
4058
4292
|
return CoverageState.deserialize(JSON.parse(raw));
|
|
4059
4293
|
} catch {
|
|
4060
4294
|
return null;
|
|
@@ -4146,17 +4380,17 @@ var init_graph = __esm({
|
|
|
4146
4380
|
});
|
|
4147
4381
|
|
|
4148
4382
|
// src/agents/00b-feature-discovery/index.ts
|
|
4149
|
-
import { readFile as
|
|
4150
|
-
import { join as
|
|
4151
|
-
import { z as
|
|
4152
|
-
import { tool as
|
|
4383
|
+
import { readFile as readFile16, writeFile as writeFile9 } from "fs/promises";
|
|
4384
|
+
import { join as join22 } from "path";
|
|
4385
|
+
import { z as z17 } from "zod";
|
|
4386
|
+
import { tool as tool17 } from "ai";
|
|
4153
4387
|
async function saveFeatures(outputDir, features) {
|
|
4154
4388
|
const obj = Object.fromEntries(features);
|
|
4155
|
-
await writeFile9(
|
|
4389
|
+
await writeFile9(join22(outputDir, FEATURES_FILE), JSON.stringify(obj, null, 2), "utf-8");
|
|
4156
4390
|
}
|
|
4157
4391
|
async function loadFeatures(outputDir) {
|
|
4158
4392
|
try {
|
|
4159
|
-
const raw = await
|
|
4393
|
+
const raw = await readFile16(join22(outputDir, FEATURES_FILE), "utf-8");
|
|
4160
4394
|
const obj = JSON.parse(raw);
|
|
4161
4395
|
return new Map(Object.entries(obj));
|
|
4162
4396
|
} catch {
|
|
@@ -4187,10 +4421,10 @@ Process every page. Call add_feature for each sub-feature you discover. When don
|
|
|
4187
4421
|
const tools = await buildCodebaseTools(model, input.projectRoot, input.outputDir, heartbeat);
|
|
4188
4422
|
return {
|
|
4189
4423
|
...tools,
|
|
4190
|
-
add_feature:
|
|
4424
|
+
add_feature: tool17({
|
|
4191
4425
|
description: "Add a discovered sub-feature",
|
|
4192
4426
|
inputSchema: Feature.extend({
|
|
4193
|
-
id:
|
|
4427
|
+
id: z17.string().min(1).describe("Unique kebab-case ID (e.g. 'settings-notifications-tab')")
|
|
4194
4428
|
}),
|
|
4195
4429
|
execute: (featureInput) => {
|
|
4196
4430
|
const { id, ...rest } = featureInput;
|
|
@@ -4202,19 +4436,19 @@ Process every page. Call add_feature for each sub-feature you discover. When don
|
|
|
4202
4436
|
return `Feature "${id}" added (${collector.features.size} total)`;
|
|
4203
4437
|
}
|
|
4204
4438
|
}),
|
|
4205
|
-
view_features:
|
|
4439
|
+
view_features: tool17({
|
|
4206
4440
|
description: "View all discovered features so far",
|
|
4207
|
-
inputSchema:
|
|
4441
|
+
inputSchema: z17.object({}),
|
|
4208
4442
|
execute: () => collector.viewFeatures()
|
|
4209
4443
|
}),
|
|
4210
|
-
view_pages:
|
|
4444
|
+
view_pages: tool17({
|
|
4211
4445
|
description: "View the pages list to know what to analyze",
|
|
4212
|
-
inputSchema:
|
|
4446
|
+
inputSchema: z17.object({}),
|
|
4213
4447
|
execute: () => pagesDescription
|
|
4214
4448
|
}),
|
|
4215
|
-
finish:
|
|
4449
|
+
finish: tool17({
|
|
4216
4450
|
description: "Signal that feature discovery is complete",
|
|
4217
|
-
inputSchema:
|
|
4451
|
+
inputSchema: z17.object({ summary: z17.string() }),
|
|
4218
4452
|
execute: async (finishInput) => {
|
|
4219
4453
|
result = {
|
|
4220
4454
|
success: true,
|
|
@@ -4245,13 +4479,13 @@ var init_b_feature_discovery = __esm({
|
|
|
4245
4479
|
init_model();
|
|
4246
4480
|
init_tools();
|
|
4247
4481
|
FEATURES_FILE = "features.json";
|
|
4248
|
-
Feature =
|
|
4249
|
-
name:
|
|
4250
|
-
type:
|
|
4251
|
-
parentPagePath:
|
|
4252
|
-
sourceFiles:
|
|
4253
|
-
interactiveElements:
|
|
4254
|
-
description:
|
|
4482
|
+
Feature = z17.object({
|
|
4483
|
+
name: z17.string().min(1).describe("Human-readable name (e.g. 'Settings > Notifications Tab', 'Create Project Modal')"),
|
|
4484
|
+
type: z17.enum(["tab", "modal", "form", "table", "wizard", "nested-route", "complex-component"]),
|
|
4485
|
+
parentPagePath: z17.string().min(1).describe("The page path this feature belongs to (from the pages list)"),
|
|
4486
|
+
sourceFiles: z17.array(z17.string()).min(1).describe("Relative paths to the source files for this sub-feature"),
|
|
4487
|
+
interactiveElements: z17.number().int().min(0).describe("Count of interactive elements found (buttons, inputs, toggles, etc.)"),
|
|
4488
|
+
description: z17.string().min(10).describe("What this sub-feature does")
|
|
4255
4489
|
});
|
|
4256
4490
|
FeatureCollector = class {
|
|
4257
4491
|
features = /* @__PURE__ */ new Map();
|
|
@@ -4332,14 +4566,14 @@ Use kebab-case IDs that indicate the parent page and feature type:
|
|
|
4332
4566
|
});
|
|
4333
4567
|
|
|
4334
4568
|
// src/agents/05-test-generator/validation.ts
|
|
4335
|
-
import
|
|
4569
|
+
import matter2 from "gray-matter";
|
|
4336
4570
|
function validateTestContent(content) {
|
|
4337
4571
|
const errors = [];
|
|
4338
4572
|
if (!/^---\n[\s\S]*?\n---/.test(content)) {
|
|
4339
4573
|
errors.push("Missing frontmatter");
|
|
4340
4574
|
} else {
|
|
4341
4575
|
try {
|
|
4342
|
-
const { data } =
|
|
4576
|
+
const { data } = matter2(content);
|
|
4343
4577
|
if (!data.verification || typeof data.verification !== "string" || data.verification.length < 20) {
|
|
4344
4578
|
errors.push("Missing or insufficient 'verification' field in frontmatter \u2014 must describe WHERE to navigate and WHAT to assert at the source of truth");
|
|
4345
4579
|
}
|
|
@@ -4394,18 +4628,18 @@ var init_validation = __esm({
|
|
|
4394
4628
|
|
|
4395
4629
|
// src/agents/05-test-generator/tools.ts
|
|
4396
4630
|
import { mkdir as mkdir3, writeFile as writeFile10 } from "fs/promises";
|
|
4397
|
-
import { dirname as dirname2, join as
|
|
4398
|
-
import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as
|
|
4399
|
-
import
|
|
4400
|
-
import { z as
|
|
4631
|
+
import { dirname as dirname2, join as join23 } from "path";
|
|
4632
|
+
import { hasToolCall as hasToolCall3, stepCountIs as stepCountIs3, tool as tool18, ToolLoopAgent as ToolLoopAgent3 } from "ai";
|
|
4633
|
+
import matter3 from "gray-matter";
|
|
4634
|
+
import { z as z18 } from "zod";
|
|
4401
4635
|
function buildWriteTestTool(state, outputDir) {
|
|
4402
|
-
return
|
|
4636
|
+
return tool18({
|
|
4403
4637
|
description: "Write a test file to qa-tests/{folder}/{filename}.md. Validates frontmatter before writing. Returns error if frontmatter is invalid.",
|
|
4404
|
-
inputSchema:
|
|
4405
|
-
folder:
|
|
4406
|
-
filename:
|
|
4407
|
-
content:
|
|
4408
|
-
nodeId:
|
|
4638
|
+
inputSchema: z18.object({
|
|
4639
|
+
folder: z18.string().describe("Subfolder name under qa-tests/"),
|
|
4640
|
+
filename: z18.string().describe("File name (e.g. login-valid-credentials.md)"),
|
|
4641
|
+
content: z18.string().describe("Full file content including YAML frontmatter"),
|
|
4642
|
+
nodeId: z18.string().describe("The FeatureNode ID this test belongs to")
|
|
4409
4643
|
}),
|
|
4410
4644
|
execute: async (input) => {
|
|
4411
4645
|
const frontmatter = extractFrontmatter(input.content);
|
|
@@ -4454,8 +4688,8 @@ function buildWriteTestTool(state, outputDir) {
|
|
|
4454
4688
|
};
|
|
4455
4689
|
}
|
|
4456
4690
|
}
|
|
4457
|
-
const relPath =
|
|
4458
|
-
const absPath =
|
|
4691
|
+
const relPath = join23("qa-tests", input.folder, input.filename);
|
|
4692
|
+
const absPath = join23(outputDir, relPath);
|
|
4459
4693
|
try {
|
|
4460
4694
|
await mkdir3(dirname2(absPath), { recursive: true });
|
|
4461
4695
|
await writeFile10(absPath, input.content, "utf-8");
|
|
@@ -4470,16 +4704,16 @@ function buildWriteTestTool(state, outputDir) {
|
|
|
4470
4704
|
});
|
|
4471
4705
|
}
|
|
4472
4706
|
function buildCreateFolderTool(outputDir) {
|
|
4473
|
-
return
|
|
4707
|
+
return tool18({
|
|
4474
4708
|
description: "Create a folder under qa-tests/ for organizing tests.",
|
|
4475
|
-
inputSchema:
|
|
4476
|
-
folder:
|
|
4709
|
+
inputSchema: z18.object({
|
|
4710
|
+
folder: z18.string().describe("Folder name (kebab-case)")
|
|
4477
4711
|
}),
|
|
4478
4712
|
execute: async (input) => {
|
|
4479
|
-
const absPath =
|
|
4713
|
+
const absPath = join23(outputDir, "qa-tests", input.folder);
|
|
4480
4714
|
try {
|
|
4481
4715
|
await mkdir3(absPath, { recursive: true });
|
|
4482
|
-
return { path:
|
|
4716
|
+
return { path: join23("qa-tests", input.folder) };
|
|
4483
4717
|
} catch (err) {
|
|
4484
4718
|
const message = err instanceof Error ? err.message : String(err);
|
|
4485
4719
|
return { error: `Failed to create folder: ${message}` };
|
|
@@ -4488,9 +4722,9 @@ function buildCreateFolderTool(outputDir) {
|
|
|
4488
4722
|
});
|
|
4489
4723
|
}
|
|
4490
4724
|
function buildNextNodeTool(state, outputDir) {
|
|
4491
|
-
return
|
|
4725
|
+
return tool18({
|
|
4492
4726
|
description: "Get the next node to write tests for. If you called next_node before without writing any tests (via write_test), the previous node is auto-skipped. Returns done:true when all nodes are processed.",
|
|
4493
|
-
inputSchema:
|
|
4727
|
+
inputSchema: z18.object({}),
|
|
4494
4728
|
execute: async () => {
|
|
4495
4729
|
const next = state.nextNode();
|
|
4496
4730
|
await saveBfsState(outputDir, state);
|
|
@@ -4517,9 +4751,9 @@ function buildNextNodeTool(state, outputDir) {
|
|
|
4517
4751
|
});
|
|
4518
4752
|
}
|
|
4519
4753
|
function buildGetProgressTool(state) {
|
|
4520
|
-
return
|
|
4754
|
+
return tool18({
|
|
4521
4755
|
description: "Check how many nodes have been tested vs how many remain.",
|
|
4522
|
-
inputSchema:
|
|
4756
|
+
inputSchema: z18.object({}),
|
|
4523
4757
|
execute: async () => {
|
|
4524
4758
|
const stats = state.summary();
|
|
4525
4759
|
const nodes = [...state.nodes.values()].map((n) => ({
|
|
@@ -4533,14 +4767,14 @@ function buildGetProgressTool(state) {
|
|
|
4533
4767
|
});
|
|
4534
4768
|
}
|
|
4535
4769
|
function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
|
|
4536
|
-
return
|
|
4770
|
+
return tool18({
|
|
4537
4771
|
description: "Spawn a research subagent to read and analyze source files without polluting your context. Use for complex sub-features where you don't want to read 20 files yourself.",
|
|
4538
|
-
inputSchema:
|
|
4539
|
-
instruction:
|
|
4772
|
+
inputSchema: z18.object({
|
|
4773
|
+
instruction: z18.string().describe("What to research \u2014 be specific about files and what to look for")
|
|
4540
4774
|
}),
|
|
4541
4775
|
execute: async (input) => {
|
|
4542
|
-
const resultSchema2 =
|
|
4543
|
-
findings:
|
|
4776
|
+
const resultSchema2 = z18.object({
|
|
4777
|
+
findings: z18.string().describe("Summary of what was found")
|
|
4544
4778
|
});
|
|
4545
4779
|
let result;
|
|
4546
4780
|
const subagent = new ToolLoopAgent3({
|
|
@@ -4551,7 +4785,7 @@ function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
|
|
|
4551
4785
|
glob: buildGlobTool(workingDirectory),
|
|
4552
4786
|
grep: buildGrepTool(workingDirectory),
|
|
4553
4787
|
read_file: buildReadFileTool(workingDirectory),
|
|
4554
|
-
finish:
|
|
4788
|
+
finish: tool18({
|
|
4555
4789
|
description: "Report your findings.",
|
|
4556
4790
|
inputSchema: resultSchema2,
|
|
4557
4791
|
execute: async (output) => {
|
|
@@ -4578,7 +4812,7 @@ function buildSpawnResearcherTool(model, workingDirectory, onHeartbeat) {
|
|
|
4578
4812
|
}
|
|
4579
4813
|
function extractFrontmatter(content) {
|
|
4580
4814
|
try {
|
|
4581
|
-
const { data } =
|
|
4815
|
+
const { data } = matter3(content);
|
|
4582
4816
|
return data && Object.keys(data).length > 0 ? data : null;
|
|
4583
4817
|
} catch {
|
|
4584
4818
|
return null;
|
|
@@ -4592,14 +4826,14 @@ var init_tools2 = __esm({
|
|
|
4592
4826
|
init_tools();
|
|
4593
4827
|
init_graph();
|
|
4594
4828
|
init_validation();
|
|
4595
|
-
testFrontmatterSchema =
|
|
4596
|
-
title:
|
|
4597
|
-
description:
|
|
4598
|
-
intent:
|
|
4599
|
-
criticality:
|
|
4600
|
-
scenario:
|
|
4601
|
-
flow:
|
|
4602
|
-
verification:
|
|
4829
|
+
testFrontmatterSchema = z18.object({
|
|
4830
|
+
title: z18.string().min(1),
|
|
4831
|
+
description: z18.string().min(1),
|
|
4832
|
+
intent: z18.string().min(30, "Intent must be at least 30 characters \u2014 describe the BEHAVIOR being tested, not the steps"),
|
|
4833
|
+
criticality: z18.enum(["critical", "high", "mid", "low"]),
|
|
4834
|
+
scenario: z18.string().min(1),
|
|
4835
|
+
flow: z18.string().min(1),
|
|
4836
|
+
verification: z18.string().min(20, "Verification must describe WHERE to navigate and WHAT to assert at the source of truth \u2014 not UI acknowledgments like toasts")
|
|
4603
4837
|
});
|
|
4604
4838
|
}
|
|
4605
4839
|
});
|
|
@@ -4995,10 +5229,10 @@ var test_generator_exports = {};
|
|
|
4995
5229
|
__export(test_generator_exports, {
|
|
4996
5230
|
runTestGenerator: () => runTestGenerator
|
|
4997
5231
|
});
|
|
4998
|
-
import { mkdir as mkdir4, readFile as
|
|
4999
|
-
import { basename as basename3, join as
|
|
5000
|
-
import { tool as
|
|
5001
|
-
import { z as
|
|
5232
|
+
import { mkdir as mkdir4, readFile as readFile17, rmdir, unlink, writeFile as writeFile11 } from "fs/promises";
|
|
5233
|
+
import { basename as basename3, join as join24 } from "path";
|
|
5234
|
+
import { tool as tool19 } from "ai";
|
|
5235
|
+
import { z as z19 } from "zod";
|
|
5002
5236
|
import { glob as glob5 } from "glob";
|
|
5003
5237
|
async function preseedQueue(state, projectRoot, pages, features) {
|
|
5004
5238
|
let seeded = 0;
|
|
@@ -5046,10 +5280,10 @@ async function runTestGenerator(input) {
|
|
|
5046
5280
|
const existingState = await loadBfsState(input.outputDir);
|
|
5047
5281
|
const state = existingState ?? new CoverageState();
|
|
5048
5282
|
let result;
|
|
5049
|
-
const finishTool =
|
|
5283
|
+
const finishTool = tool19({
|
|
5050
5284
|
description: "Call when the BFS queue is empty and all routes have been explored.",
|
|
5051
|
-
inputSchema:
|
|
5052
|
-
summary:
|
|
5285
|
+
inputSchema: z19.object({
|
|
5286
|
+
summary: z19.string().describe("Coverage summary")
|
|
5053
5287
|
}),
|
|
5054
5288
|
execute: async (finishInput) => {
|
|
5055
5289
|
const stats = state.summary();
|
|
@@ -5078,8 +5312,8 @@ async function runTestGenerator(input) {
|
|
|
5078
5312
|
});
|
|
5079
5313
|
let kbContext = "";
|
|
5080
5314
|
try {
|
|
5081
|
-
const autonomaMd = await
|
|
5082
|
-
|
|
5315
|
+
const autonomaMd = await readFile17(
|
|
5316
|
+
join24(input.outputDir, "AUTONOMA.md"),
|
|
5083
5317
|
"utf-8"
|
|
5084
5318
|
);
|
|
5085
5319
|
kbContext += `
|
|
@@ -5090,8 +5324,8 @@ ${autonomaMd}
|
|
|
5090
5324
|
} catch {
|
|
5091
5325
|
}
|
|
5092
5326
|
try {
|
|
5093
|
-
const scenariosMd = await
|
|
5094
|
-
|
|
5327
|
+
const scenariosMd = await readFile17(
|
|
5328
|
+
join24(input.outputDir, "scenarios.md"),
|
|
5095
5329
|
"utf-8"
|
|
5096
5330
|
);
|
|
5097
5331
|
kbContext += `
|
|
@@ -5287,18 +5521,18 @@ IMPORTANT: Do NOT try to finish early. Process every node via next_node until it
|
|
|
5287
5521
|
console.log(` Fix pass complete`);
|
|
5288
5522
|
}
|
|
5289
5523
|
const allTestFiles = await glob5(
|
|
5290
|
-
|
|
5524
|
+
join24(input.outputDir, "qa-tests", "**/*.md")
|
|
5291
5525
|
);
|
|
5292
5526
|
let markedInvalid = 0;
|
|
5293
5527
|
for (const testPath of allTestFiles) {
|
|
5294
5528
|
if (basename3(testPath) === "INDEX.md") continue;
|
|
5295
5529
|
if (testPath.includes("/_invalid/")) continue;
|
|
5296
|
-
const content = await
|
|
5530
|
+
const content = await readFile17(testPath, "utf-8");
|
|
5297
5531
|
const validation = validateTestContent(content);
|
|
5298
5532
|
if (!validation.valid) {
|
|
5299
|
-
const invalidDir =
|
|
5533
|
+
const invalidDir = join24(input.outputDir, "qa-tests", "_invalid");
|
|
5300
5534
|
await mkdir4(invalidDir, { recursive: true });
|
|
5301
|
-
const dest =
|
|
5535
|
+
const dest = join24(invalidDir, basename3(testPath));
|
|
5302
5536
|
const annotated = `<!-- VALIDATION ERRORS: ${validation.errors.join("; ")} -->
|
|
5303
5537
|
${content}`;
|
|
5304
5538
|
await writeFile11(dest, annotated, "utf-8");
|
|
@@ -5311,7 +5545,7 @@ ${content}`;
|
|
|
5311
5545
|
` ${markedInvalid} tests still invalid after review cycles \u2014 moved to _invalid/`
|
|
5312
5546
|
);
|
|
5313
5547
|
}
|
|
5314
|
-
const dirs = await glob5(
|
|
5548
|
+
const dirs = await glob5(join24(input.outputDir, "qa-tests", "**/"), {
|
|
5315
5549
|
dot: false
|
|
5316
5550
|
});
|
|
5317
5551
|
for (const dir of dirs.sort((a, b) => b.length - a.length)) {
|
|
@@ -5403,7 +5637,7 @@ async function generateIndex(outputDir, state) {
|
|
|
5403
5637
|
for (const paths of state.testsWritten.values()) {
|
|
5404
5638
|
for (const p9 of paths) {
|
|
5405
5639
|
try {
|
|
5406
|
-
const content2 = await
|
|
5640
|
+
const content2 = await readFile17(join24(outputDir, p9), "utf-8");
|
|
5407
5641
|
const critMatch = content2.match(/criticality:\s*(\w+)/);
|
|
5408
5642
|
const critVal = critMatch?.[1] ?? "";
|
|
5409
5643
|
if (critCounts.has(critVal))
|
|
@@ -5454,26 +5688,26 @@ ${folders.map((f) => `| ${f.name} | ${f.test_count} |`).join("\n")}
|
|
|
5454
5688
|
|
|
5455
5689
|
${[...testsByFolder.entries()].flatMap(([_folder, tests]) => tests.map((t) => `- \`${t}\``)).join("\n")}
|
|
5456
5690
|
`;
|
|
5457
|
-
await writeFile11(
|
|
5691
|
+
await writeFile11(join24(outputDir, "qa-tests", "INDEX.md"), content, "utf-8");
|
|
5458
5692
|
}
|
|
5459
5693
|
async function generateJourneyTests(outputDir, model, projectRoot) {
|
|
5460
5694
|
const logger = createStepLogger("journeys", 50);
|
|
5461
5695
|
let autonomaMd = "";
|
|
5462
5696
|
let scenariosMd = "";
|
|
5463
5697
|
try {
|
|
5464
|
-
autonomaMd = await
|
|
5698
|
+
autonomaMd = await readFile17(join24(outputDir, "AUTONOMA.md"), "utf-8");
|
|
5465
5699
|
} catch {
|
|
5466
5700
|
}
|
|
5467
5701
|
try {
|
|
5468
|
-
scenariosMd = await
|
|
5702
|
+
scenariosMd = await readFile17(join24(outputDir, "scenarios.md"), "utf-8");
|
|
5469
5703
|
} catch {
|
|
5470
5704
|
}
|
|
5471
5705
|
if (!autonomaMd) return 0;
|
|
5472
|
-
const existingTests = await glob5(
|
|
5706
|
+
const existingTests = await glob5(join24(outputDir, "qa-tests", "**/*.md"));
|
|
5473
5707
|
const existingTitles = [];
|
|
5474
5708
|
for (const t of existingTests) {
|
|
5475
5709
|
if (basename3(t) === "INDEX.md") continue;
|
|
5476
|
-
const content = await
|
|
5710
|
+
const content = await readFile17(t, "utf-8");
|
|
5477
5711
|
const titleMatch = content.match(/title:\s*"([^"]+)"/);
|
|
5478
5712
|
if (titleMatch) existingTitles.push(titleMatch[1]);
|
|
5479
5713
|
}
|
|
@@ -5516,9 +5750,9 @@ Write 5-8 journey tests using the write_test tool with folder "journeys". Then c
|
|
|
5516
5750
|
status: "queued"
|
|
5517
5751
|
});
|
|
5518
5752
|
let journeyResult;
|
|
5519
|
-
const journeyFinish =
|
|
5753
|
+
const journeyFinish = tool19({
|
|
5520
5754
|
description: "Signal journey generation is complete.",
|
|
5521
|
-
inputSchema:
|
|
5755
|
+
inputSchema: z19.object({ summary: z19.string() }),
|
|
5522
5756
|
execute: async (finishInput) => {
|
|
5523
5757
|
journeyResult = {
|
|
5524
5758
|
success: true,
|
|
@@ -5578,17 +5812,90 @@ var init_test_generator = __esm({
|
|
|
5578
5812
|
// src/index.ts
|
|
5579
5813
|
init_esm_shims();
|
|
5580
5814
|
import * as p8 from "@clack/prompts";
|
|
5581
|
-
import { readFile as
|
|
5582
|
-
import { join as
|
|
5815
|
+
import { readFile as readFile18, writeFile as writeFile12 } from "fs/promises";
|
|
5816
|
+
import { join as join25 } from "path";
|
|
5583
5817
|
|
|
5584
5818
|
// src/config.ts
|
|
5585
5819
|
init_esm_shims();
|
|
5586
|
-
import { resolve, join } from "path";
|
|
5587
|
-
import { readFileSync } from "fs";
|
|
5820
|
+
import { resolve, join as join2 } from "path";
|
|
5821
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5822
|
+
|
|
5823
|
+
// src/core/global-env.ts
|
|
5824
|
+
init_esm_shims();
|
|
5825
|
+
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
5826
|
+
import { join } from "path";
|
|
5827
|
+
import { homedir } from "os";
|
|
5828
|
+
var AUTONOMA_HOME = join(homedir(), ".autonoma");
|
|
5829
|
+
var GLOBAL_ENV_PATH = join(AUTONOMA_HOME, ".env");
|
|
5830
|
+
function getGlobalEnvPath() {
|
|
5831
|
+
return GLOBAL_ENV_PATH;
|
|
5832
|
+
}
|
|
5833
|
+
function parseEnvContent(content) {
|
|
5834
|
+
const out = {};
|
|
5835
|
+
for (const line of content.split("\n")) {
|
|
5836
|
+
const trimmed = line.trim();
|
|
5837
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
5838
|
+
const eqIdx = trimmed.indexOf("=");
|
|
5839
|
+
if (eqIdx === -1) continue;
|
|
5840
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
5841
|
+
let value = trimmed.slice(eqIdx + 1).trim();
|
|
5842
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
5843
|
+
value = value.slice(1, -1);
|
|
5844
|
+
}
|
|
5845
|
+
out[key] = value;
|
|
5846
|
+
}
|
|
5847
|
+
return out;
|
|
5848
|
+
}
|
|
5849
|
+
function loadGlobalEnv() {
|
|
5850
|
+
let content;
|
|
5851
|
+
try {
|
|
5852
|
+
content = readFileSync(GLOBAL_ENV_PATH, "utf-8");
|
|
5853
|
+
} catch {
|
|
5854
|
+
return;
|
|
5855
|
+
}
|
|
5856
|
+
for (const [key, value] of Object.entries(parseEnvContent(content))) {
|
|
5857
|
+
if (!(key in process.env)) {
|
|
5858
|
+
process.env[key] = value;
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
}
|
|
5862
|
+
function setGlobalEnv(key, value) {
|
|
5863
|
+
mkdirSync(AUTONOMA_HOME, { recursive: true });
|
|
5864
|
+
let lines = [];
|
|
5865
|
+
try {
|
|
5866
|
+
lines = readFileSync(GLOBAL_ENV_PATH, "utf-8").split("\n");
|
|
5867
|
+
} catch {
|
|
5868
|
+
lines = [];
|
|
5869
|
+
}
|
|
5870
|
+
const serialized = `${key}=${value}`;
|
|
5871
|
+
let replaced = false;
|
|
5872
|
+
lines = lines.map((line) => {
|
|
5873
|
+
const trimmed = line.trim();
|
|
5874
|
+
if (trimmed.startsWith("#") || !trimmed.includes("=")) return line;
|
|
5875
|
+
const lineKey = trimmed.slice(0, trimmed.indexOf("=")).trim();
|
|
5876
|
+
if (lineKey === key) {
|
|
5877
|
+
replaced = true;
|
|
5878
|
+
return serialized;
|
|
5879
|
+
}
|
|
5880
|
+
return line;
|
|
5881
|
+
});
|
|
5882
|
+
if (!replaced) {
|
|
5883
|
+
if (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
5884
|
+
lines.splice(lines.length - 1, 0, serialized);
|
|
5885
|
+
} else {
|
|
5886
|
+
lines.push(serialized);
|
|
5887
|
+
}
|
|
5888
|
+
}
|
|
5889
|
+
const output = lines.join("\n").replace(/\n*$/, "\n");
|
|
5890
|
+
writeFileSync(GLOBAL_ENV_PATH, output, { encoding: "utf-8", mode: 384 });
|
|
5891
|
+
process.env[key] = value;
|
|
5892
|
+
}
|
|
5893
|
+
|
|
5894
|
+
// src/config.ts
|
|
5588
5895
|
function loadProjectEnv(projectRoot) {
|
|
5589
5896
|
let content;
|
|
5590
5897
|
try {
|
|
5591
|
-
content =
|
|
5898
|
+
content = readFileSync2(join2(projectRoot, ".env"), "utf-8");
|
|
5592
5899
|
} catch {
|
|
5593
5900
|
return;
|
|
5594
5901
|
}
|
|
@@ -5610,6 +5917,7 @@ function loadProjectEnv(projectRoot) {
|
|
|
5610
5917
|
function loadConfig(args) {
|
|
5611
5918
|
const projectRoot = resolve(args.project ?? process.cwd());
|
|
5612
5919
|
loadProjectEnv(projectRoot);
|
|
5920
|
+
loadGlobalEnv();
|
|
5613
5921
|
const projectSlug = args.slug ?? projectRoot.split("/").pop()?.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") ?? "default";
|
|
5614
5922
|
return {
|
|
5615
5923
|
projectRoot,
|
|
@@ -5632,11 +5940,11 @@ init_model();
|
|
|
5632
5940
|
// src/core/output.ts
|
|
5633
5941
|
init_esm_shims();
|
|
5634
5942
|
import { mkdir } from "fs/promises";
|
|
5635
|
-
import { join as
|
|
5636
|
-
import { homedir } from "os";
|
|
5637
|
-
var
|
|
5943
|
+
import { join as join4 } from "path";
|
|
5944
|
+
import { homedir as homedir2 } from "os";
|
|
5945
|
+
var AUTONOMA_HOME2 = join4(homedir2(), ".autonoma");
|
|
5638
5946
|
function getOutputDir(projectSlug) {
|
|
5639
|
-
return
|
|
5947
|
+
return join4(AUTONOMA_HOME2, projectSlug);
|
|
5640
5948
|
}
|
|
5641
5949
|
async function ensureOutputDir(projectSlug) {
|
|
5642
5950
|
const dir = getOutputDir(projectSlug);
|
|
@@ -5644,10 +5952,140 @@ async function ensureOutputDir(projectSlug) {
|
|
|
5644
5952
|
return dir;
|
|
5645
5953
|
}
|
|
5646
5954
|
|
|
5955
|
+
// src/core/interrupt.ts
|
|
5956
|
+
init_esm_shims();
|
|
5957
|
+
import readline from "readline";
|
|
5958
|
+
import { settings } from "@clack/core";
|
|
5959
|
+
var DIM = "\x1B[2m";
|
|
5960
|
+
var RESET = "\x1B[0m";
|
|
5961
|
+
var SHOW_CURSOR = "\x1B[?25h";
|
|
5962
|
+
var EXIT_HINT = `${DIM}(press Ctrl+C again to exit)${RESET}`;
|
|
5963
|
+
var ARM_WINDOW_MS = 3e3;
|
|
5964
|
+
var installed = false;
|
|
5965
|
+
var armed = false;
|
|
5966
|
+
var armTimer = null;
|
|
5967
|
+
var onExit = null;
|
|
5968
|
+
function disarm() {
|
|
5969
|
+
if (armTimer) clearTimeout(armTimer);
|
|
5970
|
+
armTimer = null;
|
|
5971
|
+
armed = false;
|
|
5972
|
+
}
|
|
5973
|
+
function handleInterrupt() {
|
|
5974
|
+
if (armed) {
|
|
5975
|
+
disarm();
|
|
5976
|
+
onExit?.();
|
|
5977
|
+
return;
|
|
5978
|
+
}
|
|
5979
|
+
armed = true;
|
|
5980
|
+
process.stderr.write(`
|
|
5981
|
+
${EXIT_HINT}
|
|
5982
|
+
`);
|
|
5983
|
+
armTimer = setTimeout(disarm, ARM_WINDOW_MS);
|
|
5984
|
+
}
|
|
5985
|
+
function installInterruptHandler(opts) {
|
|
5986
|
+
onExit = opts.onExit;
|
|
5987
|
+
if (installed) return;
|
|
5988
|
+
installed = true;
|
|
5989
|
+
settings.aliases.delete("escape");
|
|
5990
|
+
process.on("SIGINT", handleInterrupt);
|
|
5991
|
+
const original = readline.createInterface.bind(readline);
|
|
5992
|
+
readline.createInterface = ((...args) => {
|
|
5993
|
+
const iface = original(...args);
|
|
5994
|
+
iface.on("SIGINT", handleInterrupt);
|
|
5995
|
+
return iface;
|
|
5996
|
+
});
|
|
5997
|
+
}
|
|
5998
|
+
function restoreTerminal() {
|
|
5999
|
+
try {
|
|
6000
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
6001
|
+
} catch {
|
|
6002
|
+
}
|
|
6003
|
+
process.stdout.write(SHOW_CURSOR);
|
|
6004
|
+
}
|
|
6005
|
+
|
|
6006
|
+
// src/core/analytics.ts
|
|
6007
|
+
init_esm_shims();
|
|
6008
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
6009
|
+
import { join as join5 } from "path";
|
|
6010
|
+
import { homedir as homedir3 } from "os";
|
|
6011
|
+
import { randomUUID } from "crypto";
|
|
6012
|
+
var AUTONOMA_HOME3 = join5(homedir3(), ".autonoma");
|
|
6013
|
+
var DEVICE_ID_PATH = join5(AUTONOMA_HOME3, ".device-id");
|
|
6014
|
+
var POSTHOG_PUBLIC_KEY = "phc_mUOwUj62r8vyiisFPvXLC3G5RftETIBMnKNSHqTBdka";
|
|
6015
|
+
var DEFAULT_HOST = "https://us.i.posthog.com";
|
|
6016
|
+
function resolveKey() {
|
|
6017
|
+
return (process.env.AUTONOMA_POSTHOG_KEY ?? POSTHOG_PUBLIC_KEY).trim();
|
|
6018
|
+
}
|
|
6019
|
+
function resolveHost() {
|
|
6020
|
+
return (process.env.AUTONOMA_POSTHOG_HOST ?? DEFAULT_HOST).replace(/\/+$/, "");
|
|
6021
|
+
}
|
|
6022
|
+
function trackingDisabled() {
|
|
6023
|
+
const v = process.env.DONT_TRACK;
|
|
6024
|
+
return v === "1" || v === "true";
|
|
6025
|
+
}
|
|
6026
|
+
function getIdentity() {
|
|
6027
|
+
const id = process.env.AUTONOMA_DISTINCT_ID?.trim();
|
|
6028
|
+
return id && id.length > 0 ? id : void 0;
|
|
6029
|
+
}
|
|
6030
|
+
var cachedDeviceId = null;
|
|
6031
|
+
function getDeviceId() {
|
|
6032
|
+
if (cachedDeviceId) return cachedDeviceId;
|
|
6033
|
+
try {
|
|
6034
|
+
cachedDeviceId = readFileSync3(DEVICE_ID_PATH, "utf-8").trim();
|
|
6035
|
+
if (cachedDeviceId) return cachedDeviceId;
|
|
6036
|
+
} catch {
|
|
6037
|
+
}
|
|
6038
|
+
cachedDeviceId = randomUUID();
|
|
6039
|
+
try {
|
|
6040
|
+
mkdirSync2(AUTONOMA_HOME3, { recursive: true });
|
|
6041
|
+
writeFileSync2(DEVICE_ID_PATH, cachedDeviceId, { encoding: "utf-8", mode: 384 });
|
|
6042
|
+
} catch {
|
|
6043
|
+
}
|
|
6044
|
+
return cachedDeviceId;
|
|
6045
|
+
}
|
|
6046
|
+
var enabled = null;
|
|
6047
|
+
function isEnabled() {
|
|
6048
|
+
if (enabled === null) {
|
|
6049
|
+
enabled = !trackingDisabled() && resolveKey().length > 0;
|
|
6050
|
+
}
|
|
6051
|
+
return enabled;
|
|
6052
|
+
}
|
|
6053
|
+
var pending = /* @__PURE__ */ new Set();
|
|
6054
|
+
function track(event, properties = {}) {
|
|
6055
|
+
if (!isEnabled()) return;
|
|
6056
|
+
const identity = getIdentity();
|
|
6057
|
+
const body = JSON.stringify({
|
|
6058
|
+
api_key: resolveKey(),
|
|
6059
|
+
event,
|
|
6060
|
+
distinct_id: identity ?? getDeviceId(),
|
|
6061
|
+
properties: {
|
|
6062
|
+
...properties,
|
|
6063
|
+
// Only build a person profile when we have a real identity from the app,
|
|
6064
|
+
// so the CLI joins the existing funnel person instead of creating a new one.
|
|
6065
|
+
$process_person_profile: identity != null,
|
|
6066
|
+
cli_version: process.env.npm_package_version
|
|
6067
|
+
}
|
|
6068
|
+
});
|
|
6069
|
+
const promise = fetch(`${resolveHost()}/capture/`, {
|
|
6070
|
+
method: "POST",
|
|
6071
|
+
headers: { "Content-Type": "application/json" },
|
|
6072
|
+
body
|
|
6073
|
+
}).catch(() => {
|
|
6074
|
+
}).finally(() => pending.delete(promise));
|
|
6075
|
+
pending.add(promise);
|
|
6076
|
+
}
|
|
6077
|
+
async function flushAnalytics(timeoutMs = 1500) {
|
|
6078
|
+
if (pending.size === 0) return;
|
|
6079
|
+
await Promise.race([
|
|
6080
|
+
Promise.allSettled([...pending]),
|
|
6081
|
+
new Promise((resolve5) => setTimeout(resolve5, timeoutMs))
|
|
6082
|
+
]);
|
|
6083
|
+
}
|
|
6084
|
+
|
|
5647
6085
|
// src/core/state.ts
|
|
5648
6086
|
init_esm_shims();
|
|
5649
6087
|
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
5650
|
-
import { join as
|
|
6088
|
+
import { join as join6 } from "path";
|
|
5651
6089
|
var STATE_FILE = ".pipeline-state.json";
|
|
5652
6090
|
function initialState() {
|
|
5653
6091
|
return {
|
|
@@ -5662,7 +6100,7 @@ function initialState() {
|
|
|
5662
6100
|
};
|
|
5663
6101
|
}
|
|
5664
6102
|
async function loadState(outputDir) {
|
|
5665
|
-
const path3 =
|
|
6103
|
+
const path3 = join6(outputDir, STATE_FILE);
|
|
5666
6104
|
try {
|
|
5667
6105
|
const raw = await readFile2(path3, "utf-8");
|
|
5668
6106
|
return JSON.parse(raw);
|
|
@@ -5671,7 +6109,7 @@ async function loadState(outputDir) {
|
|
|
5671
6109
|
}
|
|
5672
6110
|
}
|
|
5673
6111
|
async function saveState(outputDir, state) {
|
|
5674
|
-
const path3 =
|
|
6112
|
+
const path3 = join6(outputDir, STATE_FILE);
|
|
5675
6113
|
await writeFile2(path3, JSON.stringify(state, null, 2), "utf-8");
|
|
5676
6114
|
}
|
|
5677
6115
|
async function markStep(outputDir, state, step, status) {
|
|
@@ -5691,11 +6129,11 @@ function nextPendingStep(state) {
|
|
|
5691
6129
|
var PAGES_FILE = "pages.json";
|
|
5692
6130
|
async function savePages(outputDir, pages) {
|
|
5693
6131
|
const obj = Object.fromEntries(pages);
|
|
5694
|
-
await writeFile12(
|
|
6132
|
+
await writeFile12(join25(outputDir, PAGES_FILE), JSON.stringify(obj, null, 2), "utf-8");
|
|
5695
6133
|
}
|
|
5696
6134
|
async function loadPages(outputDir) {
|
|
5697
6135
|
try {
|
|
5698
|
-
const raw = await
|
|
6136
|
+
const raw = await readFile18(join25(outputDir, PAGES_FILE), "utf-8");
|
|
5699
6137
|
const obj = JSON.parse(raw);
|
|
5700
6138
|
return new Map(Object.entries(obj));
|
|
5701
6139
|
} catch {
|
|
@@ -5732,12 +6170,14 @@ var STEP_INTROS = {
|
|
|
5732
6170
|
kb: "Reading every page file to build a knowledge base (AUTONOMA.md). This gives the AI context about your features, flows, and UI patterns.",
|
|
5733
6171
|
entityAudit: "Identifying every database model and tracing how each gets created \u2014 which service function, what side effects. This determines which entities need test data factories.",
|
|
5734
6172
|
scenarioRecipe: "Designing test data scenarios with realistic values from your entity audit. The scenario defines exactly WHAT data will exist in the database during tests.",
|
|
5735
|
-
recipeBuilder: "Guiding you through implementing Autonoma SDK factories for each entity.
|
|
6173
|
+
recipeBuilder: "Guiding you through implementing Autonoma SDK factories for each entity. For each one we give you a copy-paste guide to hand to Claude (or your AI assistant), which implements the factory in your codebase. Work locally: run your app on localhost and we'll test each factory live (create + teardown) against it. You deploy later, once everything passes.",
|
|
5736
6174
|
testGenerator: "Generating exhaustive E2E test cases by exploring every page and feature. Each area gets test coverage proportional to its complexity."
|
|
5737
6175
|
};
|
|
5738
6176
|
async function runStep(step, outputDir, state, config, projectContext, nonInteractive) {
|
|
5739
6177
|
const label = STEP_LABELS[step];
|
|
5740
6178
|
p8.note(STEP_INTROS[step], `Step: ${label}`);
|
|
6179
|
+
const stepStartedAt = Date.now();
|
|
6180
|
+
track("cli_step_started", { step });
|
|
5741
6181
|
state = await markStep(outputDir, state, step, "running");
|
|
5742
6182
|
if (step !== "pagesFinder" && projectContext && !projectContext.pages) {
|
|
5743
6183
|
const pages = await loadPages(outputDir);
|
|
@@ -5837,6 +6277,11 @@ async function runStep(step, outputDir, state, config, projectContext, nonIntera
|
|
|
5837
6277
|
const message = err instanceof Error ? err.message : String(err);
|
|
5838
6278
|
p8.log.error(`Failed: ${label} \u2014 ${message}`);
|
|
5839
6279
|
}
|
|
6280
|
+
track("cli_step_completed", {
|
|
6281
|
+
step,
|
|
6282
|
+
status: state.steps[step],
|
|
6283
|
+
duration_ms: Date.now() - stepStartedAt
|
|
6284
|
+
});
|
|
5840
6285
|
return state;
|
|
5841
6286
|
}
|
|
5842
6287
|
async function showStatus(outputDir) {
|
|
@@ -5849,15 +6294,33 @@ async function showStatus(outputDir) {
|
|
|
5849
6294
|
}
|
|
5850
6295
|
}
|
|
5851
6296
|
var BANNER = `
|
|
5852
|
-
\x1B[36m\x1B[1m
|
|
5853
|
-
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
6297
|
+
\x1B[36m\x1B[1m \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557
|
|
6298
|
+
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
6299
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551
|
|
6300
|
+
\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551
|
|
6301
|
+
\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551
|
|
6302
|
+
\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
|
|
5858
6303
|
\x1B[0m
|
|
5859
6304
|
\x1B[2m E2E Test Planner \u2014 Generate exhaustive test suites from your codebase\x1B[0m
|
|
5860
6305
|
`;
|
|
6306
|
+
async function ensureOpenRouterKey(nonInteractive) {
|
|
6307
|
+
if (process.env.OPENROUTER_API_KEY) return true;
|
|
6308
|
+
if (nonInteractive) {
|
|
6309
|
+
p8.log.error(
|
|
6310
|
+
"OPENROUTER_API_KEY is not set. Set it in your environment or run interactively once to save it."
|
|
6311
|
+
);
|
|
6312
|
+
return false;
|
|
6313
|
+
}
|
|
6314
|
+
p8.log.info("You'll need an OpenRouter API key to run the planner. Get one at https://openrouter.ai/keys");
|
|
6315
|
+
const key = await p8.password({
|
|
6316
|
+
message: "Paste your OpenRouter API key",
|
|
6317
|
+
validate: (value) => (value ?? "").trim().length === 0 ? "API key cannot be empty" : void 0
|
|
6318
|
+
});
|
|
6319
|
+
if (p8.isCancel(key)) return false;
|
|
6320
|
+
setGlobalEnv("OPENROUTER_API_KEY", key.trim());
|
|
6321
|
+
p8.log.success(`Saved your API key to ${getGlobalEnvPath()} \u2014 you won't be asked again.`);
|
|
6322
|
+
return true;
|
|
6323
|
+
}
|
|
5861
6324
|
async function gatherProjectContext() {
|
|
5862
6325
|
const description = await p8.text({
|
|
5863
6326
|
message: "What is this project? (a short description so the agent knows what it's looking at)",
|
|
@@ -5905,18 +6368,34 @@ async function main() {
|
|
|
5905
6368
|
}
|
|
5906
6369
|
console.log(BANNER);
|
|
5907
6370
|
p8.intro("Let's generate your test suite");
|
|
6371
|
+
const resumeCommand = `autonoma-planner --resume` + (args.project ? ` --project ${args.project}` : "");
|
|
6372
|
+
installInterruptHandler({
|
|
6373
|
+
onExit: () => {
|
|
6374
|
+
track("cli_run_exited");
|
|
6375
|
+
restoreTerminal();
|
|
6376
|
+
console.log("");
|
|
6377
|
+
p8.log.warn(`Your progress is saved. To resume, run:
|
|
6378
|
+
${resumeCommand}`);
|
|
6379
|
+
void flushAnalytics().finally(() => process.exit(0));
|
|
6380
|
+
}
|
|
6381
|
+
});
|
|
5908
6382
|
const config = loadConfig({
|
|
5909
6383
|
project: args.project,
|
|
5910
6384
|
model: args.model,
|
|
5911
6385
|
slug: args.slug
|
|
5912
6386
|
});
|
|
6387
|
+
const nonInteractive = !!args["non-interactive"];
|
|
6388
|
+
if (!await ensureOpenRouterKey(nonInteractive)) {
|
|
6389
|
+
p8.log.warn("Cancelled.");
|
|
6390
|
+
return;
|
|
6391
|
+
}
|
|
5913
6392
|
const modelName = config.modelId ?? process.env.OPENROUTER_MODEL ?? DEFAULT_MODEL;
|
|
5914
6393
|
if (!args.project) {
|
|
5915
6394
|
p8.log.info(`No --project flag passed; using current working directory.`);
|
|
5916
6395
|
}
|
|
5917
6396
|
p8.log.info(`Project: ${config.projectRoot}`);
|
|
5918
6397
|
p8.log.info(`Model: ${modelName}`);
|
|
5919
|
-
|
|
6398
|
+
track("cli_run_started", { model: modelName, non_interactive: nonInteractive });
|
|
5920
6399
|
const outputDir = await ensureOutputDir(config.projectSlug);
|
|
5921
6400
|
let state = await loadState(outputDir);
|
|
5922
6401
|
let isResuming = !!(args.resume || args.step);
|
|
@@ -5956,7 +6435,14 @@ async function main() {
|
|
|
5956
6435
|
}
|
|
5957
6436
|
await saveContext(outputDir, projectContext);
|
|
5958
6437
|
}
|
|
5959
|
-
p8.
|
|
6438
|
+
p8.note(
|
|
6439
|
+
`${outputDir}
|
|
6440
|
+
|
|
6441
|
+
All generated files (knowledge base, scenarios, recipe, tests) live here.
|
|
6442
|
+
It's a hidden folder in your home directory \u2014 in Finder/Explorer use "Go to folder"
|
|
6443
|
+
or reveal hidden files (macOS: Cmd+Shift+. ) to see it.`,
|
|
6444
|
+
"Output folder"
|
|
6445
|
+
);
|
|
5960
6446
|
console.log("");
|
|
5961
6447
|
p8.log.info(
|
|
5962
6448
|
`Got it. I'll focus on: ${projectContext.criticalFlows}
|
|
@@ -5991,7 +6477,8 @@ async function main() {
|
|
|
5991
6477
|
p8.log.error("Pipeline stopped due to failure.");
|
|
5992
6478
|
break;
|
|
5993
6479
|
}
|
|
5994
|
-
|
|
6480
|
+
const skipConfirmAfter = ["pagesFinder"];
|
|
6481
|
+
if (i < steps.length - 1 && !nonInteractive && !skipConfirmAfter.includes(step)) {
|
|
5995
6482
|
const nextStep = steps[i + 1];
|
|
5996
6483
|
const shouldContinue = await p8.confirm({
|
|
5997
6484
|
message: `Continue to ${STEP_LABELS[nextStep]}?`
|
|
@@ -6009,10 +6496,13 @@ async function main() {
|
|
|
6009
6496
|
}
|
|
6010
6497
|
throw err;
|
|
6011
6498
|
}
|
|
6499
|
+
const stepsDone = Object.values(state.steps).filter((s) => s === "done").length;
|
|
6500
|
+
track("cli_run_completed", { steps_done: stepsDone });
|
|
6012
6501
|
p8.outro("Done");
|
|
6013
6502
|
}
|
|
6014
|
-
main().catch((err) => {
|
|
6503
|
+
main().then(() => flushAnalytics()).catch(async (err) => {
|
|
6015
6504
|
console.error(err);
|
|
6505
|
+
await flushAnalytics();
|
|
6016
6506
|
process.exit(1);
|
|
6017
6507
|
});
|
|
6018
6508
|
//# sourceMappingURL=index.js.map
|