builder-assistant-engineer 0.2.0
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/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/analyst/prompt.js +19 -0
- package/dist/analyst/retry.js +38 -0
- package/dist/artifacts/diff.js +47 -0
- package/dist/artifacts/lessons.js +32 -0
- package/dist/artifacts/merge.js +70 -0
- package/dist/artifacts/write.js +44 -0
- package/dist/backends/agent-cli.js +174 -0
- package/dist/backends/api.js +171 -0
- package/dist/backends/index.js +10 -0
- package/dist/backends/manual.js +52 -0
- package/dist/backends/sse.js +24 -0
- package/dist/backends/types.js +1 -0
- package/dist/bin.js +3 -0
- package/dist/cli.js +104 -0
- package/dist/commands/context.js +12 -0
- package/dist/commands/init.js +199 -0
- package/dist/commands/next.js +103 -0
- package/dist/commands/plan.js +71 -0
- package/dist/commands/replan.js +37 -0
- package/dist/commands/review.js +37 -0
- package/dist/commands/shared.js +40 -0
- package/dist/commands/status.js +95 -0
- package/dist/config/commands.js +85 -0
- package/dist/config/schema.js +43 -0
- package/dist/config/settings.js +8 -0
- package/dist/config/store.js +38 -0
- package/dist/core/bash.js +105 -0
- package/dist/core/errors.js +14 -0
- package/dist/core/fs.js +35 -0
- package/dist/core/git.js +104 -0
- package/dist/core/gitignore.js +14 -0
- package/dist/core/json.js +13 -0
- package/dist/core/paths.js +14 -0
- package/dist/core/process.js +56 -0
- package/dist/core/prompt-loader.js +27 -0
- package/dist/core/state.js +27 -0
- package/dist/core/which.js +24 -0
- package/dist/detect/agents.js +17 -0
- package/dist/detect/mode.js +11 -0
- package/dist/digest/baseline.js +192 -0
- package/dist/digest/budget.js +31 -0
- package/dist/digest/files.js +167 -0
- package/dist/digest/format.js +18 -0
- package/dist/digest/git.js +19 -0
- package/dist/digest/index.js +100 -0
- package/dist/digest/manifests.js +133 -0
- package/dist/digest/read.js +28 -0
- package/dist/digest/redact.js +37 -0
- package/dist/digest/render.js +138 -0
- package/dist/digest/select.js +116 -0
- package/dist/digest/todos.js +16 -0
- package/dist/digest/tree.js +55 -0
- package/dist/digest/walk.js +72 -0
- package/dist/gates/capture.js +145 -0
- package/dist/gates/contract.js +339 -0
- package/dist/gates/enforce.js +143 -0
- package/dist/gates/findings.js +17 -0
- package/dist/gates/gate.js +315 -0
- package/dist/gates/ignore-rules.js +104 -0
- package/dist/gates/protected-files.js +311 -0
- package/dist/gates/regression.js +219 -0
- package/dist/gates/reports.js +349 -0
- package/dist/gates/results.js +295 -0
- package/dist/gates/runners.js +189 -0
- package/dist/gates/state-guard.js +56 -0
- package/dist/i18n/en.js +288 -0
- package/dist/i18n/es.js +288 -0
- package/dist/i18n/index.js +18 -0
- package/dist/interview/adaptive.js +69 -0
- package/dist/interview/brief.js +27 -0
- package/dist/interview/questions.js +18 -0
- package/dist/interview/render.js +19 -0
- package/dist/interview/reply.js +34 -0
- package/dist/next/attempts.js +151 -0
- package/dist/next/start.js +179 -0
- package/dist/plan/continuation.js +40 -0
- package/dist/plan/evidence.js +239 -0
- package/dist/plan/filter.js +20 -0
- package/dist/plan/parser.js +283 -0
- package/dist/plan/prior.js +32 -0
- package/dist/plan/questions.js +48 -0
- package/dist/plan/repair.js +33 -0
- package/dist/plan/report.js +37 -0
- package/dist/plan/run.js +191 -0
- package/dist/prompts/analyst.md +101 -0
- package/dist/prompts/continue.md +11 -0
- package/dist/prompts/fix-format.md +13 -0
- package/dist/prompts/fix-paths.md +23 -0
- package/dist/prompts/lesson.md +18 -0
- package/dist/prompts/retry.md +9 -0
- package/dist/prompts/review.md +36 -0
- package/dist/prompts/task.md +9 -0
- package/dist/review/changes.js +177 -0
- package/dist/review/diff.js +115 -0
- package/dist/review/integrity.js +150 -0
- package/dist/review/mechanical.js +62 -0
- package/dist/review/parse.js +40 -0
- package/dist/review/reviewer.js +23 -0
- package/dist/review/run.js +134 -0
- package/dist/review/scope.js +55 -0
- package/dist/review/secrets.js +163 -0
- package/dist/review/snapshot.js +38 -0
- package/dist/review/tests.js +140 -0
- package/dist/tasks/attempts.js +71 -0
- package/dist/tasks/checks.js +181 -0
- package/dist/tasks/frontmatter.js +58 -0
- package/dist/tasks/graph.js +27 -0
- package/dist/tasks/handoff.js +62 -0
- package/dist/tasks/learn.js +128 -0
- package/dist/tasks/load.js +37 -0
- package/dist/tasks/metrics.js +40 -0
- package/dist/tasks/runs.js +12 -0
- package/dist/tasks/schema.js +96 -0
- package/dist/tasks/select.js +15 -0
- package/dist/tasks/shell-words.js +176 -0
- package/dist/tasks/status.js +13 -0
- package/dist/tasks/verify.js +181 -0
- package/dist/ui/clack.js +46 -0
- package/dist/ui/prompter.js +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const BATCH_SIZE = 64;
|
|
4
|
+
const BINARY_SNIFF_BYTES = 8_000;
|
|
5
|
+
export async function readText(root, path) {
|
|
6
|
+
try {
|
|
7
|
+
const buffer = await readFile(join(root, ...path.split("/")));
|
|
8
|
+
if (buffer.subarray(0, BINARY_SNIFF_BYTES).includes(0))
|
|
9
|
+
return undefined;
|
|
10
|
+
return buffer.toString("utf8");
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function readTexts(root, paths) {
|
|
17
|
+
const texts = new Map();
|
|
18
|
+
for (let start = 0; start < paths.length; start += BATCH_SIZE) {
|
|
19
|
+
const batch = paths.slice(start, start + BATCH_SIZE);
|
|
20
|
+
const results = await Promise.all(batch.map((path) => readText(root, path)));
|
|
21
|
+
batch.forEach((path, index) => {
|
|
22
|
+
const text = results[index];
|
|
23
|
+
if (text !== undefined)
|
|
24
|
+
texts.set(path, text);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return texts;
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const REDACTED = "[REDACTED]";
|
|
2
|
+
export const SECRET_TOKENS = [
|
|
3
|
+
["private key", /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g],
|
|
4
|
+
[
|
|
5
|
+
"private key",
|
|
6
|
+
/-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?-----END PGP PRIVATE KEY BLOCK-----/g,
|
|
7
|
+
],
|
|
8
|
+
["AWS access key", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g],
|
|
9
|
+
["Slack webhook", /https:\/\/hooks\.slack\.com\/services\/T\w+\/B\w+\/\w+/g],
|
|
10
|
+
["GitHub token", /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g],
|
|
11
|
+
["GitHub token", /\bgithub_pat_[A-Za-z0-9_]{22,}/g],
|
|
12
|
+
["Slack token", /\bxox[abprs]-[A-Za-z0-9-]{10,}/g],
|
|
13
|
+
["API key", /\bsk-[A-Za-z0-9_-]{20,}/g],
|
|
14
|
+
["Stripe key", /\b[rs]k_(?:live|test)_[A-Za-z0-9]{16,}/g],
|
|
15
|
+
["Google API key", /\bAIza[0-9A-Za-z_-]{35}/g],
|
|
16
|
+
["npm token", /\bnpm_[A-Za-z0-9]{36}\b/g],
|
|
17
|
+
["GitLab token", /\bglpat-[A-Za-z0-9_-]{20,}/g],
|
|
18
|
+
["JWT", /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g],
|
|
19
|
+
];
|
|
20
|
+
const TOKENS = [
|
|
21
|
+
...SECRET_TOKENS.map(([, pattern]) => pattern),
|
|
22
|
+
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
|
|
23
|
+
];
|
|
24
|
+
const KEYWORD = "[A-Za-z0-9_.-]*(?:api[_-]?key|secret|token|passw(?:or)?d|pwd|credential)[A-Za-z0-9_.-]*";
|
|
25
|
+
const VALUES = [
|
|
26
|
+
[new RegExp(`(${KEYWORD}["']?\\s*[:=]\\s*)(["'])[^"'\\s]{8,}\\2`, "gi"), `$1$2${REDACTED}$2`],
|
|
27
|
+
[
|
|
28
|
+
/^(\s*(?:export\s+)?[A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD|CREDENTIALS?)[A-Z0-9_]*\s*[=:]\s*)[^\s"'#]{8,}/gm,
|
|
29
|
+
`$1${REDACTED}`,
|
|
30
|
+
],
|
|
31
|
+
[/(_auth(?:Token)?\s*=\s*)\S+/g, `$1${REDACTED}`],
|
|
32
|
+
[/([a-z][a-z0-9+.-]*:\/\/)[^\s:@/]+:[^\s@/]+@/gi, `$1${REDACTED}@`],
|
|
33
|
+
];
|
|
34
|
+
export function redact(text) {
|
|
35
|
+
const withoutTokens = TOKENS.reduce((result, pattern) => result.replace(pattern, REDACTED), text);
|
|
36
|
+
return VALUES.reduce((result, [pattern, replacement]) => result.replace(pattern, replacement), withoutTokens);
|
|
37
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { BASELINE_CHECKS } from "./baseline.js";
|
|
2
|
+
import { renderBlock } from "./budget.js";
|
|
3
|
+
import { isLockfile, languageOf } from "./files.js";
|
|
4
|
+
import { codeFence, formatBytes } from "./format.js";
|
|
5
|
+
import { renderTree } from "./tree.js";
|
|
6
|
+
const LANGUAGE_ROWS = 12;
|
|
7
|
+
const LARGEST_FILES = 10;
|
|
8
|
+
const BRANCH_LIMIT = 20;
|
|
9
|
+
const OMITTED_LIMIT = 50;
|
|
10
|
+
export function renderHead(facts) {
|
|
11
|
+
return [
|
|
12
|
+
renderSummary(facts),
|
|
13
|
+
renderBaseline(facts.baseline),
|
|
14
|
+
renderLanguages(facts.scan),
|
|
15
|
+
list("Entry points", facts.entryPoints),
|
|
16
|
+
list("Configs", facts.configs),
|
|
17
|
+
list("Docs", facts.docs),
|
|
18
|
+
`## Tree (depth ${facts.treeDepth})\n\n${renderFileTree(facts)}`,
|
|
19
|
+
renderGit(facts.git),
|
|
20
|
+
renderTodos(facts.todos),
|
|
21
|
+
renderLargest(facts.scan),
|
|
22
|
+
].join("\n\n");
|
|
23
|
+
}
|
|
24
|
+
export function renderContents(blocks) {
|
|
25
|
+
let group = "";
|
|
26
|
+
let text = "";
|
|
27
|
+
for (const block of blocks) {
|
|
28
|
+
if (block.group !== group) {
|
|
29
|
+
group = block.group;
|
|
30
|
+
text += `## ${group}\n\n`;
|
|
31
|
+
}
|
|
32
|
+
text += renderBlock(block);
|
|
33
|
+
}
|
|
34
|
+
return text;
|
|
35
|
+
}
|
|
36
|
+
export function renderOmitted(paths) {
|
|
37
|
+
if (paths.length === 0)
|
|
38
|
+
return "";
|
|
39
|
+
const shown = paths.slice(0, OMITTED_LIMIT).map((path) => `- ${path}`);
|
|
40
|
+
const extra = paths.length - OMITTED_LIMIT;
|
|
41
|
+
const more = extra > 0 ? [`- … ${extra} more`] : [];
|
|
42
|
+
return [
|
|
43
|
+
"## Omitted for budget",
|
|
44
|
+
"",
|
|
45
|
+
"Not included above; read them directly if needed.",
|
|
46
|
+
"",
|
|
47
|
+
...shown,
|
|
48
|
+
...more,
|
|
49
|
+
"",
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
function renderSummary(facts) {
|
|
53
|
+
const bytes = facts.scan.files.reduce((sum, file) => sum + file.size, 0);
|
|
54
|
+
const truncated = facts.scan.truncated ? ` (scan stopped at ${facts.scan.files.length})` : "";
|
|
55
|
+
return [
|
|
56
|
+
"# Repository digest",
|
|
57
|
+
"",
|
|
58
|
+
`- Root: ${facts.name}`,
|
|
59
|
+
`- Mode: ${facts.mode}`,
|
|
60
|
+
`- Files: ${facts.scan.files.length} (${formatBytes(bytes)})${truncated}`,
|
|
61
|
+
`- Manifests: ${joinOrNone(facts.manifests.map((manifest) => manifest.path))}`,
|
|
62
|
+
`- Package managers: ${joinOrNone(facts.packageManagers)}`,
|
|
63
|
+
`- Frameworks: ${joinOrNone(facts.frameworks)}`,
|
|
64
|
+
].join("\n");
|
|
65
|
+
}
|
|
66
|
+
function renderBaseline(baseline) {
|
|
67
|
+
const rows = BASELINE_CHECKS.map((check) => {
|
|
68
|
+
const evidence = baseline[check];
|
|
69
|
+
return evidence.length > 0
|
|
70
|
+
? `- ${check}: present — ${evidence.join("; ")}`
|
|
71
|
+
: `- ${check}: absent`;
|
|
72
|
+
});
|
|
73
|
+
return ["## Baseline", "", ...rows].join("\n");
|
|
74
|
+
}
|
|
75
|
+
function renderLanguages(scan) {
|
|
76
|
+
const totals = new Map();
|
|
77
|
+
for (const file of scan.files) {
|
|
78
|
+
const language = languageOf(file.path);
|
|
79
|
+
if (!language)
|
|
80
|
+
continue;
|
|
81
|
+
const total = totals.get(language) ?? { files: 0, bytes: 0 };
|
|
82
|
+
totals.set(language, { files: total.files + 1, bytes: total.bytes + file.size });
|
|
83
|
+
}
|
|
84
|
+
if (totals.size === 0)
|
|
85
|
+
return "## Languages\n\nNo source files.";
|
|
86
|
+
const rows = [...totals.entries()]
|
|
87
|
+
.sort(([, a], [, b]) => b.bytes - a.bytes)
|
|
88
|
+
.slice(0, LANGUAGE_ROWS)
|
|
89
|
+
.map(([language, total]) => `| ${language} | ${total.files} | ${formatBytes(total.bytes)} |`);
|
|
90
|
+
return ["## Languages", "", "| Language | Files | Size |", "| --- | --- | --- |", ...rows].join("\n");
|
|
91
|
+
}
|
|
92
|
+
function renderFileTree(facts) {
|
|
93
|
+
const paths = facts.scan.files.map((file) => file.path);
|
|
94
|
+
return paths.length > 0 ? codeFence(renderTree(paths, facts.treeDepth), "text") : "Empty.";
|
|
95
|
+
}
|
|
96
|
+
function renderGit(git) {
|
|
97
|
+
if (!git)
|
|
98
|
+
return "## Git\n\nNot a git repository.";
|
|
99
|
+
const extra = git.branches.length - BRANCH_LIMIT;
|
|
100
|
+
const branches = git.branches.slice(0, BRANCH_LIMIT).join(", ") + (extra > 0 ? ` (+${extra})` : "");
|
|
101
|
+
return [
|
|
102
|
+
"## Git",
|
|
103
|
+
"",
|
|
104
|
+
`- Branch: ${git.branch || "(detached)"}`,
|
|
105
|
+
`- Branches: ${branches || "none"}`,
|
|
106
|
+
`- Uncommitted changes: ${git.changes} files`,
|
|
107
|
+
"- Recent commits:",
|
|
108
|
+
...(git.commits.length > 0 ? git.commits.map((commit) => ` - ${commit}`) : [" - none"]),
|
|
109
|
+
].join("\n");
|
|
110
|
+
}
|
|
111
|
+
function renderTodos(todos) {
|
|
112
|
+
if (todos.total === 0)
|
|
113
|
+
return "## TODO/FIXME\n\nNone found.";
|
|
114
|
+
const tags = Object.entries(todos.byTag)
|
|
115
|
+
.sort(([a, x], [b, y]) => y - x || (a < b ? -1 : 1))
|
|
116
|
+
.map(([tag, count]) => `${tag} ${count}`)
|
|
117
|
+
.join(", ");
|
|
118
|
+
const files = todos.files.map(([path, count]) => `${path} (${count})`).join(", ");
|
|
119
|
+
return ["## TODO/FIXME", "", `- Total: ${todos.total} (${tags})`, `- Top files: ${files}`].join("\n");
|
|
120
|
+
}
|
|
121
|
+
function renderLargest(scan) {
|
|
122
|
+
const largest = scan.files
|
|
123
|
+
.filter((file) => !isLockfile(file.path))
|
|
124
|
+
.sort((a, b) => b.size - a.size)
|
|
125
|
+
.slice(0, LARGEST_FILES)
|
|
126
|
+
.map((file) => `- ${file.path} — ${formatBytes(file.size)}`);
|
|
127
|
+
return ["## Largest files", "", ...(largest.length > 0 ? largest : ["None."])].join("\n");
|
|
128
|
+
}
|
|
129
|
+
function list(title, items) {
|
|
130
|
+
return [
|
|
131
|
+
`## ${title}`,
|
|
132
|
+
"",
|
|
133
|
+
...(items.length > 0 ? items.map((item) => `- ${item}`) : ["None found."]),
|
|
134
|
+
].join("\n");
|
|
135
|
+
}
|
|
136
|
+
function joinOrNone(items) {
|
|
137
|
+
return items.length > 0 ? items.join(", ") : "none";
|
|
138
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { asRecord } from "../core/json.js";
|
|
3
|
+
import { isCiPath } from "./baseline.js";
|
|
4
|
+
import { baseName, byPath } from "./files.js";
|
|
5
|
+
import { ecosystemOf } from "./manifests.js";
|
|
6
|
+
export const MAX_ENTRY_POINTS = 8;
|
|
7
|
+
export const MAX_DOCS = 10;
|
|
8
|
+
export const MAX_CONFIGS = 20;
|
|
9
|
+
const ENTRY_PATTERNS = [
|
|
10
|
+
/^(src\/)?(index|main|app|server|cli)\.(ts|tsx|mts|js|jsx|mjs|cjs)$/,
|
|
11
|
+
/^(src\/)?App\.(tsx|jsx|vue|svelte)$/,
|
|
12
|
+
/^(src\/)?app\/(page|layout)\.(tsx|jsx)$/,
|
|
13
|
+
/^pages\/(index|_app)\.(tsx|jsx|js)$/,
|
|
14
|
+
/^(app\/|src\/[^/]+\/)?(main|app|manage|wsgi|asgi|__main__)\.py$/,
|
|
15
|
+
/^src\/(main|lib)\.rs$/,
|
|
16
|
+
/^(cmd\/[^/]+\/)?main\.go$/,
|
|
17
|
+
/^Program\.cs$/,
|
|
18
|
+
/^src\/main\/java\/.+Application\.java$/,
|
|
19
|
+
/^public\/index\.php$/,
|
|
20
|
+
/^artisan$/,
|
|
21
|
+
/^config\.ru$/,
|
|
22
|
+
/^lib\/main\.dart$/,
|
|
23
|
+
/^Sources\/[^/]+\/main\.swift$/,
|
|
24
|
+
];
|
|
25
|
+
const ROOT_DOCS = [
|
|
26
|
+
/^readme(\.[a-z]+)?$/i,
|
|
27
|
+
/^agents\.md$/i,
|
|
28
|
+
/^claude\.md$/i,
|
|
29
|
+
/^gemini\.md$/i,
|
|
30
|
+
/^contributing(\.[a-z]+)?$/i,
|
|
31
|
+
/^architecture(\.[a-z]+)?$/i,
|
|
32
|
+
];
|
|
33
|
+
const CONFIG_NAMES = [
|
|
34
|
+
/^tsconfig(\..+)?\.json$/,
|
|
35
|
+
/^jsconfig\.json$/,
|
|
36
|
+
/^Dockerfile(\..+)?$/,
|
|
37
|
+
/^(docker-)?compose(\..+)?\.ya?ml$/,
|
|
38
|
+
/^eslint\.config\.[cm]?[jt]s$/,
|
|
39
|
+
/^\.eslintrc(\..+)?$/,
|
|
40
|
+
/^\.prettierrc(\..+)?$/,
|
|
41
|
+
/^prettier\.config\.[cm]?[jt]s$/,
|
|
42
|
+
/^biome\.jsonc?$/,
|
|
43
|
+
/^\.editorconfig$/,
|
|
44
|
+
/^(vite|vitest|jest|playwright|webpack|rollup|next|nuxt|svelte|astro|tailwind|postcss|babel)\.config\.[cm]?[jt]s$/,
|
|
45
|
+
/^Makefile$/,
|
|
46
|
+
/^justfile$/i,
|
|
47
|
+
/^Taskfile\.ya?ml$/,
|
|
48
|
+
/^\.nvmrc$/,
|
|
49
|
+
/^\.node-version$/,
|
|
50
|
+
/^\.python-version$/,
|
|
51
|
+
/^\.tool-versions$/,
|
|
52
|
+
/^tox\.ini$/,
|
|
53
|
+
/^\.?ruff\.toml$/,
|
|
54
|
+
/^\.?mypy\.ini$/,
|
|
55
|
+
/^pytest\.ini$/,
|
|
56
|
+
/^\.golangci\.ya?ml$/,
|
|
57
|
+
/^\.rubocop\.yml$/,
|
|
58
|
+
/^phpunit\.xml(\.dist)?$/,
|
|
59
|
+
/^Procfile$/,
|
|
60
|
+
/^fly\.toml$/,
|
|
61
|
+
/^vercel\.json$/,
|
|
62
|
+
/^netlify\.toml$/,
|
|
63
|
+
/^turbo\.json$/,
|
|
64
|
+
/^nx\.json$/,
|
|
65
|
+
/^pnpm-workspace\.yaml$/,
|
|
66
|
+
/^\.env\.(example|sample|template)$/,
|
|
67
|
+
];
|
|
68
|
+
export function findEntryPoints(paths, manifests) {
|
|
69
|
+
const available = new Set(paths);
|
|
70
|
+
const declared = manifests
|
|
71
|
+
.flatMap(declaredEntries)
|
|
72
|
+
.map((path) => resolveSource(path, available))
|
|
73
|
+
.filter((path) => path !== undefined);
|
|
74
|
+
const conventional = paths.filter((path) => ENTRY_PATTERNS.some((pattern) => pattern.test(path)));
|
|
75
|
+
return [...new Set([...declared, ...conventional])].slice(0, MAX_ENTRY_POINTS);
|
|
76
|
+
}
|
|
77
|
+
export function findDocs(paths) {
|
|
78
|
+
const root = ROOT_DOCS.flatMap((pattern) => paths.filter((path) => !path.includes("/") && pattern.test(path)));
|
|
79
|
+
const nested = paths
|
|
80
|
+
.filter((path) => /^docs?\/.+\.(md|mdx|rst|txt)$/i.test(path) && !path.startsWith("docs/plan/"))
|
|
81
|
+
.sort(byPath);
|
|
82
|
+
return [...new Set([...root, ...nested])].slice(0, MAX_DOCS);
|
|
83
|
+
}
|
|
84
|
+
export function findConfigs(paths) {
|
|
85
|
+
return paths
|
|
86
|
+
.filter((path) => isCiPath(path) || CONFIG_NAMES.some((pattern) => pattern.test(baseName(path))))
|
|
87
|
+
.filter((path) => ecosystemOf(path) === undefined)
|
|
88
|
+
.sort(byPath)
|
|
89
|
+
.slice(0, MAX_CONFIGS);
|
|
90
|
+
}
|
|
91
|
+
function declaredEntries(manifest) {
|
|
92
|
+
if (baseName(manifest.path) !== "package.json" || !manifest.json)
|
|
93
|
+
return [];
|
|
94
|
+
const { main, module, bin, exports } = manifest.json;
|
|
95
|
+
const dir = posix.dirname(manifest.path);
|
|
96
|
+
return [main, module, ...binEntries(bin), exportRoot(exports)]
|
|
97
|
+
.filter((value) => typeof value === "string")
|
|
98
|
+
.map((value) => posix.normalize(posix.join(dir, value)));
|
|
99
|
+
}
|
|
100
|
+
function binEntries(bin) {
|
|
101
|
+
return typeof bin === "string" ? [bin] : Object.values(asRecord(bin));
|
|
102
|
+
}
|
|
103
|
+
function exportRoot(exports) {
|
|
104
|
+
if (typeof exports === "string")
|
|
105
|
+
return exports;
|
|
106
|
+
const root = asRecord(exports)["."] ?? exports;
|
|
107
|
+
if (typeof root === "string")
|
|
108
|
+
return root;
|
|
109
|
+
const conditions = asRecord(root);
|
|
110
|
+
return conditions.import ?? conditions.default ?? conditions.require;
|
|
111
|
+
}
|
|
112
|
+
function resolveSource(path, available) {
|
|
113
|
+
const stem = path.replace(/^(dist|build|lib|out)\//, "src/").replace(/\.(m|c)?js$/, "");
|
|
114
|
+
const candidates = [path, ...[".ts", ".tsx", ".mts", ".js"].map((ext) => `${stem}${ext}`)];
|
|
115
|
+
return candidates.find((candidate) => available.has(candidate));
|
|
116
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const TAG = /\b(TODO|FIXME|HACK|XXX)\b/g;
|
|
2
|
+
const TOP_FILES = 10;
|
|
3
|
+
export function countTodos(texts) {
|
|
4
|
+
const byTag = {};
|
|
5
|
+
const files = [];
|
|
6
|
+
for (const [path, text] of texts) {
|
|
7
|
+
const tags = Array.from(text.matchAll(TAG), (match) => match[1] ?? "");
|
|
8
|
+
for (const tag of tags)
|
|
9
|
+
byTag[tag] = (byTag[tag] ?? 0) + 1;
|
|
10
|
+
if (tags.length > 0)
|
|
11
|
+
files.push([path, tags.length]);
|
|
12
|
+
}
|
|
13
|
+
files.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
|
|
14
|
+
const total = files.reduce((sum, [, count]) => sum + count, 0);
|
|
15
|
+
return { total, byTag, files: files.slice(0, TOP_FILES) };
|
|
16
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const DEFAULT_TREE_DEPTH = 3;
|
|
2
|
+
export const TREE_MAX_LINES = 300;
|
|
3
|
+
export const TREE_ENTRIES_PER_DIR = 30;
|
|
4
|
+
export function renderTree(paths, depth = DEFAULT_TREE_DEPTH) {
|
|
5
|
+
const lines = [];
|
|
6
|
+
renderNode(buildTree(paths), 0, depth, lines);
|
|
7
|
+
if (lines.length <= TREE_MAX_LINES)
|
|
8
|
+
return lines.join("\n");
|
|
9
|
+
const hidden = lines.length - TREE_MAX_LINES;
|
|
10
|
+
return [...lines.slice(0, TREE_MAX_LINES), `… ${hidden} more lines`].join("\n");
|
|
11
|
+
}
|
|
12
|
+
function buildTree(paths) {
|
|
13
|
+
const root = newNode();
|
|
14
|
+
for (const path of paths) {
|
|
15
|
+
const parts = path.split("/");
|
|
16
|
+
const file = parts.pop() ?? path;
|
|
17
|
+
let node = root;
|
|
18
|
+
node.count++;
|
|
19
|
+
for (const part of parts) {
|
|
20
|
+
const child = node.dirs.get(part) ?? newNode();
|
|
21
|
+
node.dirs.set(part, child);
|
|
22
|
+
node = child;
|
|
23
|
+
node.count++;
|
|
24
|
+
}
|
|
25
|
+
node.files.push(file);
|
|
26
|
+
}
|
|
27
|
+
return root;
|
|
28
|
+
}
|
|
29
|
+
function renderNode(node, level, depth, lines) {
|
|
30
|
+
const indent = " ".repeat(level);
|
|
31
|
+
const dirs = [...node.dirs.entries()].sort(([a], [b]) => (a < b ? -1 : 1));
|
|
32
|
+
const files = [...node.files].sort();
|
|
33
|
+
let shown = 0;
|
|
34
|
+
for (const [name, child] of dirs) {
|
|
35
|
+
if (shown++ >= TREE_ENTRIES_PER_DIR)
|
|
36
|
+
break;
|
|
37
|
+
if (level + 1 >= depth) {
|
|
38
|
+
lines.push(`${indent}${name}/ (${child.count} files)`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
lines.push(`${indent}${name}/`);
|
|
42
|
+
renderNode(child, level + 1, depth, lines);
|
|
43
|
+
}
|
|
44
|
+
for (const file of files) {
|
|
45
|
+
if (shown++ >= TREE_ENTRIES_PER_DIR)
|
|
46
|
+
break;
|
|
47
|
+
lines.push(`${indent}${file}`);
|
|
48
|
+
}
|
|
49
|
+
const hidden = dirs.length + files.length - TREE_ENTRIES_PER_DIR;
|
|
50
|
+
if (hidden > 0)
|
|
51
|
+
lines.push(`${indent}… ${hidden} more`);
|
|
52
|
+
}
|
|
53
|
+
function newNode() {
|
|
54
|
+
return { dirs: new Map(), files: [], count: 0 };
|
|
55
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import ignore, {} from "ignore";
|
|
4
|
+
import { readTextIfExists } from "../core/fs.js";
|
|
5
|
+
export const MAX_FILES = 50_000;
|
|
6
|
+
const SKIPPED_DIRS = new Set([
|
|
7
|
+
".git",
|
|
8
|
+
".hg",
|
|
9
|
+
".svn",
|
|
10
|
+
".bae",
|
|
11
|
+
"node_modules",
|
|
12
|
+
".venv",
|
|
13
|
+
"venv",
|
|
14
|
+
"__pycache__",
|
|
15
|
+
".mypy_cache",
|
|
16
|
+
".pytest_cache",
|
|
17
|
+
".ruff_cache",
|
|
18
|
+
".tox",
|
|
19
|
+
".next",
|
|
20
|
+
".nuxt",
|
|
21
|
+
".svelte-kit",
|
|
22
|
+
".turbo",
|
|
23
|
+
".cache",
|
|
24
|
+
".gradle",
|
|
25
|
+
".idea",
|
|
26
|
+
".terraform",
|
|
27
|
+
"coverage",
|
|
28
|
+
]);
|
|
29
|
+
export async function scanFiles(root, maxFiles = MAX_FILES) {
|
|
30
|
+
const scan = { files: [], truncated: false };
|
|
31
|
+
const rules = await loadRules(root, "", [".gitignore", ".baeignore"]);
|
|
32
|
+
await walk(root, "", rules, scan, maxFiles);
|
|
33
|
+
return scan;
|
|
34
|
+
}
|
|
35
|
+
async function walk(root, dir, inherited, scan, maxFiles) {
|
|
36
|
+
const rules = dir ? [...inherited, ...(await loadRules(root, dir, [".gitignore"]))] : inherited;
|
|
37
|
+
for (const entry of await listDir(join(root, dir))) {
|
|
38
|
+
if (scan.files.length >= maxFiles) {
|
|
39
|
+
scan.truncated = true;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const path = dir ? `${dir}/${entry.name}` : entry.name;
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
if (!SKIPPED_DIRS.has(entry.name) && !isIgnored(path, true, rules)) {
|
|
45
|
+
await walk(root, path, rules, scan, maxFiles);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else if (entry.isFile() && !isIgnored(path, false, rules)) {
|
|
49
|
+
scan.files.push({ path, size: (await stat(join(root, path))).size });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function listDir(path) {
|
|
54
|
+
try {
|
|
55
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
56
|
+
return entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function loadRules(root, dir, names) {
|
|
63
|
+
const texts = await Promise.all(names.map((name) => readTextIfExists(join(root, dir, name))));
|
|
64
|
+
const patterns = texts.filter((text) => text !== undefined);
|
|
65
|
+
return patterns.length > 0 ? [{ base: dir, matcher: ignore().add(patterns.join("\n")) }] : [];
|
|
66
|
+
}
|
|
67
|
+
function isIgnored(path, isDir, rules) {
|
|
68
|
+
return rules.some(({ base, matcher }) => {
|
|
69
|
+
const relative = base ? path.slice(base.length + 1) : path;
|
|
70
|
+
return matcher.ignores(isDir ? `${relative}/` : relative);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { configSchema } from "../config/schema.js";
|
|
5
|
+
import { readTextIfExists, writeText } from "../core/fs.js";
|
|
6
|
+
import { flaggedFiles, headCommit, isGitRepo } from "../core/git.js";
|
|
7
|
+
import { BAE_DIR } from "../core/paths.js";
|
|
8
|
+
import { loadPrompt } from "../core/prompt-loader.js";
|
|
9
|
+
import { repoState } from "../core/state.js";
|
|
10
|
+
import { findReviewer } from "../review/reviewer.js";
|
|
11
|
+
import { scopePaths } from "../review/scope.js";
|
|
12
|
+
import { hashPaths, takeSnapshot } from "../review/snapshot.js";
|
|
13
|
+
import { runDir } from "../tasks/runs.js";
|
|
14
|
+
import { verificationCommands } from "../tasks/schema.js";
|
|
15
|
+
import { collectProtected, TASKS_DIR } from "./contract.js";
|
|
16
|
+
import { captureIgnore } from "./ignore-rules.js";
|
|
17
|
+
import { suiteBaselineSchema, suiteCommands } from "./regression.js";
|
|
18
|
+
export const CAPTURED_PROMPTS = ["review", "retry", "lesson", "fix-format"];
|
|
19
|
+
const promptSchema = z.object({ path: z.string(), text: z.string() });
|
|
20
|
+
const captureSchema = z.object({
|
|
21
|
+
version: z.literal(1),
|
|
22
|
+
id: z.string(),
|
|
23
|
+
path: z.string(),
|
|
24
|
+
takenAt: z.string(),
|
|
25
|
+
git: z.boolean(),
|
|
26
|
+
base: z.string().optional(),
|
|
27
|
+
snapshot: z.record(z.string(), z.string().nullable()),
|
|
28
|
+
ignore: z.array(z.object({ dir: z.string(), text: z.string() })),
|
|
29
|
+
config: configSchema,
|
|
30
|
+
task: z.string(),
|
|
31
|
+
prompts: z.object({
|
|
32
|
+
review: promptSchema,
|
|
33
|
+
retry: promptSchema,
|
|
34
|
+
lesson: promptSchema,
|
|
35
|
+
"fix-format": promptSchema,
|
|
36
|
+
}),
|
|
37
|
+
reviewer: z.string(),
|
|
38
|
+
agentsMd: z.string().optional(),
|
|
39
|
+
protected: z.record(z.string(), z.string()),
|
|
40
|
+
shadows: z.array(z.string()).default([]),
|
|
41
|
+
flagged: z.array(z.string()).default([]),
|
|
42
|
+
baseline: suiteBaselineSchema.optional(),
|
|
43
|
+
skips: z.array(z.string()).default([]),
|
|
44
|
+
late: z.boolean().optional(),
|
|
45
|
+
finished: z.boolean().optional(),
|
|
46
|
+
});
|
|
47
|
+
const CAPTURE_FILE = "capture.json";
|
|
48
|
+
const ACTIVE_FILE = "active.json";
|
|
49
|
+
const AGENTS_MD = "AGENTS.md";
|
|
50
|
+
export async function prepareCapture(cwd, config, task) {
|
|
51
|
+
const previous = await readCapture(cwd, task.meta.id);
|
|
52
|
+
const reuse = previous && !previous.finished ? previous : undefined;
|
|
53
|
+
const fixed = reuse
|
|
54
|
+
? {
|
|
55
|
+
takenAt: reuse.takenAt,
|
|
56
|
+
git: reuse.git,
|
|
57
|
+
base: reuse.base,
|
|
58
|
+
snapshot: reuse.snapshot,
|
|
59
|
+
ignore: reuse.ignore,
|
|
60
|
+
baseline: reuse.baseline,
|
|
61
|
+
skips: reuse.skips,
|
|
62
|
+
late: reuse.late,
|
|
63
|
+
}
|
|
64
|
+
: await captureFixed(cwd);
|
|
65
|
+
return {
|
|
66
|
+
version: 1,
|
|
67
|
+
id: task.meta.id,
|
|
68
|
+
path: task.path,
|
|
69
|
+
...fixed,
|
|
70
|
+
...(await captureContract(cwd, config, task)),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
export async function readCapture(cwd, id) {
|
|
74
|
+
const text = await readTextIfExists(join(runDir(cwd, id), CAPTURE_FILE));
|
|
75
|
+
if (text === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
try {
|
|
78
|
+
const result = captureSchema.safeParse(JSON.parse(text));
|
|
79
|
+
return result.success ? result.data : undefined;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function saveCapture(cwd, capture) {
|
|
86
|
+
const path = join(runDir(cwd, capture.id), CAPTURE_FILE);
|
|
87
|
+
await writeText(path, `${JSON.stringify(capture, null, 2)}\n`);
|
|
88
|
+
}
|
|
89
|
+
export async function markActive(cwd, id) {
|
|
90
|
+
await writeText(join(repoState(cwd), ACTIVE_FILE), `${JSON.stringify({ id })}\n`);
|
|
91
|
+
}
|
|
92
|
+
export async function clearActive(cwd) {
|
|
93
|
+
await rm(join(repoState(cwd), ACTIVE_FILE), { force: true });
|
|
94
|
+
}
|
|
95
|
+
export async function readActive(cwd) {
|
|
96
|
+
const text = await readTextIfExists(join(repoState(cwd), ACTIVE_FILE));
|
|
97
|
+
try {
|
|
98
|
+
const id = JSON.parse(text ?? "").id;
|
|
99
|
+
return typeof id === "string" ? id : undefined;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export async function trustAgentsMd(cwd, capture, trusted) {
|
|
106
|
+
const current = await readTextIfExists(join(cwd, AGENTS_MD));
|
|
107
|
+
capture.agentsMd = trusted;
|
|
108
|
+
if (current === trusted) {
|
|
109
|
+
capture.protected[AGENTS_MD] = trusted;
|
|
110
|
+
Object.assign(capture.snapshot, await hashPaths(cwd, [AGENTS_MD]));
|
|
111
|
+
}
|
|
112
|
+
await saveCapture(cwd, capture);
|
|
113
|
+
}
|
|
114
|
+
export function capturedPrompt(capture, name) {
|
|
115
|
+
return capture.prompts[name];
|
|
116
|
+
}
|
|
117
|
+
async function captureFixed(cwd) {
|
|
118
|
+
const git = await isGitRepo(cwd);
|
|
119
|
+
const base = git ? await headCommit(cwd) : undefined;
|
|
120
|
+
return {
|
|
121
|
+
takenAt: new Date().toISOString(),
|
|
122
|
+
git,
|
|
123
|
+
...(base ? { base } : {}),
|
|
124
|
+
snapshot: await takeSnapshot(cwd, [BAE_DIR, TASKS_DIR]),
|
|
125
|
+
ignore: git ? await captureIgnore(cwd) : [],
|
|
126
|
+
skips: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
async function captureContract(cwd, config, task) {
|
|
130
|
+
const prompts = await Promise.all(CAPTURED_PROMPTS.map((name) => loadPrompt(name, cwd)));
|
|
131
|
+
const agentsMd = await readTextIfExists(join(cwd, AGENTS_MD));
|
|
132
|
+
return {
|
|
133
|
+
config,
|
|
134
|
+
task: task.text,
|
|
135
|
+
prompts: Object.fromEntries(CAPTURED_PROMPTS.map((name, index) => [name, prompts[index]])),
|
|
136
|
+
reviewer: await findReviewer(cwd, config.backend),
|
|
137
|
+
...(agentsMd === undefined ? {} : { agentsMd }),
|
|
138
|
+
...(await collectProtected(cwd, {
|
|
139
|
+
suite: suiteCommands(config).map((item) => item.command),
|
|
140
|
+
verification: verificationCommands(task.body),
|
|
141
|
+
own: scopePaths(task),
|
|
142
|
+
})),
|
|
143
|
+
flagged: (await flaggedFiles(cwd)) ?? [],
|
|
144
|
+
};
|
|
145
|
+
}
|