karajan-code 1.2.2 → 1.2.3
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 -2
- package/docs/README.es.md +2 -2
- package/package.json +1 -1
- package/src/agents/availability.js +3 -9
- package/src/agents/index.js +32 -11
- package/src/agents/model-registry.js +62 -0
- package/src/mcp/orphan-guard.js +21 -0
- package/src/mcp/server.js +4 -0
- package/src/orchestrator/iteration-stages.js +404 -0
- package/src/orchestrator/post-loop-stages.js +141 -0
- package/src/orchestrator/pre-loop-stages.js +149 -0
- package/src/orchestrator/reviewer-fallback.js +39 -0
- package/src/orchestrator/solomon-escalation.js +84 -0
- package/src/orchestrator.js +80 -883
- package/src/prompts/planner.js +51 -0
- package/src/repeat-detector.js +11 -0
- package/src/roles/coder-role.js +4 -1
- package/src/roles/planner-role.js +2 -2
- package/src/roles/refactorer-role.js +2 -0
- package/src/roles/reviewer-role.js +13 -6
- package/src/utils/budget.js +30 -0
- package/src/utils/pricing.js +3 -13
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://www.npmjs.com/package/karajan-code"><img src="https://img.shields.io/npm/v/karajan-code.svg" alt="npm version"></a>
|
|
13
13
|
<a href="https://github.com/manufosela/karajan-code/actions"><img src="https://github.com/manufosela/karajan-code/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
14
|
-
<a href="https://
|
|
14
|
+
<a href="https://www.gnu.org/licenses/agpl-3.0"><img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="License"></a>
|
|
15
15
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg" alt="Node.js"></a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
@@ -441,5 +441,5 @@ npm run validate # Lint + test
|
|
|
441
441
|
|
|
442
442
|
- [Changelog](CHANGELOG.md)
|
|
443
443
|
- [Security Policy](SECURITY.md)
|
|
444
|
-
- [License (
|
|
444
|
+
- [License (AGPL-3.0)](LICENSE)
|
|
445
445
|
- [Issues](https://github.com/manufosela/karajan-code/issues)
|
package/docs/README.es.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://www.npmjs.com/package/karajan-code"><img src="https://img.shields.io/npm/v/karajan-code.svg" alt="npm version"></a>
|
|
13
13
|
<a href="https://github.com/manufosela/karajan-code/actions"><img src="https://github.com/manufosela/karajan-code/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
14
|
-
<a href="https://
|
|
14
|
+
<a href="https://www.gnu.org/licenses/agpl-3.0"><img src="https://img.shields.io/badge/license-AGPL--3.0-blue.svg" alt="License"></a>
|
|
15
15
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg" alt="Node.js"></a>
|
|
16
16
|
</p>
|
|
17
17
|
|
|
@@ -240,5 +240,5 @@ npm run validate # Lint + test
|
|
|
240
240
|
|
|
241
241
|
- [Changelog](../CHANGELOG.md)
|
|
242
242
|
- [Politica de seguridad](../SECURITY.md)
|
|
243
|
-
- [Licencia (
|
|
243
|
+
- [Licencia (AGPL-3.0)](../LICENSE)
|
|
244
244
|
- [Issues](https://github.com/manufosela/karajan-code/issues)
|
package/package.json
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import { runCommand } from "../utils/process.js";
|
|
2
2
|
import { resolveBin } from "./resolve-bin.js";
|
|
3
|
-
|
|
4
|
-
const AGENT_META = {
|
|
5
|
-
codex: { bin: "codex", installUrl: "https://developers.openai.com/codex/cli" },
|
|
6
|
-
claude: { bin: "claude", installUrl: "https://docs.anthropic.com/en/docs/claude-code" },
|
|
7
|
-
gemini: { bin: "gemini", installUrl: "https://github.com/google-gemini/gemini-cli" },
|
|
8
|
-
aider: { bin: "aider", installUrl: "https://aider.chat/docs/install.html" }
|
|
9
|
-
};
|
|
3
|
+
import { getAgentMeta } from "./index.js";
|
|
10
4
|
|
|
11
5
|
export async function assertAgentsAvailable(agentNames = []) {
|
|
12
6
|
const unique = [...new Set(agentNames.filter(Boolean))];
|
|
13
7
|
const missing = [];
|
|
14
8
|
|
|
15
9
|
for (const name of unique) {
|
|
16
|
-
const meta =
|
|
17
|
-
if (!meta) continue;
|
|
10
|
+
const meta = getAgentMeta(name);
|
|
11
|
+
if (!meta || !meta.bin) continue;
|
|
18
12
|
const res = await runCommand(resolveBin(meta.bin), ["--version"]);
|
|
19
13
|
if (res.exitCode !== 0) {
|
|
20
14
|
missing.push({ name, ...meta });
|
package/src/agents/index.js
CHANGED
|
@@ -3,17 +3,38 @@ import { CodexAgent } from "./codex-agent.js";
|
|
|
3
3
|
import { GeminiAgent } from "./gemini-agent.js";
|
|
4
4
|
import { AiderAgent } from "./aider-agent.js";
|
|
5
5
|
|
|
6
|
+
const agentRegistry = new Map();
|
|
7
|
+
|
|
8
|
+
export function registerAgent(name, AgentClass, meta = {}) {
|
|
9
|
+
if (!name || typeof name !== "string") {
|
|
10
|
+
throw new Error("Agent name must be a non-empty string");
|
|
11
|
+
}
|
|
12
|
+
agentRegistry.set(name, { AgentClass, meta });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getAvailableAgents() {
|
|
16
|
+
return [...agentRegistry.entries()].map(([name, { AgentClass, meta }]) => ({
|
|
17
|
+
name,
|
|
18
|
+
AgentClass,
|
|
19
|
+
...meta,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getAgentMeta(name) {
|
|
24
|
+
const entry = agentRegistry.get(name);
|
|
25
|
+
return entry ? { ...entry.meta } : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
6
28
|
export function createAgent(agentName, config, logger) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
case "codex":
|
|
11
|
-
return new CodexAgent(agentName, config, logger);
|
|
12
|
-
case "gemini":
|
|
13
|
-
return new GeminiAgent(agentName, config, logger);
|
|
14
|
-
case "aider":
|
|
15
|
-
return new AiderAgent(agentName, config, logger);
|
|
16
|
-
default:
|
|
17
|
-
throw new Error(`Unsupported agent: ${agentName}`);
|
|
29
|
+
const entry = agentRegistry.get(agentName);
|
|
30
|
+
if (!entry) {
|
|
31
|
+
throw new Error(`Unsupported agent: ${agentName}`);
|
|
18
32
|
}
|
|
33
|
+
return new entry.AgentClass(agentName, config, logger);
|
|
19
34
|
}
|
|
35
|
+
|
|
36
|
+
// Auto-register built-in agents with CLI metadata
|
|
37
|
+
registerAgent("claude", ClaudeAgent, { bin: "claude", installUrl: "https://docs.anthropic.com/en/docs/claude-code" });
|
|
38
|
+
registerAgent("codex", CodexAgent, { bin: "codex", installUrl: "https://developers.openai.com/codex/cli" });
|
|
39
|
+
registerAgent("gemini", GeminiAgent, { bin: "gemini", installUrl: "https://github.com/google-gemini/gemini-cli" });
|
|
40
|
+
registerAgent("aider", AiderAgent, { bin: "aider", installUrl: "https://aider.chat/docs/install.html" });
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const modelRegistry = new Map();
|
|
2
|
+
|
|
3
|
+
export function registerModel(name, { provider, pricing, deprecated } = {}) {
|
|
4
|
+
if (!name || typeof name !== "string") {
|
|
5
|
+
throw new Error("Model name must be a non-empty string");
|
|
6
|
+
}
|
|
7
|
+
if (!pricing || typeof pricing.input_per_million !== "number" || typeof pricing.output_per_million !== "number") {
|
|
8
|
+
throw new Error(`Model "${name}" requires pricing with input_per_million and output_per_million`);
|
|
9
|
+
}
|
|
10
|
+
modelRegistry.set(name, {
|
|
11
|
+
provider: provider || name.split("/")[0],
|
|
12
|
+
pricing: { input_per_million: pricing.input_per_million, output_per_million: pricing.output_per_million },
|
|
13
|
+
deprecated: deprecated || null,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getModelPricing(name) {
|
|
18
|
+
const entry = modelRegistry.get(name);
|
|
19
|
+
return entry ? { ...entry.pricing } : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isModelDeprecated(name) {
|
|
23
|
+
const entry = modelRegistry.get(name);
|
|
24
|
+
if (!entry || !entry.deprecated) return false;
|
|
25
|
+
return new Date(entry.deprecated) <= new Date();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getModelInfo(name) {
|
|
29
|
+
const entry = modelRegistry.get(name);
|
|
30
|
+
if (!entry) return null;
|
|
31
|
+
return { name, provider: entry.provider, pricing: { ...entry.pricing }, deprecated: entry.deprecated };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getRegisteredModels() {
|
|
35
|
+
return [...modelRegistry.entries()].map(([name, entry]) => ({
|
|
36
|
+
name,
|
|
37
|
+
provider: entry.provider,
|
|
38
|
+
pricing: { ...entry.pricing },
|
|
39
|
+
deprecated: entry.deprecated,
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildDefaultPricingTable() {
|
|
44
|
+
const table = {};
|
|
45
|
+
for (const [name, entry] of modelRegistry) {
|
|
46
|
+
table[name] = { ...entry.pricing };
|
|
47
|
+
}
|
|
48
|
+
return table;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Auto-register built-in models
|
|
52
|
+
registerModel("claude", { provider: "anthropic", pricing: { input_per_million: 3, output_per_million: 15 } });
|
|
53
|
+
registerModel("claude/sonnet", { provider: "anthropic", pricing: { input_per_million: 3, output_per_million: 15 } });
|
|
54
|
+
registerModel("claude/opus", { provider: "anthropic", pricing: { input_per_million: 15, output_per_million: 75 } });
|
|
55
|
+
registerModel("claude/haiku", { provider: "anthropic", pricing: { input_per_million: 0.25, output_per_million: 1.25 } });
|
|
56
|
+
registerModel("codex", { provider: "openai", pricing: { input_per_million: 1.5, output_per_million: 4 } });
|
|
57
|
+
registerModel("codex/o4-mini", { provider: "openai", pricing: { input_per_million: 1.5, output_per_million: 4 } });
|
|
58
|
+
registerModel("codex/o3", { provider: "openai", pricing: { input_per_million: 10, output_per_million: 40 } });
|
|
59
|
+
registerModel("gemini", { provider: "google", pricing: { input_per_million: 1.25, output_per_million: 5 } });
|
|
60
|
+
registerModel("gemini/pro", { provider: "google", pricing: { input_per_million: 1.25, output_per_million: 5 } });
|
|
61
|
+
registerModel("gemini/flash", { provider: "google", pricing: { input_per_million: 0.075, output_per_million: 0.3 } });
|
|
62
|
+
registerModel("aider", { provider: "aider", pricing: { input_per_million: 3, output_per_million: 15 } });
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const DEFAULT_INTERVAL_MS = 5000;
|
|
2
|
+
|
|
3
|
+
export function setupOrphanGuard({ intervalMs = DEFAULT_INTERVAL_MS, exitFn = () => process.exit(0) } = {}) {
|
|
4
|
+
const parentPid = process.ppid;
|
|
5
|
+
|
|
6
|
+
const timer = setInterval(() => {
|
|
7
|
+
try {
|
|
8
|
+
process.kill(parentPid, 0);
|
|
9
|
+
} catch {
|
|
10
|
+
clearInterval(timer);
|
|
11
|
+
exitFn();
|
|
12
|
+
}
|
|
13
|
+
}, intervalMs);
|
|
14
|
+
timer.unref();
|
|
15
|
+
|
|
16
|
+
process.stdin.on("end", exitFn);
|
|
17
|
+
process.stdin.on("close", exitFn);
|
|
18
|
+
process.on("SIGHUP", exitFn);
|
|
19
|
+
|
|
20
|
+
return { timer, parentPid };
|
|
21
|
+
}
|
package/src/mcp/server.js
CHANGED
|
@@ -33,5 +33,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
|
33
33
|
}
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
// --- Orphan process protection ---
|
|
37
|
+
import { setupOrphanGuard } from "./orphan-guard.js";
|
|
38
|
+
setupOrphanGuard();
|
|
39
|
+
|
|
36
40
|
const transport = new StdioServerTransport();
|
|
37
41
|
await server.connect(transport);
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { createAgent } from "../agents/index.js";
|
|
2
|
+
import { RefactorerRole } from "../roles/refactorer-role.js";
|
|
3
|
+
import { SonarRole } from "../roles/sonar-role.js";
|
|
4
|
+
import { addCheckpoint, markSessionStatus, saveSession, pauseSession } from "../session-store.js";
|
|
5
|
+
import { generateDiff } from "../review/diff-generator.js";
|
|
6
|
+
import { evaluateTddPolicy } from "../review/tdd-policy.js";
|
|
7
|
+
import { validateReviewResult } from "../review/schema.js";
|
|
8
|
+
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
9
|
+
import { runReviewerWithFallback } from "./reviewer-fallback.js";
|
|
10
|
+
import { invokeSolomon } from "./solomon-escalation.js";
|
|
11
|
+
|
|
12
|
+
export async function runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration }) {
|
|
13
|
+
logger.setContext({ iteration, stage: "coder" });
|
|
14
|
+
emitProgress(
|
|
15
|
+
emitter,
|
|
16
|
+
makeEvent("coder:start", { ...eventBase, stage: "coder" }, {
|
|
17
|
+
message: `Coder (${coderRole.provider}) running`,
|
|
18
|
+
detail: { coder: coderRole.provider }
|
|
19
|
+
})
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const coderOnOutput = ({ stream, line }) => {
|
|
23
|
+
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "coder" }, {
|
|
24
|
+
message: line,
|
|
25
|
+
detail: { stream, agent: coderRole.provider }
|
|
26
|
+
}));
|
|
27
|
+
};
|
|
28
|
+
const coderStart = Date.now();
|
|
29
|
+
const coderExecResult = await coderRoleInstance.execute({
|
|
30
|
+
task: plannedTask,
|
|
31
|
+
reviewerFeedback: session.last_reviewer_feedback,
|
|
32
|
+
sonarSummary: session.last_sonar_summary,
|
|
33
|
+
onOutput: coderOnOutput
|
|
34
|
+
});
|
|
35
|
+
trackBudget({ role: "coder", provider: coderRole.provider, model: coderRole.model, result: coderExecResult.result, duration_ms: Date.now() - coderStart });
|
|
36
|
+
|
|
37
|
+
if (!coderExecResult.ok) {
|
|
38
|
+
await markSessionStatus(session, "failed");
|
|
39
|
+
const details = coderExecResult.result?.error || coderExecResult.summary || "unknown error";
|
|
40
|
+
emitProgress(
|
|
41
|
+
emitter,
|
|
42
|
+
makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
|
|
43
|
+
status: "fail",
|
|
44
|
+
message: `Coder failed: ${details}`
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
throw new Error(`Coder failed: ${details}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await addCheckpoint(session, { stage: "coder", iteration, note: "Coder applied changes" });
|
|
51
|
+
emitProgress(
|
|
52
|
+
emitter,
|
|
53
|
+
makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
|
|
54
|
+
message: "Coder completed"
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration }) {
|
|
60
|
+
logger.setContext({ iteration, stage: "refactorer" });
|
|
61
|
+
emitProgress(
|
|
62
|
+
emitter,
|
|
63
|
+
makeEvent("refactorer:start", { ...eventBase, stage: "refactorer" }, {
|
|
64
|
+
message: `Refactorer (${refactorerRole.provider}) running`,
|
|
65
|
+
detail: { refactorer: refactorerRole.provider }
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
const refRole = new RefactorerRole({ config, logger, emitter, createAgentFn: createAgent });
|
|
69
|
+
await refRole.init();
|
|
70
|
+
const refactorerStart = Date.now();
|
|
71
|
+
const refResult = await refRole.execute(plannedTask);
|
|
72
|
+
trackBudget({ role: "refactorer", provider: refactorerRole.provider, model: refactorerRole.model, result: refResult.result, duration_ms: Date.now() - refactorerStart });
|
|
73
|
+
if (!refResult.ok) {
|
|
74
|
+
await markSessionStatus(session, "failed");
|
|
75
|
+
const details = refResult.result?.error || refResult.summary || "unknown error";
|
|
76
|
+
emitProgress(
|
|
77
|
+
emitter,
|
|
78
|
+
makeEvent("refactorer:end", { ...eventBase, stage: "refactorer" }, {
|
|
79
|
+
status: "fail",
|
|
80
|
+
message: `Refactorer failed: ${details}`
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
throw new Error(`Refactorer failed: ${details}`);
|
|
84
|
+
}
|
|
85
|
+
await addCheckpoint(session, { stage: "refactorer", iteration, note: "Refactorer applied cleanups" });
|
|
86
|
+
emitProgress(
|
|
87
|
+
emitter,
|
|
88
|
+
makeEvent("refactorer:end", { ...eventBase, stage: "refactorer" }, {
|
|
89
|
+
message: "Refactorer completed"
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration, askQuestion }) {
|
|
95
|
+
logger.setContext({ iteration, stage: "tdd" });
|
|
96
|
+
const tddDiff = await generateDiff({ baseRef: session.session_start_sha });
|
|
97
|
+
const tddEval = evaluateTddPolicy(tddDiff, config.development);
|
|
98
|
+
await addCheckpoint(session, {
|
|
99
|
+
stage: "tdd-policy",
|
|
100
|
+
iteration,
|
|
101
|
+
ok: tddEval.ok,
|
|
102
|
+
reason: tddEval.reason,
|
|
103
|
+
source_files: tddEval.sourceFiles?.length || 0,
|
|
104
|
+
test_files: tddEval.testFiles?.length || 0
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
emitProgress(
|
|
108
|
+
emitter,
|
|
109
|
+
makeEvent("tdd:result", { ...eventBase, stage: "tdd" }, {
|
|
110
|
+
status: tddEval.ok ? "ok" : "fail",
|
|
111
|
+
message: tddEval.ok ? "TDD policy passed" : `TDD policy failed: ${tddEval.reason}`,
|
|
112
|
+
detail: {
|
|
113
|
+
ok: tddEval.ok,
|
|
114
|
+
reason: tddEval.reason,
|
|
115
|
+
sourceFiles: tddEval.sourceFiles?.length || 0,
|
|
116
|
+
testFiles: tddEval.testFiles?.length || 0
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (!tddEval.ok) {
|
|
122
|
+
session.last_reviewer_feedback = tddEval.message;
|
|
123
|
+
session.repeated_issue_count += 1;
|
|
124
|
+
await saveSession(session);
|
|
125
|
+
if (session.repeated_issue_count >= config.session.fail_fast_repeats) {
|
|
126
|
+
const question = `TDD policy has failed ${session.repeated_issue_count} times. The coder is not creating tests. How should we proceed? Issue: ${tddEval.reason}`;
|
|
127
|
+
if (askQuestion) {
|
|
128
|
+
const answer = await askQuestion(question, { iteration, stage: "tdd" });
|
|
129
|
+
if (answer) {
|
|
130
|
+
session.last_reviewer_feedback += `\nUser guidance: ${answer}`;
|
|
131
|
+
session.repeated_issue_count = 0;
|
|
132
|
+
await saveSession(session);
|
|
133
|
+
return { action: "continue" };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
await pauseSession(session, {
|
|
137
|
+
question,
|
|
138
|
+
context: {
|
|
139
|
+
iteration,
|
|
140
|
+
stage: "tdd",
|
|
141
|
+
lastFeedback: tddEval.message,
|
|
142
|
+
repeatedCount: session.repeated_issue_count
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
emitProgress(
|
|
146
|
+
emitter,
|
|
147
|
+
makeEvent("question", { ...eventBase, stage: "tdd" }, {
|
|
148
|
+
status: "paused",
|
|
149
|
+
message: question,
|
|
150
|
+
detail: { question, sessionId: session.id }
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
return { action: "pause", result: { paused: true, sessionId: session.id, question, context: "tdd_fail_fast" } };
|
|
154
|
+
}
|
|
155
|
+
return { action: "continue" };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return { action: "ok" };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function runSonarStage({ config, logger, emitter, eventBase, session, trackBudget, iteration, repeatDetector, budgetSummary, sonarState, askQuestion, task }) {
|
|
162
|
+
logger.setContext({ iteration, stage: "sonar" });
|
|
163
|
+
emitProgress(
|
|
164
|
+
emitter,
|
|
165
|
+
makeEvent("sonar:start", { ...eventBase, stage: "sonar" }, {
|
|
166
|
+
message: "SonarQube scanning"
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const sonarRole = new SonarRole({ config, logger, emitter });
|
|
171
|
+
await sonarRole.init({ iteration });
|
|
172
|
+
const sonarStart = Date.now();
|
|
173
|
+
const sonarOutput = await sonarRole.run();
|
|
174
|
+
trackBudget({ role: "sonar", provider: "sonar", result: sonarOutput, duration_ms: Date.now() - sonarStart });
|
|
175
|
+
const sonarResult = sonarOutput.result;
|
|
176
|
+
|
|
177
|
+
if (!sonarResult.gateStatus && sonarResult.error) {
|
|
178
|
+
await markSessionStatus(session, "failed");
|
|
179
|
+
emitProgress(
|
|
180
|
+
emitter,
|
|
181
|
+
makeEvent("sonar:end", { ...eventBase, stage: "sonar" }, {
|
|
182
|
+
status: "fail",
|
|
183
|
+
message: `Sonar scan failed: ${sonarResult.error}`
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
throw new Error(`Sonar scan failed: ${sonarResult.error}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
session.last_sonar_summary = sonarOutput.summary;
|
|
190
|
+
if (typeof sonarResult.openIssuesTotal === "number") {
|
|
191
|
+
if (sonarState.issuesInitial === null) {
|
|
192
|
+
sonarState.issuesInitial = sonarResult.openIssuesTotal;
|
|
193
|
+
}
|
|
194
|
+
sonarState.issuesFinal = sonarResult.openIssuesTotal;
|
|
195
|
+
}
|
|
196
|
+
await addCheckpoint(session, {
|
|
197
|
+
stage: "sonar",
|
|
198
|
+
iteration,
|
|
199
|
+
project_key: sonarResult.projectKey,
|
|
200
|
+
quality_gate: sonarResult.gateStatus,
|
|
201
|
+
open_issues: sonarResult.openIssuesTotal
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
emitProgress(
|
|
205
|
+
emitter,
|
|
206
|
+
makeEvent("sonar:end", { ...eventBase, stage: "sonar" }, {
|
|
207
|
+
status: sonarResult.blocking ? "fail" : "ok",
|
|
208
|
+
message: `Quality gate: ${sonarResult.gateStatus}`,
|
|
209
|
+
detail: { projectKey: sonarResult.projectKey, gateStatus: sonarResult.gateStatus, openIssues: sonarResult.openIssuesTotal }
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (sonarResult.blocking) {
|
|
214
|
+
repeatDetector.addIteration(sonarResult.issues, []);
|
|
215
|
+
const repeatState = repeatDetector.isStalled();
|
|
216
|
+
if (repeatState.stalled) {
|
|
217
|
+
const repeatCounts = repeatDetector.getRepeatCounts();
|
|
218
|
+
const message = `No progress: SonarQube issues repeated ${repeatCounts.sonar} times.`;
|
|
219
|
+
logger.warn(message);
|
|
220
|
+
await markSessionStatus(session, "stalled");
|
|
221
|
+
emitProgress(
|
|
222
|
+
emitter,
|
|
223
|
+
makeEvent("session:end", { ...eventBase, stage: "sonar" }, {
|
|
224
|
+
status: "stalled",
|
|
225
|
+
message,
|
|
226
|
+
detail: { reason: repeatState.reason, repeats: repeatCounts.sonar, budget: budgetSummary() }
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
return { action: "stalled", result: { approved: false, sessionId: session.id, reason: "stalled" } };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
session.last_reviewer_feedback = `Sonar gate blocking (${sonarResult.gateStatus}). Resolve critical findings first.`;
|
|
233
|
+
session.sonar_retry_count = (session.sonar_retry_count || 0) + 1;
|
|
234
|
+
await saveSession(session);
|
|
235
|
+
const maxSonarRetries = config.session.max_sonar_retries ?? config.session.fail_fast_repeats;
|
|
236
|
+
if (session.sonar_retry_count >= maxSonarRetries) {
|
|
237
|
+
emitProgress(
|
|
238
|
+
emitter,
|
|
239
|
+
makeEvent("solomon:escalate", { ...eventBase, stage: "sonar" }, {
|
|
240
|
+
message: `Sonar sub-loop limit reached (${session.sonar_retry_count}/${maxSonarRetries})`,
|
|
241
|
+
detail: { subloop: "sonar", retryCount: session.sonar_retry_count, limit: maxSonarRetries, gateStatus: sonarResult.gateStatus }
|
|
242
|
+
})
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
const solomonResult = await invokeSolomon({
|
|
246
|
+
config, logger, emitter, eventBase, stage: "sonar", askQuestion, session, iteration,
|
|
247
|
+
conflict: {
|
|
248
|
+
stage: "sonar",
|
|
249
|
+
task,
|
|
250
|
+
iterationCount: session.sonar_retry_count,
|
|
251
|
+
maxIterations: maxSonarRetries,
|
|
252
|
+
history: [{ agent: "sonar", feedback: session.last_sonar_summary }]
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (solomonResult.action === "pause") {
|
|
257
|
+
return { action: "pause", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "sonar_fail_fast" } };
|
|
258
|
+
}
|
|
259
|
+
if (solomonResult.action === "continue") {
|
|
260
|
+
if (solomonResult.humanGuidance) {
|
|
261
|
+
session.last_reviewer_feedback += `\nUser guidance: ${solomonResult.humanGuidance}`;
|
|
262
|
+
}
|
|
263
|
+
session.sonar_retry_count = 0;
|
|
264
|
+
await saveSession(session);
|
|
265
|
+
return { action: "continue" };
|
|
266
|
+
}
|
|
267
|
+
if (solomonResult.action === "subtask") {
|
|
268
|
+
return { action: "pause", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "sonar_subtask" } };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { action: "continue" };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Sonar passed — reset retry counter
|
|
275
|
+
session.sonar_retry_count = 0;
|
|
276
|
+
const issuesInitial = sonarState.issuesInitial ?? sonarResult.openIssuesTotal ?? 0;
|
|
277
|
+
const issuesFinal = sonarState.issuesFinal ?? sonarResult.openIssuesTotal ?? 0;
|
|
278
|
+
const stageResult = {
|
|
279
|
+
gateStatus: sonarResult.gateStatus,
|
|
280
|
+
openIssues: sonarResult.openIssuesTotal,
|
|
281
|
+
issuesInitial,
|
|
282
|
+
issuesFinal,
|
|
283
|
+
issuesResolved: Math.max(issuesInitial - issuesFinal, 0)
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return { action: "ok", stageResult };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function runReviewerStage({ reviewerRole, config, logger, emitter, eventBase, session, trackBudget, iteration, reviewRules, task, repeatDetector, budgetSummary }) {
|
|
290
|
+
logger.setContext({ iteration, stage: "reviewer" });
|
|
291
|
+
emitProgress(
|
|
292
|
+
emitter,
|
|
293
|
+
makeEvent("reviewer:start", { ...eventBase, stage: "reviewer" }, {
|
|
294
|
+
message: `Reviewer (${reviewerRole.provider}) running`,
|
|
295
|
+
detail: { reviewer: reviewerRole.provider }
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const diff = await generateDiff({ baseRef: session.session_start_sha });
|
|
300
|
+
const reviewerOnOutput = ({ stream, line }) => {
|
|
301
|
+
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "reviewer" }, {
|
|
302
|
+
message: line,
|
|
303
|
+
detail: { stream, agent: reviewerRole.provider }
|
|
304
|
+
}));
|
|
305
|
+
};
|
|
306
|
+
const reviewerStart = Date.now();
|
|
307
|
+
const reviewerExec = await runReviewerWithFallback({
|
|
308
|
+
reviewerName: reviewerRole.provider,
|
|
309
|
+
config,
|
|
310
|
+
logger,
|
|
311
|
+
emitter,
|
|
312
|
+
reviewInput: { task, diff, reviewRules, onOutput: reviewerOnOutput },
|
|
313
|
+
session,
|
|
314
|
+
iteration,
|
|
315
|
+
onAttemptResult: ({ reviewer, result }) => {
|
|
316
|
+
trackBudget({ role: "reviewer", provider: reviewer, model: reviewerRole.model, result, duration_ms: Date.now() - reviewerStart });
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
if (!reviewerExec.execResult || !reviewerExec.execResult.ok) {
|
|
321
|
+
await markSessionStatus(session, "failed");
|
|
322
|
+
const lastAttempt = reviewerExec.attempts.at(-1);
|
|
323
|
+
const details =
|
|
324
|
+
lastAttempt?.result?.error ||
|
|
325
|
+
lastAttempt?.execResult?.summary ||
|
|
326
|
+
`reviewer=${lastAttempt?.reviewer || "unknown"}`;
|
|
327
|
+
emitProgress(
|
|
328
|
+
emitter,
|
|
329
|
+
makeEvent("reviewer:end", { ...eventBase, stage: "reviewer" }, {
|
|
330
|
+
status: "fail",
|
|
331
|
+
message: `Reviewer failed: ${details}`
|
|
332
|
+
})
|
|
333
|
+
);
|
|
334
|
+
throw new Error(`Reviewer failed: ${details}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const reviewResult = reviewerExec.execResult.result;
|
|
338
|
+
let review;
|
|
339
|
+
try {
|
|
340
|
+
review = validateReviewResult({
|
|
341
|
+
approved: reviewResult.approved,
|
|
342
|
+
blocking_issues: reviewResult.blocking_issues || [],
|
|
343
|
+
non_blocking_suggestions: reviewResult.non_blocking_suggestions || [],
|
|
344
|
+
summary: reviewResult.raw_summary || "",
|
|
345
|
+
confidence: reviewResult.confidence ?? 0
|
|
346
|
+
});
|
|
347
|
+
} catch (parseErr) {
|
|
348
|
+
logger.warn(`Reviewer output validation failed: ${parseErr.message}`);
|
|
349
|
+
review = {
|
|
350
|
+
approved: false,
|
|
351
|
+
blocking_issues: [{
|
|
352
|
+
id: "PARSE_ERROR",
|
|
353
|
+
severity: "high",
|
|
354
|
+
description: `Reviewer output could not be parsed: ${parseErr.message}`
|
|
355
|
+
}],
|
|
356
|
+
non_blocking_suggestions: [],
|
|
357
|
+
summary: `Parse error: ${parseErr.message}`,
|
|
358
|
+
confidence: 0
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
await addCheckpoint(session, {
|
|
362
|
+
stage: "reviewer",
|
|
363
|
+
iteration,
|
|
364
|
+
approved: review.approved,
|
|
365
|
+
blocking_issues: review.blocking_issues.length
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
emitProgress(
|
|
369
|
+
emitter,
|
|
370
|
+
makeEvent("reviewer:end", { ...eventBase, stage: "reviewer" }, {
|
|
371
|
+
status: review.approved ? "ok" : "fail",
|
|
372
|
+
message: review.approved ? "Review approved" : `Review rejected (${review.blocking_issues.length} blocking)`,
|
|
373
|
+
detail: {
|
|
374
|
+
approved: review.approved,
|
|
375
|
+
blockingCount: review.blocking_issues.length,
|
|
376
|
+
issues: review.blocking_issues.map(
|
|
377
|
+
(x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
if (!review.approved) {
|
|
384
|
+
repeatDetector.addIteration([], review.blocking_issues);
|
|
385
|
+
const repeatState = repeatDetector.isStalled();
|
|
386
|
+
if (repeatState.stalled) {
|
|
387
|
+
const repeatCounts = repeatDetector.getRepeatCounts();
|
|
388
|
+
const message = `Manual intervention required: reviewer issues repeated ${repeatCounts.reviewer} times.`;
|
|
389
|
+
logger.warn(message);
|
|
390
|
+
await markSessionStatus(session, "stalled");
|
|
391
|
+
emitProgress(
|
|
392
|
+
emitter,
|
|
393
|
+
makeEvent("session:end", { ...eventBase, stage: "reviewer" }, {
|
|
394
|
+
status: "stalled",
|
|
395
|
+
message,
|
|
396
|
+
detail: { reason: repeatState.reason, repeats: repeatCounts.reviewer, budget: budgetSummary() }
|
|
397
|
+
})
|
|
398
|
+
);
|
|
399
|
+
return { review, stalled: true, stalledResult: { approved: false, sessionId: session.id, reason: "stalled" } };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return { review };
|
|
404
|
+
}
|