karajan-code 1.41.0 → 1.42.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/package.json +1 -1
- package/src/audit/basal-cost.js +190 -0
- package/src/hu/lazy-planner.js +104 -0
- package/src/orchestrator/hu-sub-pipeline.js +36 -1
- package/src/orchestrator/pre-loop-stages.js +11 -5
- package/src/orchestrator.js +2 -1
- package/src/prompts/audit.js +35 -1
- package/src/roles/audit-role.js +19 -1
package/package.json
CHANGED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
4
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
5
|
+
|
|
6
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
7
|
+
".js", ".mjs", ".cjs", ".ts", ".mts", ".cts", ".tsx", ".jsx",
|
|
8
|
+
".py", ".rb", ".go", ".rs", ".java", ".kt", ".swift", ".c", ".cpp", ".h",
|
|
9
|
+
".vue", ".svelte", ".astro", ".php", ".cs", ".sh"
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const EXCLUDE_DIRS = new Set([
|
|
13
|
+
"node_modules", ".git", "dist", "build", "coverage", ".next",
|
|
14
|
+
".nuxt", ".output", "__pycache__", ".cache", ".parcel-cache"
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
function slugify(projectDir) {
|
|
18
|
+
return path.basename(projectDir).replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function walkSourceFiles(dir) {
|
|
22
|
+
// Use git ls-files for speed (instant vs recursive walk)
|
|
23
|
+
try {
|
|
24
|
+
const output = execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard"], { cwd: dir, encoding: "utf8", timeout: 5000 });
|
|
25
|
+
return output.trim().split("\n")
|
|
26
|
+
.filter(f => f && SOURCE_EXTENSIONS.has(path.extname(f)) && !EXCLUDE_DIRS.has(f.split("/")[0]))
|
|
27
|
+
.map(f => path.join(dir, f));
|
|
28
|
+
} catch {
|
|
29
|
+
// Fallback to recursive walk if not a git repo
|
|
30
|
+
const files = [];
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
34
|
+
} catch {
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (EXCLUDE_DIRS.has(entry.name)) continue;
|
|
39
|
+
const full = path.join(dir, entry.name);
|
|
40
|
+
if (entry.isDirectory()) {
|
|
41
|
+
files.push(...walkSourceFiles(full));
|
|
42
|
+
} else if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
|
|
43
|
+
files.push(full);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return files;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function countLines(filePath) {
|
|
51
|
+
try {
|
|
52
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
53
|
+
return content.split("\n").length;
|
|
54
|
+
} catch {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function countDependencies(projectDir) {
|
|
60
|
+
const pkgPath = path.join(projectDir, "package.json");
|
|
61
|
+
try {
|
|
62
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
63
|
+
const deps = Object.keys(pkg.dependencies || {});
|
|
64
|
+
const devDeps = Object.keys(pkg.devDependencies || {});
|
|
65
|
+
return { dependencies: deps.length, devDependencies: devDeps.length, total: deps.length + devDeps.length };
|
|
66
|
+
} catch {
|
|
67
|
+
return { dependencies: 0, devDependencies: 0, total: 0 };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function runDepcheck(projectDir) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
execFile("npx", ["depcheck", "--json"], { cwd: projectDir, timeout: 30000 }, (err, stdout) => {
|
|
74
|
+
if (err && !stdout) {
|
|
75
|
+
resolve({ unused: [], note: "depcheck not available or failed" });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const result = JSON.parse(stdout);
|
|
80
|
+
const unused = [
|
|
81
|
+
...(result.dependencies || []),
|
|
82
|
+
...(result.devDependencies || [])
|
|
83
|
+
];
|
|
84
|
+
resolve({ unused });
|
|
85
|
+
} catch {
|
|
86
|
+
resolve({ unused: [], note: "depcheck output not parseable" });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function findDeadExports(sourceFiles) {
|
|
93
|
+
const exportMap = new Map(); // file -> [exportName]
|
|
94
|
+
const importedNames = new Set();
|
|
95
|
+
|
|
96
|
+
const exportRe = /export\s+(?:default\s+)?(?:function|class|const|let|var|async\s+function)\s+(\w+)/g;
|
|
97
|
+
const namedExportRe = /export\s*\{([^}]+)\}/g;
|
|
98
|
+
const importRe = /import\s+\{([^}]+)\}\s+from/g;
|
|
99
|
+
const defaultImportRe = /import\s+(\w+)\s+from/g;
|
|
100
|
+
|
|
101
|
+
for (const file of sourceFiles) {
|
|
102
|
+
let content;
|
|
103
|
+
try {
|
|
104
|
+
content = fs.readFileSync(file, "utf8");
|
|
105
|
+
} catch {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const exports = [];
|
|
110
|
+
for (const match of content.matchAll(exportRe)) {
|
|
111
|
+
exports.push(match[1]);
|
|
112
|
+
}
|
|
113
|
+
for (const match of content.matchAll(namedExportRe)) {
|
|
114
|
+
const names = match[1].split(",").map(n => n.trim().split(/\s+as\s+/).pop().trim());
|
|
115
|
+
exports.push(...names);
|
|
116
|
+
}
|
|
117
|
+
if (exports.length > 0) {
|
|
118
|
+
exportMap.set(file, exports);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const match of content.matchAll(importRe)) {
|
|
122
|
+
const names = match[1].split(",").map(n => n.trim().split(/\s+as\s+/)[0].trim());
|
|
123
|
+
for (const name of names) importedNames.add(name);
|
|
124
|
+
}
|
|
125
|
+
for (const match of content.matchAll(defaultImportRe)) {
|
|
126
|
+
importedNames.add(match[1]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const dead = [];
|
|
131
|
+
for (const [file, exports] of exportMap) {
|
|
132
|
+
for (const name of exports) {
|
|
133
|
+
if (!importedNames.has(name)) {
|
|
134
|
+
dead.push({ file, name });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return dead;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function measureBasalCost(projectDir) {
|
|
142
|
+
const sourceFiles = walkSourceFiles(projectDir);
|
|
143
|
+
let totalLines = 0;
|
|
144
|
+
for (const f of sourceFiles) {
|
|
145
|
+
totalLines += countLines(f);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const depInfo = countDependencies(projectDir);
|
|
149
|
+
const depcheck = await runDepcheck(projectDir);
|
|
150
|
+
const deadExports = findDeadExports(sourceFiles);
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
totalLines,
|
|
154
|
+
totalFiles: sourceFiles.length,
|
|
155
|
+
dependencies: depInfo,
|
|
156
|
+
unusedDependencies: depcheck,
|
|
157
|
+
deadExports
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function auditDir() {
|
|
162
|
+
return path.join(getKarajanHome(), "audits");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function loadPreviousAudit(projectDir) {
|
|
166
|
+
const file = path.join(auditDir(), slugify(projectDir), "latest.json");
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
169
|
+
} catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function saveAuditSnapshot(projectDir, metrics) {
|
|
175
|
+
const dir = path.join(auditDir(), slugify(projectDir));
|
|
176
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
177
|
+
const snapshot = { ...metrics, timestamp: new Date().toISOString() };
|
|
178
|
+
fs.writeFileSync(path.join(dir, "latest.json"), JSON.stringify(snapshot, null, 2));
|
|
179
|
+
return snapshot;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function computeGrowthDelta(current, previous) {
|
|
183
|
+
if (!previous) return null;
|
|
184
|
+
return {
|
|
185
|
+
lines: current.totalLines - previous.totalLines,
|
|
186
|
+
files: current.totalFiles - previous.totalFiles,
|
|
187
|
+
deps: current.dependencies.total - (previous.dependencies?.total ?? 0),
|
|
188
|
+
since: previous.timestamp || null
|
|
189
|
+
};
|
|
190
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy HU Planner — refines HUs one at a time using context from completed HUs.
|
|
3
|
+
*
|
|
4
|
+
* Instead of fully planning all HUs upfront, only the first HU is decomposed
|
|
5
|
+
* with full acceptance criteria. Subsequent HUs are refined lazily before
|
|
6
|
+
* execution, incorporating learnings from previously completed HUs.
|
|
7
|
+
*/
|
|
8
|
+
import { createAgent } from "../agents/index.js";
|
|
9
|
+
import { extractFirstJson } from "../utils/json-extract.js";
|
|
10
|
+
|
|
11
|
+
const SUBAGENT_PREAMBLE = [
|
|
12
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
13
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
14
|
+
"Do NOT use any MCP tools. Focus only on refining the user story."
|
|
15
|
+
].join(" ");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build a prompt asking the AI to refine an HU based on context from completed HUs.
|
|
19
|
+
* @param {object} hu - The HU to refine (with at least id, original.text or certified.text).
|
|
20
|
+
* @param {Array<object>} completedHus - Array of completed HU results with summaries.
|
|
21
|
+
* @returns {string} The assembled refinement prompt.
|
|
22
|
+
*/
|
|
23
|
+
export function buildRefinementPrompt(hu, completedHus) {
|
|
24
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
25
|
+
|
|
26
|
+
sections.push("## HU Refinement");
|
|
27
|
+
sections.push(
|
|
28
|
+
"You are a senior product owner. Refine the following user story by adding or updating its acceptance criteria.",
|
|
29
|
+
"Use the context from previously completed HUs to make the criteria more precise and avoid redundant work."
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (completedHus.length > 0) {
|
|
33
|
+
sections.push("## Previously Completed HUs");
|
|
34
|
+
for (const completed of completedHus) {
|
|
35
|
+
const title = completed.certified?.title || completed.id;
|
|
36
|
+
const summary = completed.resultSummary || completed.certified?.text || completed.original?.text || "No summary available";
|
|
37
|
+
sections.push(`### ${completed.id} — ${title}\nResult: ${summary}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const huText = hu.certified?.text || hu.original?.text || `Implement HU ${hu.id}`;
|
|
42
|
+
sections.push(`## HU to Refine\n### ${hu.id}\n${huText}`);
|
|
43
|
+
|
|
44
|
+
sections.push(
|
|
45
|
+
"Return a single valid JSON object and nothing else.",
|
|
46
|
+
`JSON schema: {"id":string,"title":string,"text":string,"acceptanceCriteria":[string]}`,
|
|
47
|
+
"Rules:",
|
|
48
|
+
"- Keep the same HU id",
|
|
49
|
+
"- Refine the description to be more specific based on what was learned",
|
|
50
|
+
"- Add concrete, testable acceptance criteria",
|
|
51
|
+
"- Avoid repeating work already done in completed HUs",
|
|
52
|
+
"- The text field should be the full story description ready for implementation"
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return sections.join("\n\n");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Refine an HU using AI, incorporating context from completed HUs.
|
|
60
|
+
* @param {object} hu - The HU story object to refine.
|
|
61
|
+
* @param {Array<object>} completedHus - Completed HU stories with results.
|
|
62
|
+
* @param {object} config - Pipeline configuration.
|
|
63
|
+
* @param {object} logger - Logger instance.
|
|
64
|
+
* @returns {Promise<object>} Updated HU object with refined description and AC.
|
|
65
|
+
*/
|
|
66
|
+
export async function refineHuWithContext(hu, completedHus, config, logger) {
|
|
67
|
+
const provider = config?.roles?.hu_reviewer?.provider || config?.roles?.coder?.provider || "claude";
|
|
68
|
+
const prompt = buildRefinementPrompt(hu, completedHus);
|
|
69
|
+
|
|
70
|
+
logger.info(`Lazy planner: refining HU ${hu.id} with context from ${completedHus.length} completed HU(s)`);
|
|
71
|
+
|
|
72
|
+
const agent = createAgent(provider, config, logger);
|
|
73
|
+
let result;
|
|
74
|
+
try {
|
|
75
|
+
result = await agent.runTask({ prompt, role: "hu-reviewer" });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
logger.warn(`Lazy planner: refinement failed for HU ${hu.id}: ${err.message}`);
|
|
78
|
+
return hu;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!result.ok) {
|
|
82
|
+
logger.warn(`Lazy planner: refinement returned not-ok for HU ${hu.id}`);
|
|
83
|
+
return hu;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const parsed = extractFirstJson(result.output);
|
|
87
|
+
if (!parsed || !parsed.text) {
|
|
88
|
+
logger.warn(`Lazy planner: could not parse refinement output for HU ${hu.id}`);
|
|
89
|
+
return hu;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Update the HU with refined content
|
|
93
|
+
const refined = { ...hu };
|
|
94
|
+
refined.certified = {
|
|
95
|
+
...(hu.certified || {}),
|
|
96
|
+
text: parsed.text,
|
|
97
|
+
title: parsed.title || hu.certified?.title || hu.id,
|
|
98
|
+
acceptanceCriteria: parsed.acceptanceCriteria || []
|
|
99
|
+
};
|
|
100
|
+
refined.needsRefinement = false;
|
|
101
|
+
|
|
102
|
+
logger.info(`Lazy planner: HU ${hu.id} refined successfully`);
|
|
103
|
+
return refined;
|
|
104
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { topologicalSort } from "../hu/graph.js";
|
|
6
6
|
import { updateStoryStatus, loadHuBatch, saveHuBatch, HU_STATUS } from "../hu/store.js";
|
|
7
7
|
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
8
|
+
import { refineHuWithContext } from "../hu/lazy-planner.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Determine whether the HU reviewer result requires a sub-pipeline
|
|
@@ -72,7 +73,7 @@ function buildHuTask(story) {
|
|
|
72
73
|
* @param {object} params.logger - Logger instance
|
|
73
74
|
* @returns {Promise<{approved: boolean, results: object[], blockedIds: string[]}>}
|
|
74
75
|
*/
|
|
75
|
-
export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitter, eventBase, logger }) {
|
|
76
|
+
export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitter, eventBase, logger, config = null }) {
|
|
76
77
|
const batchSessionId = huReviewerResult.batchSessionId;
|
|
77
78
|
let batch;
|
|
78
79
|
try {
|
|
@@ -108,6 +109,40 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
108
109
|
continue;
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
// --- Lazy refinement: refine HU if it needs it, using completed HU context ---
|
|
113
|
+
if (story.needsRefinement && config) {
|
|
114
|
+
const completedHus = results
|
|
115
|
+
.filter(r => r.approved)
|
|
116
|
+
.map(r => {
|
|
117
|
+
const completedStory = batch.stories.find(s => s.id === r.huId);
|
|
118
|
+
return {
|
|
119
|
+
...completedStory,
|
|
120
|
+
resultSummary: r.result?.summary || r.result?.reason || null
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
emitProgress(emitter, makeEvent("hu:refine-start", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
125
|
+
message: `Refining HU ${story.id} with context from ${completedHus.length} completed HU(s)`,
|
|
126
|
+
detail: { huId: story.id, completedCount: completedHus.length }
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const refined = await refineHuWithContext(story, completedHus, config, logger);
|
|
131
|
+
// Apply refinement to the batch story in-place
|
|
132
|
+
Object.assign(story, refined);
|
|
133
|
+
story.needsRefinement = false;
|
|
134
|
+
await saveHuBatch(batchSessionId, batch);
|
|
135
|
+
|
|
136
|
+
emitProgress(emitter, makeEvent("hu:refine-end", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
137
|
+
message: `HU ${story.id} refined successfully`,
|
|
138
|
+
detail: { huId: story.id }
|
|
139
|
+
}));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
logger.warn(`Lazy refinement failed for HU ${story.id}: ${err.message} — proceeding with original`);
|
|
142
|
+
story.needsRefinement = false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
111
146
|
const huTask = buildHuTask(story);
|
|
112
147
|
|
|
113
148
|
// --- hu:start ---
|
|
@@ -646,11 +646,17 @@ export async function runHuReviewerStage({ config, logger, emitter, eventBase, s
|
|
|
646
646
|
config, logger, emitter, eventBase, session, coderRole, trackBudget
|
|
647
647
|
});
|
|
648
648
|
if (decomposed && decomposed.length > 1) {
|
|
649
|
-
stories = decomposed.map(hu =>
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
649
|
+
stories = decomposed.map((hu, idx) => {
|
|
650
|
+
const isFirst = idx === 0;
|
|
651
|
+
const fullText = `As a ${hu.role}, I want to ${hu.goal}, so that ${hu.benefit}\n\nTitle: ${hu.title}\nAcceptance Criteria:\n${hu.acceptanceCriteria.map(ac => `- ${ac}`).join("\n")}`;
|
|
652
|
+
const skeletonText = `As a ${hu.role}, I want to ${hu.goal}, so that ${hu.benefit}\n\nTitle: ${hu.title}`;
|
|
653
|
+
return {
|
|
654
|
+
id: hu.id,
|
|
655
|
+
text: isFirst ? fullText : skeletonText,
|
|
656
|
+
blocked_by: hu.dependsOn || [],
|
|
657
|
+
needsRefinement: !isFirst
|
|
658
|
+
};
|
|
659
|
+
});
|
|
654
660
|
} else {
|
|
655
661
|
stories = [{ id: "HU-AUTO-001", text: session.task }];
|
|
656
662
|
}
|
package/src/orchestrator.js
CHANGED
|
@@ -1426,7 +1426,8 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1426
1426
|
runIterationFn: async (huTask) => runIterationLoop(ctx, { task: huTask, askQuestion, emitter, logger }),
|
|
1427
1427
|
emitter,
|
|
1428
1428
|
eventBase: ctx.eventBase,
|
|
1429
|
-
logger
|
|
1429
|
+
logger,
|
|
1430
|
+
config: ctx.config
|
|
1430
1431
|
});
|
|
1431
1432
|
|
|
1432
1433
|
emitProgress(emitter, makeEvent("hu:sub-pipeline:end", { ...ctx.eventBase, stage: "hu-sub-pipeline" }, {
|
package/src/prompts/audit.js
CHANGED
|
@@ -14,7 +14,7 @@ const VALID_SCORES = new Set(["A", "B", "C", "D", "F"]);
|
|
|
14
14
|
const VALID_SEVERITIES = new Set(["critical", "high", "medium", "low"]);
|
|
15
15
|
const VALID_IMPACT = new Set(["high", "medium", "low"]);
|
|
16
16
|
|
|
17
|
-
export function buildAuditPrompt({ task, instructions, dimensions = null, context = null }) {
|
|
17
|
+
export function buildAuditPrompt({ task, instructions, dimensions = null, context = null, basalCost = null, growthDelta = null }) {
|
|
18
18
|
const sections = [SUBAGENT_PREAMBLE];
|
|
19
19
|
|
|
20
20
|
if (instructions) {
|
|
@@ -112,6 +112,40 @@ export function buildAuditPrompt({ task, instructions, dimensions = null, contex
|
|
|
112
112
|
`Only include dimensions you were asked to analyze: ${activeDimensions.join(", ")}`
|
|
113
113
|
);
|
|
114
114
|
|
|
115
|
+
if (basalCost) {
|
|
116
|
+
const lines = [
|
|
117
|
+
"## Basal Cost",
|
|
118
|
+
`- Total source lines: ${basalCost.totalLines}`,
|
|
119
|
+
`- Total source files: ${basalCost.totalFiles}`,
|
|
120
|
+
`- Dependencies: ${basalCost.dependencies?.total ?? 0} (${basalCost.dependencies?.dependencies ?? 0} prod + ${basalCost.dependencies?.devDependencies ?? 0} dev)`
|
|
121
|
+
];
|
|
122
|
+
if (basalCost.unusedDependencies?.unused?.length > 0) {
|
|
123
|
+
lines.push(`- Unused dependencies: ${basalCost.unusedDependencies.unused.join(", ")}`);
|
|
124
|
+
} else if (basalCost.unusedDependencies?.note) {
|
|
125
|
+
lines.push(`- Unused dependencies: ${basalCost.unusedDependencies.note}`);
|
|
126
|
+
}
|
|
127
|
+
if (basalCost.deadExports?.length > 0) {
|
|
128
|
+
lines.push(`- Dead exports (exported but never imported): ${basalCost.deadExports.length}`);
|
|
129
|
+
for (const de of basalCost.deadExports.slice(0, 20)) {
|
|
130
|
+
lines.push(` - ${de.name} in ${de.file}`);
|
|
131
|
+
}
|
|
132
|
+
if (basalCost.deadExports.length > 20) {
|
|
133
|
+
lines.push(` - ... and ${basalCost.deadExports.length - 20} more`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (growthDelta) {
|
|
137
|
+
lines.push("");
|
|
138
|
+
lines.push("### Growth since last audit");
|
|
139
|
+
lines.push(`- Lines: ${growthDelta.lines >= 0 ? "+" : ""}${growthDelta.lines}`);
|
|
140
|
+
lines.push(`- Files: ${growthDelta.files >= 0 ? "+" : ""}${growthDelta.files}`);
|
|
141
|
+
lines.push(`- Dependencies: ${growthDelta.deps >= 0 ? "+" : ""}${growthDelta.deps}`);
|
|
142
|
+
if (growthDelta.since) lines.push(`- Since: ${growthDelta.since}`);
|
|
143
|
+
}
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push("Flag if basal cost is growing faster than feature delivery. Recommend elimination of unused code and dependencies.");
|
|
146
|
+
sections.push(lines.join("\n"));
|
|
147
|
+
}
|
|
148
|
+
|
|
115
149
|
if (context) {
|
|
116
150
|
sections.push(`## Context\n${context}`);
|
|
117
151
|
}
|
package/src/roles/audit-role.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BaseRole } from "./base-role.js";
|
|
2
2
|
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
3
3
|
import { buildAuditPrompt, parseAuditOutput, AUDIT_DIMENSIONS } from "../prompts/audit.js";
|
|
4
|
+
import { measureBasalCost, loadPreviousAudit, saveAuditSnapshot, computeGrowthDelta } from "../audit/basal-cost.js";
|
|
4
5
|
|
|
5
6
|
function resolveProvider(config) {
|
|
6
7
|
return (
|
|
@@ -57,7 +58,18 @@ export class AuditRole extends BaseRole {
|
|
|
57
58
|
const provider = resolveProvider(this.config);
|
|
58
59
|
const agent = this._createAgent(provider, this.config, this.logger);
|
|
59
60
|
|
|
60
|
-
const
|
|
61
|
+
const projectDir = this.config?.projectDir || process.cwd();
|
|
62
|
+
let basalCost = null;
|
|
63
|
+
let growthDelta = null;
|
|
64
|
+
try {
|
|
65
|
+
basalCost = await measureBasalCost(projectDir);
|
|
66
|
+
const previous = loadPreviousAudit(projectDir);
|
|
67
|
+
growthDelta = computeGrowthDelta(basalCost, previous);
|
|
68
|
+
} catch {
|
|
69
|
+
// basal cost is best-effort, don't fail the audit
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const prompt = buildAuditPrompt({ task, instructions: this.instructions, dimensions, context, basalCost, growthDelta });
|
|
61
73
|
const runArgs = { prompt, role: "audit" };
|
|
62
74
|
if (onOutput) runArgs.onOutput = onOutput;
|
|
63
75
|
const result = await agent.runTask(runArgs);
|
|
@@ -82,12 +94,18 @@ export class AuditRole extends BaseRole {
|
|
|
82
94
|
};
|
|
83
95
|
}
|
|
84
96
|
|
|
97
|
+
if (basalCost) {
|
|
98
|
+
try { saveAuditSnapshot(projectDir, basalCost); } catch { /* best-effort */ }
|
|
99
|
+
}
|
|
100
|
+
|
|
85
101
|
return {
|
|
86
102
|
ok: true,
|
|
87
103
|
result: {
|
|
88
104
|
summary: parsed.summary,
|
|
89
105
|
dimensions: parsed.dimensions,
|
|
90
106
|
topRecommendations: parsed.topRecommendations,
|
|
107
|
+
basalCost: basalCost || undefined,
|
|
108
|
+
growthDelta: growthDelta || undefined,
|
|
91
109
|
provider
|
|
92
110
|
},
|
|
93
111
|
summary: buildSummary(parsed),
|