karajan-code 1.59.1 → 2.0.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/README.md +2 -0
- package/package.json +1 -1
- package/scripts/install.js +18 -15
- package/src/agents/aider-agent.js +0 -7
- package/src/agents/claude-agent.js +1 -4
- package/src/agents/codex-agent.js +2 -6
- package/src/agents/gemini-agent.js +0 -4
- package/src/agents/opencode-agent.js +0 -4
- package/src/{becaria → ci}/dispatch.js +13 -13
- package/src/{becaria → ci}/pr-diff.js +1 -1
- package/src/cli.js +2 -2
- package/src/commands/agents.js +0 -1
- package/src/commands/doctor.js +22 -40
- package/src/commands/init.js +8 -8
- package/src/commands/report.js +0 -13
- package/src/commands/review.js +17 -17
- package/src/commands/run.js +1 -2
- package/src/commands/status.js +1 -7
- package/src/config.js +10 -52
- package/src/git/automation.js +9 -9
- package/src/guards/output-guard.js +0 -17
- package/src/mcp/handlers/direct-handlers.js +1 -1
- package/src/mcp/handlers/run-handler.js +1 -2
- package/src/mcp/sovereignty-guard.js +1 -1
- package/src/mcp/tools.js +1 -1
- package/src/orchestrator/brain-coordinator.js +165 -0
- package/src/orchestrator/ci-integration.js +139 -0
- package/src/orchestrator/config-init.js +435 -0
- package/src/orchestrator/direct-actions.js +186 -0
- package/src/orchestrator/feedback-enrichment.js +140 -0
- package/src/orchestrator/feedback-queue.js +152 -0
- package/src/orchestrator/flow-control.js +159 -0
- package/src/orchestrator/post-loop-stages.js +20 -15
- package/src/orchestrator/preflight-checks.js +7 -3
- package/src/orchestrator/role-output-compressor.js +190 -0
- package/src/orchestrator/session-journal.js +344 -0
- package/src/orchestrator/solomon-escalation.js +38 -5
- package/src/orchestrator/solomon-rules.js +32 -33
- package/src/orchestrator/stages/reviewer-stage.js +16 -5
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +428 -664
- package/src/planning-game/client.js +2 -2
- package/src/planning-game/pipeline-adapter.js +1 -1
- package/src/prompts/coder.js +17 -3
- package/src/prompts/reviewer.js +2 -2
- package/src/prompts/rtk-snippet.js +3 -5
- package/src/review/diff-generator.js +66 -17
- package/src/review/snapshot-diff.js +122 -0
- package/src/review/tdd-policy.js +4 -2
- package/src/roles/agent-role.js +135 -0
- package/src/roles/architect-role.js +61 -108
- package/src/roles/audit-role.js +25 -71
- package/src/roles/coder-role.js +32 -57
- package/src/roles/discover-role.js +30 -71
- package/src/roles/hu-reviewer-role.js +51 -99
- package/src/roles/impeccable-role.js +33 -88
- package/src/roles/index.js +2 -0
- package/src/roles/karajan-brain-role.js +134 -0
- package/src/roles/planner-role.js +40 -75
- package/src/roles/refactorer-role.js +22 -64
- package/src/roles/researcher-role.js +51 -115
- package/src/roles/reviewer-role.js +68 -125
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +69 -11
- package/src/roles/tester-role.js +121 -95
- package/src/roles/triage-role.js +50 -111
- package/src/session-store.js +1 -1
- package/src/skills/skill-detector.js +11 -0
- package/src/sonar/config-resolver.js +20 -1
- package/src/sonar/credentials.js +17 -0
- package/src/sonar/project-key.js +1 -1
- package/src/sonar/scanner.js +1 -1
- package/src/utils/display.js +33 -14
- package/src/utils/injection-guard.js +23 -8
- package/src/utils/logger.js +6 -1
- package/src/utils/process.js +3 -4
- package/src/utils/role-assigner.js +131 -0
- package/src/utils/update-check.js +5 -1
- package/src/webperf/devtools-detect.js +70 -23
- package/templates/roles/coder.md +7 -0
- package/templates/roles/karajan-brain.md +95 -0
- package/templates/roles/solomon.md +67 -106
- package/templates/workflows/automerge.yml +4 -4
- package/templates/workflows/houston-override.yml +4 -4
- package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
- package/src/proxy/adapters/anthropic.js +0 -131
- package/src/proxy/adapters/gemini.js +0 -106
- package/src/proxy/adapters/index.js +0 -51
- package/src/proxy/adapters/openai.js +0 -71
- package/src/proxy/compression/ai-compressor.js +0 -209
- package/src/proxy/compression/dedup-cache.js +0 -100
- package/src/proxy/compression/deterministic/bash-build.js +0 -60
- package/src/proxy/compression/deterministic/bash-git.js +0 -101
- package/src/proxy/compression/deterministic/bash-infra.js +0 -64
- package/src/proxy/compression/deterministic/bash-misc.js +0 -53
- package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
- package/src/proxy/compression/deterministic/bash-test.js +0 -61
- package/src/proxy/compression/deterministic/glob.js +0 -26
- package/src/proxy/compression/deterministic/grep.js +0 -53
- package/src/proxy/compression/deterministic/index.js +0 -58
- package/src/proxy/compression/deterministic/read.js +0 -22
- package/src/proxy/compression/deterministic/utils.js +0 -33
- package/src/proxy/compression/pipeline.js +0 -270
- package/src/proxy/middleware/prompt-injector.js +0 -165
- package/src/proxy/middleware/response-interceptor.js +0 -223
- package/src/proxy/progress-monitor.js +0 -98
- package/src/proxy/proxy-lifecycle.js +0 -201
- package/src/proxy/proxy-server.js +0 -239
- package/src/proxy/security.js +0 -45
- /package/src/{becaria → ci}/index.js +0 -0
- /package/src/{becaria → ci}/repo.js +0 -0
|
@@ -65,11 +65,11 @@ export async function fetchCard({ projectId, cardId, timeoutMs = DEFAULT_TIMEOUT
|
|
|
65
65
|
return data?.card || data;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
async function getCard({ projectId, cardId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
69
69
|
return fetchCard({ projectId, cardId, timeoutMs });
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
async function listCards({ projectId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
73
73
|
const url = `${getApiUrl()}/projects/${encodeURIComponent(projectId)}/cards`;
|
|
74
74
|
const response = await fetchWithRetry(url, {}, timeoutMs);
|
|
75
75
|
const data = await parseJsonResponse(response);
|
|
@@ -188,7 +188,7 @@ export function accumulateCommit(session, commitInfo) {
|
|
|
188
188
|
hash: commitInfo.hash,
|
|
189
189
|
message: commitInfo.message || "",
|
|
190
190
|
date: commitInfo.date || new Date().toISOString(),
|
|
191
|
-
author: commitInfo.author || "
|
|
191
|
+
author: commitInfo.author || "Karajan"
|
|
192
192
|
});
|
|
193
193
|
}
|
|
194
194
|
|
package/src/prompts/coder.js
CHANGED
|
@@ -33,7 +33,7 @@ const SERENA_INSTRUCTIONS = [
|
|
|
33
33
|
"Fall back to reading files only when Serena tools are not sufficient."
|
|
34
34
|
].join("\n");
|
|
35
35
|
|
|
36
|
-
export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false,
|
|
36
|
+
export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, domainContext = null, plan = null, projectDir = null, language = "en" }) {
|
|
37
37
|
const langInstruction = getLanguageInstruction(language);
|
|
38
38
|
const sections = [
|
|
39
39
|
serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
|
|
@@ -41,6 +41,10 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
|
|
|
41
41
|
`Task:\n${task}`,
|
|
42
42
|
"Implement directly in the repository.",
|
|
43
43
|
"Keep changes minimal and production-ready.",
|
|
44
|
+
"Follow SOLID principles. Write small, focused functions (< 30 lines).",
|
|
45
|
+
"Make atomic commits: 1 logical change = 1 commit. Keep PRs small and reviewable.",
|
|
46
|
+
"Security: use httpOnly cookies for auth tokens, validate all input, parameterize queries, never expose secrets.",
|
|
47
|
+
"No console.log — use a structured logger. No 'any' types — use JSDoc annotations.",
|
|
44
48
|
SUBPROCESS_CONSTRAINTS
|
|
45
49
|
];
|
|
46
50
|
|
|
@@ -48,7 +52,7 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
|
|
|
48
52
|
sections.push(SERENA_INSTRUCTIONS);
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
const rtkSnippet = buildRtkInstructions({ rtkAvailable
|
|
55
|
+
const rtkSnippet = buildRtkInstructions({ rtkAvailable });
|
|
52
56
|
if (rtkSnippet) {
|
|
53
57
|
sections.push(rtkSnippet);
|
|
54
58
|
}
|
|
@@ -85,7 +89,17 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
if (reviewerFeedback) {
|
|
88
|
-
sections.push(
|
|
92
|
+
sections.push(
|
|
93
|
+
`## Reviewer blocking feedback — YOU MUST FIX THESE BEFORE PROCEEDING`,
|
|
94
|
+
reviewerFeedback,
|
|
95
|
+
"",
|
|
96
|
+
"For each issue above:",
|
|
97
|
+
"1. Find the relevant file(s) in the project",
|
|
98
|
+
"2. Make the specific code change needed to resolve the issue",
|
|
99
|
+
"3. If tests are missing, write them",
|
|
100
|
+
"4. If dependencies are needed, install them (npm install)",
|
|
101
|
+
"5. Do NOT skip any issue — all must be resolved"
|
|
102
|
+
);
|
|
89
103
|
}
|
|
90
104
|
|
|
91
105
|
if (deferredContext) {
|
package/src/prompts/reviewer.js
CHANGED
|
@@ -24,7 +24,7 @@ const SERENA_INSTRUCTIONS = [
|
|
|
24
24
|
"Fall back to reading files only when Serena tools are not sufficient."
|
|
25
25
|
].join("\n");
|
|
26
26
|
|
|
27
|
-
export async function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false,
|
|
27
|
+
export async function buildReviewerPrompt({ task, diff, reviewRules, mode, serenaEnabled = false, rtkAvailable = false, productContext = null, domainContext = null, projectDir = null, language = "en" }) {
|
|
28
28
|
const truncatedDiff = diff.length > 12000 ? `${diff.slice(0, 12000)}\n\n[TRUNCATED]` : diff;
|
|
29
29
|
|
|
30
30
|
const langInstruction = getLanguageInstruction(language);
|
|
@@ -43,7 +43,7 @@ export async function buildReviewerPrompt({ task, diff, reviewRules, mode, seren
|
|
|
43
43
|
sections.push(SERENA_INSTRUCTIONS);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const rtkSnippet = buildRtkInstructions({ rtkAvailable
|
|
46
|
+
const rtkSnippet = buildRtkInstructions({ rtkAvailable });
|
|
47
47
|
if (rtkSnippet) {
|
|
48
48
|
sections.push(rtkSnippet);
|
|
49
49
|
}
|
|
@@ -15,13 +15,11 @@ export const RTK_INSTRUCTIONS = [
|
|
|
15
15
|
].join("\n");
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Build RTK instructions
|
|
19
|
-
*
|
|
20
|
-
* @param {{ rtkAvailable?: boolean, proxyEnabled?: boolean }} options
|
|
18
|
+
* Build RTK instructions when RTK is detected.
|
|
19
|
+
* @param {{ rtkAvailable?: boolean }} options
|
|
21
20
|
* @returns {string}
|
|
22
21
|
*/
|
|
23
|
-
export function buildRtkInstructions({ rtkAvailable = false
|
|
24
|
-
if (proxyEnabled) return "";
|
|
22
|
+
export function buildRtkInstructions({ rtkAvailable = false } = {}) {
|
|
25
23
|
if (!rtkAvailable) return "";
|
|
26
24
|
return RTK_INSTRUCTIONS;
|
|
27
25
|
}
|
|
@@ -1,8 +1,33 @@
|
|
|
1
1
|
import { runCommand } from "../utils/process.js";
|
|
2
|
+
import { generateSnapshotDiff } from "./snapshot-diff.js";
|
|
2
3
|
|
|
3
4
|
/** @type {((command: string, args?: string[], options?: object) => Promise<object>)|null} */
|
|
4
5
|
let _runner = null;
|
|
5
6
|
|
|
7
|
+
/** @type {Map<string, string>|null} — pre-coder filesystem snapshot for git-free diff */
|
|
8
|
+
let _snapshot = null;
|
|
9
|
+
|
|
10
|
+
/** @type {string|null} — project directory scope for git diff (prevents leaking unrelated changes) */
|
|
11
|
+
let _projectDir = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Set the project directory scope. When set, all diffs are scoped to this directory.
|
|
15
|
+
* Call once at pipeline start when projectDir differs from repo root.
|
|
16
|
+
* @param {string|null} dir
|
|
17
|
+
*/
|
|
18
|
+
export function setProjectDir(dir) {
|
|
19
|
+
_projectDir = dir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Store a filesystem snapshot for git-free diff fallback.
|
|
24
|
+
* Call before the coder runs. If git is available, this is unused.
|
|
25
|
+
* @param {Map<string, string>} snapshot
|
|
26
|
+
*/
|
|
27
|
+
export function setSnapshot(snapshot) {
|
|
28
|
+
_snapshot = snapshot;
|
|
29
|
+
}
|
|
30
|
+
|
|
6
31
|
/**
|
|
7
32
|
* Inject an RTK-aware runner so all git operations in this module benefit from token savings.
|
|
8
33
|
* Call once at pipeline start when RTK is available.
|
|
@@ -18,29 +43,53 @@ function run(command, args, ...rest) {
|
|
|
18
43
|
|
|
19
44
|
export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
|
|
20
45
|
if (baseRef) return baseRef;
|
|
21
|
-
const mergeBase = await run("git", ["merge-base", "HEAD", `origin/${baseBranch}`]);
|
|
22
|
-
if (mergeBase.exitCode === 0) return mergeBase.stdout.trim();
|
|
23
46
|
|
|
24
|
-
|
|
25
|
-
|
|
47
|
+
try {
|
|
48
|
+
const mergeBase = await run("git", ["merge-base", "HEAD", `origin/${baseBranch}`]);
|
|
49
|
+
if (mergeBase.exitCode === 0) return mergeBase.stdout.trim();
|
|
26
50
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (emptyTree.exitCode === 0) return emptyTree.stdout.trim();
|
|
51
|
+
const fallback = await run("git", ["rev-parse", "HEAD~1"]);
|
|
52
|
+
if (fallback.exitCode === 0) return fallback.stdout.trim();
|
|
30
53
|
|
|
31
|
-
|
|
54
|
+
// Empty repo (zero commits): diff against the empty tree
|
|
55
|
+
const emptyTree = await run("git", ["hash-object", "-t", "tree", "/dev/null"]);
|
|
56
|
+
if (emptyTree.exitCode === 0) return emptyTree.stdout.trim();
|
|
57
|
+
} catch { /* git not available */ }
|
|
58
|
+
|
|
59
|
+
// No git or all strategies failed — return sentinel for snapshot-based diff
|
|
60
|
+
return "__snapshot__";
|
|
32
61
|
}
|
|
33
62
|
|
|
34
|
-
export async function generateDiff({ baseRef, stageNewFiles = false }) {
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
63
|
+
export async function generateDiff({ baseRef, stageNewFiles = false, projectDir = null }) {
|
|
64
|
+
// Auto-resolve projectDir from config if not passed explicitly
|
|
65
|
+
const scopeDir = projectDir || _projectDir || null;
|
|
66
|
+
|
|
67
|
+
// Try git diff first
|
|
68
|
+
try {
|
|
69
|
+
if (stageNewFiles) {
|
|
70
|
+
const addArgs = scopeDir ? ["-A", scopeDir] : ["-A"];
|
|
71
|
+
await run("git", ["add", ...addArgs]);
|
|
72
|
+
}
|
|
73
|
+
const diffArgs = ["diff", stageNewFiles ? "--cached" : "", `${baseRef}`].filter(Boolean);
|
|
74
|
+
// Scope diff to projectDir when it's a subdirectory (prevents leaking unrelated changes)
|
|
75
|
+
if (scopeDir) diffArgs.push("--", scopeDir);
|
|
76
|
+
const result = await run("git", diffArgs);
|
|
77
|
+
if (result.exitCode === 0) {
|
|
78
|
+
return result.stdout;
|
|
79
|
+
}
|
|
80
|
+
} catch { /* git not available or failed */ }
|
|
81
|
+
|
|
82
|
+
// Fallback: snapshot-based diff (no git needed)
|
|
83
|
+
if (_snapshot) {
|
|
84
|
+
const dir = projectDir || process.cwd();
|
|
85
|
+
return generateSnapshotDiff(_snapshot, null, dir);
|
|
42
86
|
}
|
|
43
|
-
|
|
87
|
+
|
|
88
|
+
// No git and no snapshot — generate diff of all files as new
|
|
89
|
+
const dir = projectDir || process.cwd();
|
|
90
|
+
const { takeSnapshot } = await import("./snapshot-diff.js");
|
|
91
|
+
const emptySnapshot = new Map();
|
|
92
|
+
return generateSnapshotDiff(emptySnapshot, null, dir);
|
|
44
93
|
}
|
|
45
94
|
|
|
46
95
|
export async function getUntrackedFiles() {
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-free diff generation using filesystem snapshots.
|
|
3
|
+
*
|
|
4
|
+
* Takes a snapshot of project files (path → content hash), then compares
|
|
5
|
+
* a later snapshot to generate a unified diff. Works on any OS, no git needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
9
|
+
import { join, relative } from "node:path";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
const DEFAULT_IGNORE = new Set([
|
|
13
|
+
"node_modules", ".git", ".kj", ".karajan", "dist", "build",
|
|
14
|
+
".next", ".nuxt", ".svelte-kit", "__pycache__", ".venv",
|
|
15
|
+
"coverage", ".nyc_output", ".reviews"
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
const MAX_FILE_SIZE = 512 * 1024; // 512KB — skip large files
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Scan a directory recursively and return a Map of relative path → content hash.
|
|
22
|
+
* @param {string} rootDir
|
|
23
|
+
* @returns {Promise<Map<string, string>>} path → sha256 hash
|
|
24
|
+
*/
|
|
25
|
+
export async function takeSnapshot(rootDir) {
|
|
26
|
+
const snapshot = new Map();
|
|
27
|
+
await scanDir(rootDir, rootDir, snapshot);
|
|
28
|
+
return snapshot;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function scanDir(dir, rootDir, snapshot) {
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
35
|
+
} catch { return; }
|
|
36
|
+
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (DEFAULT_IGNORE.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
39
|
+
|
|
40
|
+
const fullPath = join(dir, entry.name);
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
await scanDir(fullPath, rootDir, snapshot);
|
|
43
|
+
} else if (entry.isFile()) {
|
|
44
|
+
try {
|
|
45
|
+
const s = await stat(fullPath);
|
|
46
|
+
if (s.size > MAX_FILE_SIZE) continue;
|
|
47
|
+
const content = await readFile(fullPath);
|
|
48
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
49
|
+
snapshot.set(relative(rootDir, fullPath), hash);
|
|
50
|
+
} catch { /* skip unreadable files */ }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compare two snapshots and generate a unified diff string.
|
|
57
|
+
* @param {Map<string, string>} before — snapshot before coder
|
|
58
|
+
* @param {Map<string, string>|null} after — snapshot after coder (null = read from disk)
|
|
59
|
+
* @param {string} rootDir — project root (for reading file contents)
|
|
60
|
+
* @returns {Promise<string>} unified diff text
|
|
61
|
+
*/
|
|
62
|
+
export async function generateSnapshotDiff(before, after, rootDir) {
|
|
63
|
+
if (!after) {
|
|
64
|
+
after = await takeSnapshot(rootDir);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const lines = [];
|
|
68
|
+
|
|
69
|
+
// New files (in after but not before)
|
|
70
|
+
for (const [path, hash] of after) {
|
|
71
|
+
if (!before.has(path)) {
|
|
72
|
+
const content = await safeRead(join(rootDir, path));
|
|
73
|
+
if (content !== null) {
|
|
74
|
+
lines.push(`diff --snapshot a/${path} b/${path}`);
|
|
75
|
+
lines.push("new file");
|
|
76
|
+
lines.push(`--- /dev/null`);
|
|
77
|
+
lines.push(`+++ b/${path}`);
|
|
78
|
+
for (const line of content.split("\n")) {
|
|
79
|
+
lines.push(`+${line}`);
|
|
80
|
+
}
|
|
81
|
+
lines.push("");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Modified files (in both, different hash)
|
|
87
|
+
for (const [path, hash] of after) {
|
|
88
|
+
if (before.has(path) && before.get(path) !== hash) {
|
|
89
|
+
const content = await safeRead(join(rootDir, path));
|
|
90
|
+
if (content !== null) {
|
|
91
|
+
lines.push(`diff --snapshot a/${path} b/${path}`);
|
|
92
|
+
lines.push("modified file");
|
|
93
|
+
lines.push(`--- a/${path}`);
|
|
94
|
+
lines.push(`+++ b/${path}`);
|
|
95
|
+
// Show full new content (we don't have the old content)
|
|
96
|
+
for (const line of content.split("\n")) {
|
|
97
|
+
lines.push(`+${line}`);
|
|
98
|
+
}
|
|
99
|
+
lines.push("");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Deleted files (in before but not after)
|
|
105
|
+
for (const [path] of before) {
|
|
106
|
+
if (!after.has(path)) {
|
|
107
|
+
lines.push(`diff --snapshot a/${path} b/${path}`);
|
|
108
|
+
lines.push("deleted file");
|
|
109
|
+
lines.push(`--- a/${path}`);
|
|
110
|
+
lines.push(`+++ /dev/null`);
|
|
111
|
+
lines.push("");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function safeRead(filePath) {
|
|
119
|
+
try {
|
|
120
|
+
return await readFile(filePath, "utf-8");
|
|
121
|
+
} catch { return null; }
|
|
122
|
+
}
|
package/src/review/tdd-policy.js
CHANGED
|
@@ -2,7 +2,8 @@ function extractChangedFiles(diff) {
|
|
|
2
2
|
const files = new Set();
|
|
3
3
|
const lines = String(diff || "").split("\n");
|
|
4
4
|
for (const line of lines) {
|
|
5
|
-
|
|
5
|
+
// Support both git diff and snapshot diff formats
|
|
6
|
+
if (!line.startsWith("diff --git ") && !line.startsWith("diff --snapshot ")) continue;
|
|
6
7
|
const parts = line.split(" ");
|
|
7
8
|
const b = parts[3] || "";
|
|
8
9
|
if (b.startsWith("b/")) files.add(b.slice(2));
|
|
@@ -49,7 +50,8 @@ export function evaluateTddPolicy(diff, developmentConfig = {}, untrackedFiles =
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
if (sourceFiles.length === 0) {
|
|
52
|
-
|
|
53
|
+
const reason = files.length === 0 ? "no_diff_available" : "no_source_changes";
|
|
54
|
+
return { ok: true, reason, sourceFiles, testFiles };
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
if (testFiles.length === 0) {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for LLM-backed roles.
|
|
3
|
+
* Handles the common execute() flow: resolve provider → create agent → call LLM → parse output.
|
|
4
|
+
* Subclasses override hooks (buildPrompt, parseOutput, buildSuccessResult, buildSummary)
|
|
5
|
+
* instead of reimplementing the full execute() boilerplate.
|
|
6
|
+
*/
|
|
7
|
+
import { BaseRole } from "./base-role.js";
|
|
8
|
+
import { createAgent as defaultCreateAgent } from "../agents/index.js";
|
|
9
|
+
|
|
10
|
+
export class AgentRole extends BaseRole {
|
|
11
|
+
constructor({ name, config, logger, emitter = null, createAgentFn = null }) {
|
|
12
|
+
super({ name, config, logger, emitter });
|
|
13
|
+
this._createAgent = createAgentFn || defaultCreateAgent;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Provider fallback chain. Override for roles with non-standard chains. */
|
|
17
|
+
resolveProvider() {
|
|
18
|
+
return (
|
|
19
|
+
this.config?.roles?.[this.name]?.provider ||
|
|
20
|
+
this.config?.roles?.coder?.provider ||
|
|
21
|
+
"claude"
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Agent method to call. Override with "reviewTask" for reviewer. */
|
|
26
|
+
get agentMethod() { return "runTask"; }
|
|
27
|
+
|
|
28
|
+
/** Create the agent. Override for special logic (e.g. HostAgent in coder). */
|
|
29
|
+
createAgentInstance(provider) {
|
|
30
|
+
return this._createAgent(provider, this.config, this.logger);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the prompt for this role. MUST be overridden.
|
|
35
|
+
* @param {*} input - raw input from pipeline
|
|
36
|
+
* @returns {Promise<{prompt: string, onOutput?: Function}>|{prompt: string, onOutput?: Function}|string}
|
|
37
|
+
*/
|
|
38
|
+
async buildPrompt(_input) {
|
|
39
|
+
throw new Error(`${this.name}: buildPrompt() not implemented`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse raw LLM output into structured data. Return null to trigger handleParseNull.
|
|
44
|
+
* @param {string} raw - raw agent output
|
|
45
|
+
* @returns {*} parsed result
|
|
46
|
+
*/
|
|
47
|
+
parseOutput(raw) { return raw; }
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build the result object for a successful parse.
|
|
51
|
+
* @param {*} parsed - output of parseOutput
|
|
52
|
+
* @param {string} provider
|
|
53
|
+
* @param {object} agentResult - full agent result (for accessing .usage, .output, etc.)
|
|
54
|
+
* @returns {object} result to include in { ok, result, summary, usage }
|
|
55
|
+
*/
|
|
56
|
+
buildSuccessResult(parsed, provider, _agentResult) { return { ...parsed, provider }; }
|
|
57
|
+
|
|
58
|
+
/** Build human-readable summary from parsed output. */
|
|
59
|
+
buildSummary(_parsed) { return `${this.name} completed`; }
|
|
60
|
+
|
|
61
|
+
/** Determine ok status from parsed result. Default: true. Override for verdict-based roles. */
|
|
62
|
+
isSuccessful(_parsed) { return true; }
|
|
63
|
+
|
|
64
|
+
/** Handle null parse (no JSON found). Default returns error. Override for lenient roles. */
|
|
65
|
+
handleParseNull(agentResult, provider) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
result: { error: `Failed to parse ${this.name} output: no JSON found`, provider },
|
|
69
|
+
summary: `${this.name} output parse error: no JSON found`,
|
|
70
|
+
usage: agentResult.usage
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Handle parse exception. Default returns error. Override for lenient roles. */
|
|
75
|
+
handleParseError(err, agentResult, provider) {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
result: { error: `Failed to parse ${this.name} output: ${err.message}`, provider },
|
|
79
|
+
summary: `${this.name} output parse error: ${err.message}`,
|
|
80
|
+
usage: agentResult.usage
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Extract input fields from raw input. Default handles string-or-object.
|
|
86
|
+
* Override for roles needing extra fields.
|
|
87
|
+
*/
|
|
88
|
+
extractInput(input) {
|
|
89
|
+
if (typeof input === "string") return { task: input, onOutput: null };
|
|
90
|
+
return {
|
|
91
|
+
task: input?.task || this.context?.task || "",
|
|
92
|
+
onOutput: input?.onOutput || null,
|
|
93
|
+
...input
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async execute(input) {
|
|
98
|
+
const extracted = this.extractInput(input);
|
|
99
|
+
const provider = this.resolveProvider();
|
|
100
|
+
const agent = this.createAgentInstance(provider);
|
|
101
|
+
|
|
102
|
+
const promptResult = await this.buildPrompt(extracted);
|
|
103
|
+
const { prompt, onOutput } = typeof promptResult === "string"
|
|
104
|
+
? { prompt: promptResult, onOutput: extracted.onOutput }
|
|
105
|
+
: { onOutput: extracted.onOutput, ...promptResult };
|
|
106
|
+
|
|
107
|
+
const runArgs = { prompt, role: this.name };
|
|
108
|
+
if (onOutput) runArgs.onOutput = onOutput;
|
|
109
|
+
|
|
110
|
+
const result = await agent[this.agentMethod](runArgs);
|
|
111
|
+
|
|
112
|
+
if (!result.ok) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
result: { error: result.error || result.output || `${this.name} failed`, provider },
|
|
116
|
+
summary: `${this.name} failed: ${result.error || "unknown error"}`,
|
|
117
|
+
usage: result.usage
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const parsed = this.parseOutput(result.output);
|
|
123
|
+
if (parsed === null) return this.handleParseNull(result, provider);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
ok: this.isSuccessful(parsed),
|
|
127
|
+
result: this.buildSuccessResult(parsed, provider, result),
|
|
128
|
+
summary: this.buildSummary(parsed),
|
|
129
|
+
usage: result.usage
|
|
130
|
+
};
|
|
131
|
+
} catch (err) {
|
|
132
|
+
return this.handleParseError(err, result, provider);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|