karajan-code 2.0.0 → 2.0.2
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/agents/claude-agent.js +32 -5
- package/src/commands/board.js +8 -1
- package/src/config.js +7 -0
- package/src/orchestrator/pipeline-context.js +3 -0
- package/src/orchestrator/stages/architect-stage.js +2 -7
- package/src/orchestrator/stages/coder-stage.js +52 -17
- package/src/orchestrator/stages/planner-stage.js +2 -7
- package/src/orchestrator/stages/research-stage.js +2 -7
- package/src/orchestrator/stages/reviewer-stage.js +38 -11
- package/src/orchestrator/stages/triage-stage.js +2 -7
- package/src/orchestrator.js +150 -25
- package/src/utils/display.js +18 -4
- package/src/utils/events.js +12 -0
package/package.json
CHANGED
|
@@ -116,18 +116,45 @@ function extractTextFromStreamJson(raw) {
|
|
|
116
116
|
* Create a wrapping onOutput that parses stream-json lines and forwards
|
|
117
117
|
* meaningful content (assistant text, tool usage) to the original callback.
|
|
118
118
|
*/
|
|
119
|
+
function summarizeToolCall(name, input = {}) {
|
|
120
|
+
// Produce concise human-readable action line from tool_use block
|
|
121
|
+
const rel = (p) => String(p || "").replace(process.cwd() + "/", "");
|
|
122
|
+
switch (name) {
|
|
123
|
+
case "Read": return `Read ${rel(input.file_path)}`;
|
|
124
|
+
case "Write": return `Write ${rel(input.file_path)}`;
|
|
125
|
+
case "Edit": return `Edit ${rel(input.file_path)}`;
|
|
126
|
+
case "MultiEdit": return `MultiEdit ${rel(input.file_path)} (${(input.edits || []).length} edits)`;
|
|
127
|
+
case "NotebookEdit": return `NotebookEdit ${rel(input.notebook_path)}`;
|
|
128
|
+
case "Glob": return `Glob ${input.pattern || ""}`.trim();
|
|
129
|
+
case "Grep": return `Grep "${(input.pattern || "").slice(0, 60)}"${input.path ? ` in ${rel(input.path)}` : ""}`;
|
|
130
|
+
case "Bash": {
|
|
131
|
+
const cmd = String(input.command || "").replace(/\s+/g, " ").slice(0, 100);
|
|
132
|
+
return `Bash $ ${cmd}`;
|
|
133
|
+
}
|
|
134
|
+
case "TodoWrite": return `Todo ${(input.todos || []).length} items`;
|
|
135
|
+
case "WebFetch": return `WebFetch ${input.url || ""}`;
|
|
136
|
+
case "WebSearch": return `WebSearch "${(input.query || "").slice(0, 60)}"`;
|
|
137
|
+
case "Task": return `Agent: ${input.subagent_type || "?"} — ${(input.description || "").slice(0, 60)}`;
|
|
138
|
+
default: return `${name}${Object.keys(input).length > 0 ? " (…)" : ""}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
function createStreamJsonFilter(onOutput) {
|
|
120
143
|
if (!onOutput) return null;
|
|
121
144
|
return ({ stream, line }) => {
|
|
122
145
|
try {
|
|
123
146
|
const obj = JSON.parse(line);
|
|
124
|
-
// Forward assistant
|
|
147
|
+
// Forward assistant content
|
|
125
148
|
if (obj.type === "assistant" && obj.message?.content) {
|
|
126
149
|
for (const block of obj.message.content) {
|
|
127
150
|
if (block.type === "text" && block.text) {
|
|
128
|
-
|
|
151
|
+
// Only emit first line of text for brevity
|
|
152
|
+
const firstLine = block.text.split("\n")[0].trim().slice(0, 120);
|
|
153
|
+
if (firstLine) onOutput({ stream, line: firstLine, kind: "text" });
|
|
129
154
|
} else if (block.type === "tool_use") {
|
|
130
|
-
onOutput({ stream, line:
|
|
155
|
+
onOutput({ stream, line: summarizeToolCall(block.name, block.input), kind: "tool" });
|
|
156
|
+
} else if (block.type === "thinking" && block.thinking) {
|
|
157
|
+
onOutput({ stream, line: `thinking: ${block.thinking.slice(0, 80).replace(/\s+/g, " ")}…`, kind: "thinking" });
|
|
131
158
|
}
|
|
132
159
|
}
|
|
133
160
|
return;
|
|
@@ -135,9 +162,9 @@ function createStreamJsonFilter(onOutput) {
|
|
|
135
162
|
// Forward result
|
|
136
163
|
if (obj.type === "result") {
|
|
137
164
|
const summary = typeof obj.result === "string"
|
|
138
|
-
? obj.result.slice(0,
|
|
165
|
+
? obj.result.split("\n")[0].slice(0, 120)
|
|
139
166
|
: "result received";
|
|
140
|
-
onOutput({ stream, line: `
|
|
167
|
+
onOutput({ stream, line: `done: ${summary}` });
|
|
141
168
|
return;
|
|
142
169
|
}
|
|
143
170
|
} catch { /* not JSON, forward raw */ }
|
package/src/commands/board.js
CHANGED
|
@@ -34,14 +34,21 @@ export async function startBoard(port = 4000) {
|
|
|
34
34
|
const karajanHome = getKarajanHome();
|
|
35
35
|
fs.mkdirSync(karajanHome, { recursive: true });
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// Use process.execPath (absolute path to current node binary) instead of "node".
|
|
38
|
+
// Fixes ENOENT on nvm setups where `node` is not in the spawned process's PATH.
|
|
39
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
38
40
|
env: { ...process.env, PORT: String(port) },
|
|
39
41
|
detached: true,
|
|
40
42
|
stdio: "ignore",
|
|
41
43
|
cwd: BOARD_DIR
|
|
42
44
|
});
|
|
45
|
+
// Swallow spawn errors so an HU Board failure never crashes the pipeline.
|
|
46
|
+
child.on("error", () => { /* non-blocking — caller logs via tryAutoStartBoard catch */ });
|
|
43
47
|
child.unref();
|
|
44
48
|
|
|
49
|
+
if (!child.pid) {
|
|
50
|
+
throw new Error("Failed to spawn HU Board server");
|
|
51
|
+
}
|
|
45
52
|
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
46
53
|
return { ok: true, alreadyRunning: false, pid: child.pid, url: `http://localhost:${port}` };
|
|
47
54
|
}
|
package/src/config.js
CHANGED
|
@@ -34,8 +34,15 @@ const DEFAULTS = {
|
|
|
34
34
|
discover: { enabled: false },
|
|
35
35
|
architect: { enabled: false },
|
|
36
36
|
hu_reviewer: { enabled: false },
|
|
37
|
+
brain: { enabled: true },
|
|
37
38
|
auto_simplify: true
|
|
38
39
|
},
|
|
40
|
+
brain: {
|
|
41
|
+
enabled: true,
|
|
42
|
+
provider: "claude",
|
|
43
|
+
bypass_solomon_on_correctness: true,
|
|
44
|
+
max_consecutive_verification_failures: 2
|
|
45
|
+
},
|
|
39
46
|
review_mode: "standard",
|
|
40
47
|
max_iterations: 5,
|
|
41
48
|
max_budget_usd: null,
|
|
@@ -46,6 +46,9 @@ export class PipelineContext {
|
|
|
46
46
|
// Planned task (may differ from original task after planner)
|
|
47
47
|
this.plannedTask = null;
|
|
48
48
|
|
|
49
|
+
// Karajan Brain runtime context (feedback queue, verification tracker, compression stats)
|
|
50
|
+
this.brainCtx = null;
|
|
51
|
+
|
|
49
52
|
// Timing
|
|
50
53
|
this.startedAt = null;
|
|
51
54
|
}
|
|
@@ -7,7 +7,7 @@ import { ArchitectRole } from "../../roles/architect-role.js";
|
|
|
7
7
|
import { createAgent } from "../../agents/index.js";
|
|
8
8
|
import { createArchitectADRs } from "../../planning-game/architect-adrs.js";
|
|
9
9
|
import { addCheckpoint } from "../../session-store.js";
|
|
10
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
10
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
11
11
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
12
12
|
|
|
13
13
|
async function handleArchitectClarification({ architectOutput, askQuestion, config, logger, emitter, eventBase, session, architectOnOutput, architectProvider, coderRole, researchContext, discoverResult, triageLevel, trackBudget }) {
|
|
@@ -77,12 +77,7 @@ export async function runArchitectStage({ config, logger, emitter, eventBase, se
|
|
|
77
77
|
detail: { architect: architectProvider, provider: architectProvider, executorType: "agent" }
|
|
78
78
|
})
|
|
79
79
|
);
|
|
80
|
-
const architectOnOutput = (
|
|
81
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "architect" }, {
|
|
82
|
-
message: line,
|
|
83
|
-
detail: { stream, agent: architectProvider }
|
|
84
|
-
}));
|
|
85
|
-
};
|
|
80
|
+
const architectOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "architect", architectProvider, payload);
|
|
86
81
|
const architectStall = createStallDetector({
|
|
87
82
|
onOutput: architectOnOutput, emitter, eventBase, stage: "architect", provider: architectProvider
|
|
88
83
|
});
|
|
@@ -10,13 +10,13 @@ import { addCheckpoint, markSessionStatus, saveSession } from "../../session-sto
|
|
|
10
10
|
import { generateDiff, getUntrackedFiles } from "../../review/diff-generator.js";
|
|
11
11
|
import { evaluateTddPolicy } from "../../review/tdd-policy.js";
|
|
12
12
|
import { buildDeferredContext } from "../../review/scope-filter.js";
|
|
13
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
13
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
14
14
|
import { runCoderWithFallback } from "../agent-fallback.js";
|
|
15
15
|
import { invokeSolomon } from "../solomon-escalation.js";
|
|
16
16
|
import { detectRateLimit } from "../../utils/rate-limit-detector.js";
|
|
17
17
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
18
18
|
|
|
19
|
-
export async function runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration }) {
|
|
19
|
+
export async function runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration, brainCtx }) {
|
|
20
20
|
logger.setContext({ iteration, stage: "coder" });
|
|
21
21
|
emitProgress(
|
|
22
22
|
emitter,
|
|
@@ -26,12 +26,18 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
26
26
|
})
|
|
27
27
|
);
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
// Brain: if enabled and queue has entries, use enriched feedback instead of flat string
|
|
30
|
+
let reviewerFeedback = session.last_reviewer_feedback;
|
|
31
|
+
if (brainCtx?.enabled) {
|
|
32
|
+
const { buildCoderFeedbackPrompt } = await import("../brain-coordinator.js");
|
|
33
|
+
const enriched = buildCoderFeedbackPrompt(brainCtx);
|
|
34
|
+
if (enriched) {
|
|
35
|
+
reviewerFeedback = enriched;
|
|
36
|
+
logger.info(`Brain: using enriched feedback (${brainCtx.feedbackQueue.entries.length} entries)`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const coderOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "coder", coderRole.provider, payload);
|
|
35
41
|
const coderStall = createStallDetector({
|
|
36
42
|
onOutput: coderOnOutput, emitter, eventBase, stage: "coder", provider: coderRole.provider
|
|
37
43
|
});
|
|
@@ -40,7 +46,7 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
40
46
|
try {
|
|
41
47
|
coderExecResult = await coderRoleInstance.execute({
|
|
42
48
|
task: plannedTask,
|
|
43
|
-
reviewerFeedback
|
|
49
|
+
reviewerFeedback,
|
|
44
50
|
sonarSummary: session.last_sonar_summary,
|
|
45
51
|
deferredContext: buildDeferredContext(session.deferred_issues),
|
|
46
52
|
onOutput: coderStall.onOutput
|
|
@@ -123,14 +129,48 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
123
129
|
throw new Error(`Coder failed: ${details}`);
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
|
|
132
|
+
// Measure files changed so stale detection (solomon-rules) has accurate data
|
|
133
|
+
let filesChanged = 0;
|
|
134
|
+
try {
|
|
135
|
+
const { verifyCoderOutput } = await import("../verification-gate.js");
|
|
136
|
+
const verif = verifyCoderOutput({
|
|
137
|
+
baseRef: session.session_start_sha,
|
|
138
|
+
projectDir: config.projectDir || process.cwd()
|
|
139
|
+
});
|
|
140
|
+
filesChanged = verif.filesChanged || 0;
|
|
141
|
+
} catch { /* ignore verification errors */ }
|
|
142
|
+
|
|
143
|
+
await addCheckpoint(session, { stage: "coder", iteration, note: "Coder applied changes", provider: coderRole.provider, model: coderRole.model || null, filesChanged });
|
|
127
144
|
emitProgress(
|
|
128
145
|
emitter,
|
|
129
146
|
makeEvent("coder:end", { ...eventBase, stage: "coder" }, {
|
|
130
147
|
message: "Coder completed",
|
|
131
|
-
detail: { provider: coderRole.provider, executorType: "agent" }
|
|
148
|
+
detail: { provider: coderRole.provider, executorType: "agent", filesChanged }
|
|
132
149
|
})
|
|
133
150
|
);
|
|
151
|
+
|
|
152
|
+
// Brain: verify coder produced real changes + clear feedback queue (now addressed)
|
|
153
|
+
if (brainCtx?.enabled) {
|
|
154
|
+
const { verifyCoderRan, clearFeedback } = await import("../brain-coordinator.js");
|
|
155
|
+
const result = verifyCoderRan(brainCtx, {
|
|
156
|
+
baseRef: session.session_start_sha,
|
|
157
|
+
projectDir: config.projectDir || process.cwd()
|
|
158
|
+
});
|
|
159
|
+
const maxFailures = config.brain?.max_consecutive_verification_failures ?? 2;
|
|
160
|
+
if (!result.passed) {
|
|
161
|
+
logger.warn(`Brain verification: coder made 0 changes (consecutive failures: ${brainCtx.verificationTracker.consecutiveFailures}/${maxFailures})`);
|
|
162
|
+
emitProgress(emitter, makeEvent("brain:verification", { ...eventBase, stage: "coder" }, {
|
|
163
|
+
message: `Brain: coder produced no changes (${brainCtx.verificationTracker.consecutiveFailures}/${maxFailures} consecutive failures)`,
|
|
164
|
+
detail: { filesChanged: result.filesChanged, consecutiveFailures: brainCtx.verificationTracker.consecutiveFailures }
|
|
165
|
+
}));
|
|
166
|
+
if (brainCtx.verificationTracker.consecutiveFailures >= maxFailures) {
|
|
167
|
+
await markSessionStatus(session, "stalled");
|
|
168
|
+
throw new Error(`Brain: ${maxFailures} consecutive coder iterations with 0 file changes — pipeline stalled`);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
clearFeedback(brainCtx);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
134
174
|
}
|
|
135
175
|
|
|
136
176
|
export async function runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration }) {
|
|
@@ -142,12 +182,7 @@ export async function runRefactorerStage({ refactorerRole, config, logger, emitt
|
|
|
142
182
|
detail: { refactorer: refactorerRole.provider, provider: refactorerRole.provider, executorType: "agent" }
|
|
143
183
|
})
|
|
144
184
|
);
|
|
145
|
-
const refactorerOnOutput = (
|
|
146
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "refactorer" }, {
|
|
147
|
-
message: line,
|
|
148
|
-
detail: { stream, agent: refactorerRole.provider }
|
|
149
|
-
}));
|
|
150
|
-
};
|
|
185
|
+
const refactorerOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "refactorer", refactorerRole.provider, payload);
|
|
151
186
|
const refactorerStall = createStallDetector({
|
|
152
187
|
onOutput: refactorerOnOutput, emitter, eventBase, stage: "refactorer", provider: refactorerRole.provider
|
|
153
188
|
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { PlannerRole } from "../../roles/planner-role.js";
|
|
7
7
|
import { createAgent } from "../../agents/index.js";
|
|
8
8
|
import { addCheckpoint, markSessionStatus } from "../../session-store.js";
|
|
9
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
9
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
10
10
|
import { parsePlannerOutput } from "../../prompts/planner.js";
|
|
11
11
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
12
12
|
|
|
@@ -21,12 +21,7 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
|
|
|
21
21
|
})
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
-
const plannerOnOutput = (
|
|
25
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "planner" }, {
|
|
26
|
-
message: line,
|
|
27
|
-
detail: { stream, agent: plannerRole.provider }
|
|
28
|
-
}));
|
|
29
|
-
};
|
|
24
|
+
const plannerOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "planner", plannerRole.provider, payload);
|
|
30
25
|
const plannerStall = createStallDetector({
|
|
31
26
|
onOutput: plannerOnOutput, emitter, eventBase, stage: "planner", provider: plannerRole.provider
|
|
32
27
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { ResearcherRole } from "../../roles/researcher-role.js";
|
|
7
7
|
import { addCheckpoint } from "../../session-store.js";
|
|
8
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
8
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
9
9
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
10
10
|
|
|
11
11
|
export async function runResearcherStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget }) {
|
|
@@ -20,12 +20,7 @@ export async function runResearcherStage({ config, logger, emitter, eventBase, s
|
|
|
20
20
|
})
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
const researcherOnOutput = (
|
|
24
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "researcher" }, {
|
|
25
|
-
message: line,
|
|
26
|
-
detail: { stream, agent: researcherProvider }
|
|
27
|
-
}));
|
|
28
|
-
};
|
|
23
|
+
const researcherOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "researcher", researcherProvider, payload);
|
|
29
24
|
const researcherStall = createStallDetector({
|
|
30
25
|
onOutput: researcherOnOutput, emitter, eventBase, stage: "researcher", provider: researcherProvider
|
|
31
26
|
});
|
|
@@ -7,7 +7,7 @@ import { addCheckpoint, markSessionStatus, saveSession } from "../../session-sto
|
|
|
7
7
|
import { generateDiff } from "../../review/diff-generator.js";
|
|
8
8
|
import { validateReviewResult } from "../../review/schema.js";
|
|
9
9
|
import { filterReviewScope, buildDeferredContext } from "../../review/scope-filter.js";
|
|
10
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
10
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
11
11
|
import { runReviewerWithFallback } from "../reviewer-fallback.js";
|
|
12
12
|
import { invokeSolomon } from "../solomon-escalation.js";
|
|
13
13
|
import { detectRateLimit } from "../../utils/rate-limit-detector.js";
|
|
@@ -117,12 +117,38 @@ async function handleReviewerStalledSolomon({ review, repeatCounts, repeatState,
|
|
|
117
117
|
return { review, stalled: true, stalledResult: { approved: false, sessionId: session.id, reason: "stalled" } };
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
async function handleReviewerRejection({ review, repeatDetector, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary }) {
|
|
120
|
+
async function handleReviewerRejection({ review, repeatDetector, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary, brainCtx }) {
|
|
121
121
|
repeatDetector.addIteration([], review.blocking_issues);
|
|
122
122
|
const repeatState = repeatDetector.isStalled();
|
|
123
123
|
|
|
124
124
|
const solomonEnabled = Boolean(config.pipeline?.solomon?.enabled);
|
|
125
125
|
|
|
126
|
+
// Brain: if enabled and bypass_solomon_on_correctness, push issues to queue and skip Solomon
|
|
127
|
+
// for non-style rejections. Solomon is still consulted on genuine dilemmas (style-only, deadlock).
|
|
128
|
+
if (brainCtx?.enabled && config.brain?.bypass_solomon_on_correctness !== false) {
|
|
129
|
+
const cats = categorizeIssues(review.blocking_issues);
|
|
130
|
+
const nonStyleIssues = cats.security + cats.correctness + cats.tests;
|
|
131
|
+
const styleIssues = cats.style;
|
|
132
|
+
// Only bypass if there ARE non-style issues (correctness, tests, security, or "other" is correctness)
|
|
133
|
+
// If it's style-only, let Solomon evaluate whether to override
|
|
134
|
+
if (nonStyleIssues > 0 || cats.other > 0) {
|
|
135
|
+
const { processRoleOutput } = await import("../brain-coordinator.js");
|
|
136
|
+
processRoleOutput(brainCtx, { roleName: "reviewer", output: review, iteration });
|
|
137
|
+
logger.info(`Brain: reviewer rejected with ${nonStyleIssues} non-style issue(s) — bypassing Solomon, returning to coder`);
|
|
138
|
+
emitProgress(emitter, makeEvent("brain:bypass-solomon", { ...eventBase, stage: "reviewer" }, {
|
|
139
|
+
message: `Brain: ${nonStyleIssues} non-style issue(s) — sending to coder without Solomon`,
|
|
140
|
+
detail: { categories: cats, queueSize: brainCtx.feedbackQueue.entries.length }
|
|
141
|
+
}));
|
|
142
|
+
// Persist flat feedback for compat with non-Brain flow
|
|
143
|
+
session.last_reviewer_feedback = review.blocking_issues
|
|
144
|
+
.map((x, idx) => `R-${idx + 1} [${x.severity || "medium"}]: ${x.description}`)
|
|
145
|
+
.join("\n");
|
|
146
|
+
await saveSession(session);
|
|
147
|
+
return null; // null = continue to next iteration (coder fixes)
|
|
148
|
+
}
|
|
149
|
+
logger.info("Brain: reviewer rejected with style-only issues — consulting Solomon for dilemma");
|
|
150
|
+
}
|
|
151
|
+
|
|
126
152
|
// When Solomon is disabled, only act on stalls (legacy behavior)
|
|
127
153
|
if (!solomonEnabled) {
|
|
128
154
|
if (!repeatState.stalled) return null;
|
|
@@ -159,7 +185,7 @@ export async function fetchReviewDiff(session, logger) {
|
|
|
159
185
|
return generateDiff({ baseRef: session.session_start_sha, stageNewFiles: true });
|
|
160
186
|
}
|
|
161
187
|
|
|
162
|
-
export async function runReviewerStage({ reviewerRole, config, logger, emitter, eventBase, session, trackBudget, iteration, reviewRules, task, repeatDetector, budgetSummary, askQuestion }) {
|
|
188
|
+
export async function runReviewerStage({ reviewerRole, config, logger, emitter, eventBase, session, trackBudget, iteration, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx }) {
|
|
163
189
|
logger.setContext({ iteration, stage: "reviewer" });
|
|
164
190
|
emitProgress(
|
|
165
191
|
emitter,
|
|
@@ -200,12 +226,7 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
200
226
|
};
|
|
201
227
|
}
|
|
202
228
|
|
|
203
|
-
const reviewerOnOutput = (
|
|
204
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "reviewer" }, {
|
|
205
|
-
message: line,
|
|
206
|
-
detail: { stream, agent: reviewerRole.provider }
|
|
207
|
-
}));
|
|
208
|
-
};
|
|
229
|
+
const reviewerOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "reviewer", reviewerRole.provider, payload);
|
|
209
230
|
const reviewerStall = createStallDetector({
|
|
210
231
|
onOutput: reviewerOnOutput, emitter, eventBase, stage: "reviewer", provider: reviewerRole.provider
|
|
211
232
|
});
|
|
@@ -326,13 +347,19 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
326
347
|
});
|
|
327
348
|
}
|
|
328
349
|
|
|
350
|
+
// Feedback signature for stale detection: concatenated issue descriptions
|
|
351
|
+
const feedbackSignature = review.blocking_issues
|
|
352
|
+
.map(x => (x.description || "").slice(0, 120))
|
|
353
|
+
.join("|") || "approved";
|
|
354
|
+
|
|
329
355
|
await addCheckpoint(session, {
|
|
330
356
|
stage: "reviewer",
|
|
331
357
|
iteration,
|
|
332
358
|
approved: review.approved,
|
|
333
359
|
blocking_issues: review.blocking_issues.length,
|
|
334
360
|
provider: reviewerRole.provider,
|
|
335
|
-
model: reviewerRole.model || null
|
|
361
|
+
model: reviewerRole.model || null,
|
|
362
|
+
note: feedbackSignature
|
|
336
363
|
});
|
|
337
364
|
|
|
338
365
|
emitProgress(
|
|
@@ -353,7 +380,7 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
353
380
|
);
|
|
354
381
|
|
|
355
382
|
if (!review.approved) {
|
|
356
|
-
const rejectionResult = await handleReviewerRejection({ review, repeatDetector, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary });
|
|
383
|
+
const rejectionResult = await handleReviewerRejection({ review, repeatDetector, config, logger, emitter, eventBase, session, iteration, task, askQuestion, budgetSummary, brainCtx });
|
|
357
384
|
if (rejectionResult) return rejectionResult;
|
|
358
385
|
}
|
|
359
386
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { TriageRole } from "../../roles/triage-role.js";
|
|
7
7
|
import { addCheckpoint } from "../../session-store.js";
|
|
8
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
8
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
9
9
|
import { selectModelsForRoles } from "../../utils/model-selector.js";
|
|
10
10
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
11
11
|
|
|
@@ -53,12 +53,7 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
|
|
|
53
53
|
})
|
|
54
54
|
);
|
|
55
55
|
|
|
56
|
-
const triageOnOutput = (
|
|
57
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "triage" }, {
|
|
58
|
-
message: line,
|
|
59
|
-
detail: { stream, agent: triageProvider }
|
|
60
|
-
}));
|
|
61
|
-
};
|
|
56
|
+
const triageOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "triage", triageProvider, payload);
|
|
62
57
|
const triageStall = createStallDetector({
|
|
63
58
|
onOutput: triageOnOutput, emitter, eventBase, stage: "triage", provider: triageProvider
|
|
64
59
|
});
|
package/src/orchestrator.js
CHANGED
|
@@ -71,14 +71,20 @@ export const parseCheckpointAnswer = _parseCheckpointAnswer;
|
|
|
71
71
|
|
|
72
72
|
// PG card "In Progress" logic moved to src/planning-game/pipeline-adapter.js → initPgAdapter()
|
|
73
73
|
|
|
74
|
-
async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion }) {
|
|
74
|
+
async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion, brainCtx }) {
|
|
75
75
|
let researchContext = null;
|
|
76
76
|
let plannedTask = task;
|
|
77
77
|
|
|
78
|
+
// Brain: track compression across pre-loop roles
|
|
79
|
+
const brainCompress = brainCtx?.enabled
|
|
80
|
+
? (await import("./orchestrator/brain-coordinator.js")).processRoleOutput
|
|
81
|
+
: null;
|
|
82
|
+
|
|
78
83
|
if (pipelineFlags.researcherEnabled) {
|
|
79
84
|
const researcherResult = await runResearcherStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
|
|
80
85
|
researchContext = researcherResult.researchContext;
|
|
81
86
|
stageResults.researcher = researcherResult.stageResult;
|
|
87
|
+
if (brainCompress) brainCompress(brainCtx, { roleName: "researcher", output: researcherResult.stageResult, iteration: 0 });
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
// --- Architect (between researcher and planner) ---
|
|
@@ -93,6 +99,7 @@ async function runPlanningPhases({ config, logger, emitter, eventBase, session,
|
|
|
93
99
|
});
|
|
94
100
|
architectContext = architectResult.architectContext;
|
|
95
101
|
stageResults.architect = architectResult.stageResult;
|
|
102
|
+
if (brainCompress) brainCompress(brainCtx, { roleName: "architect", output: architectResult.stageResult, iteration: 0 });
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
const triageDecomposition = stageResults.triage?.shouldDecompose ? stageResults.triage.subtasks : null;
|
|
@@ -101,6 +108,7 @@ async function runPlanningPhases({ config, logger, emitter, eventBase, session,
|
|
|
101
108
|
const plannerResult = await runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, architectContext, triageDecomposition, trackBudget });
|
|
102
109
|
plannedTask = plannerResult.plannedTask;
|
|
103
110
|
stageResults.planner = plannerResult.stageResult;
|
|
111
|
+
if (brainCompress) brainCompress(brainCtx, { roleName: "planner", output: plannerResult.stageResult, iteration: 0 });
|
|
104
112
|
|
|
105
113
|
await tryCiComment({
|
|
106
114
|
config, session, logger,
|
|
@@ -201,16 +209,16 @@ async function handleStandbyResult({ stageResult, session, emitter, eventBase, i
|
|
|
201
209
|
|
|
202
210
|
function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
|
|
203
211
|
for (const alert of alerts) {
|
|
204
|
-
emitProgress(emitter, makeEvent("
|
|
212
|
+
emitProgress(emitter, makeEvent("brain:rules-alert", { ...eventBase, stage: "brain" }, {
|
|
205
213
|
status: alert.severity === "critical" ? "fail" : "warn",
|
|
206
214
|
message: alert.message,
|
|
207
215
|
detail: alert.detail
|
|
208
216
|
}));
|
|
209
|
-
logger.warn(`
|
|
217
|
+
logger.warn(`Rules alert [${alert.rule}]: ${alert.message}`);
|
|
210
218
|
}
|
|
211
219
|
}
|
|
212
220
|
|
|
213
|
-
async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, ciEnabled, blockingIssues }) {
|
|
221
|
+
async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, ciEnabled, blockingIssues, brainCtx }) {
|
|
214
222
|
if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
|
|
215
223
|
|
|
216
224
|
try {
|
|
@@ -220,8 +228,45 @@ async function handleSolomonCheck({ config, session, emitter, eventBase, logger,
|
|
|
220
228
|
|
|
221
229
|
if (rulesResult.alerts.length > 0) {
|
|
222
230
|
emitSolomonAlerts(rulesResult.alerts, emitter, eventBase, logger);
|
|
223
|
-
|
|
224
|
-
|
|
231
|
+
// Brain gateway: when Brain is the orchestrator, rule alerts are telemetry.
|
|
232
|
+
// On critical alerts Brain consults Solomon (AI judge). Only if Solomon can't
|
|
233
|
+
// resolve does Brain escalate to human. Solomon-rules never prompts directly.
|
|
234
|
+
if (!brainCtx?.enabled) {
|
|
235
|
+
const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
|
|
236
|
+
if (pauseResult) return pauseResult;
|
|
237
|
+
} else if (rulesResult.hasCritical) {
|
|
238
|
+
const criticalAlerts = rulesResult.alerts.filter(a => a.severity === "critical");
|
|
239
|
+
brainCtx.ruleAlerts = brainCtx.ruleAlerts || [];
|
|
240
|
+
brainCtx.ruleAlerts.push(...criticalAlerts);
|
|
241
|
+
logger.info(`Brain: ${criticalAlerts.length} critical rule alert(s) — consulting Solomon`);
|
|
242
|
+
|
|
243
|
+
const { invokeSolomon } = await import("./orchestrator/solomon-escalation.js");
|
|
244
|
+
const alertSummary = criticalAlerts.map(a => a.message).join("; ");
|
|
245
|
+
const solomonOpinion = await invokeSolomon({
|
|
246
|
+
config, logger, emitter, eventBase, stage: "brain-dilemma", askQuestion, session, iteration: i,
|
|
247
|
+
conflict: {
|
|
248
|
+
stage: "brain-dilemma",
|
|
249
|
+
task,
|
|
250
|
+
iterationCount: i,
|
|
251
|
+
maxIterations: config.max_iterations,
|
|
252
|
+
dilemma: `Brain detected critical rule alerts: ${alertSummary}. Should we continue iterating, pause for human, or stop?`,
|
|
253
|
+
ruleAlerts: criticalAlerts,
|
|
254
|
+
blockingIssues: blockingIssues || [],
|
|
255
|
+
history: (session.checkpoints || []).filter(cp => cp.stage === "reviewer").slice(-5).map(cp => ({ iteration: cp.iteration, feedback: cp.note || "" }))
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (solomonOpinion.action === "pause") {
|
|
260
|
+
logger.info("Brain: Solomon advised pause — escalating to human");
|
|
261
|
+
return { action: "return", result: { paused: true, sessionId: session.id, question: solomonOpinion.question || `Brain+Solomon paused: ${alertSummary}`, context: "brain_solomon_dilemma" } };
|
|
262
|
+
}
|
|
263
|
+
if (solomonOpinion.action === "approve") {
|
|
264
|
+
logger.info("Brain: Solomon advised proceeding — treating as approved");
|
|
265
|
+
return { action: "continue", approved: true };
|
|
266
|
+
}
|
|
267
|
+
// action === "continue" | "subtask" | fallback → Brain continues the loop
|
|
268
|
+
logger.info(`Brain: Solomon said '${solomonOpinion.action}' — continuing loop`);
|
|
269
|
+
}
|
|
225
270
|
}
|
|
226
271
|
|
|
227
272
|
if (ciEnabled && session.ci_pr_number) {
|
|
@@ -276,7 +321,7 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
|
|
|
276
321
|
}
|
|
277
322
|
|
|
278
323
|
|
|
279
|
-
async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, ciEnabled, testerEnabled, securityEnabled, askQuestion, logger }) {
|
|
324
|
+
async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, ciEnabled, testerEnabled, securityEnabled, askQuestion, logger, brainCtx }) {
|
|
280
325
|
const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
|
|
281
326
|
|
|
282
327
|
if (testerEnabled) {
|
|
@@ -290,6 +335,11 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
290
335
|
session.last_reviewer_feedback = `Tester FAILED — fix these issues:\n${summary}`;
|
|
291
336
|
await saveSession(session);
|
|
292
337
|
if (testerResult.stageResult) stageResults.tester = testerResult.stageResult;
|
|
338
|
+
// Brain: push tester failure into feedback queue + compress for next coder iteration
|
|
339
|
+
if (brainCtx?.enabled) {
|
|
340
|
+
const { processRoleOutput } = await import("./orchestrator/brain-coordinator.js");
|
|
341
|
+
processRoleOutput(brainCtx, { roleName: "tester", output: testerResult.stageResult, iteration: i });
|
|
342
|
+
}
|
|
293
343
|
return { action: "continue" };
|
|
294
344
|
}
|
|
295
345
|
if (testerResult.stageResult) {
|
|
@@ -309,6 +359,11 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
309
359
|
session.last_reviewer_feedback = `Security FAILED — fix these issues:\n${summary}`;
|
|
310
360
|
await saveSession(session);
|
|
311
361
|
if (securityResult.stageResult) stageResults.security = securityResult.stageResult;
|
|
362
|
+
// Brain: push security findings into feedback queue + compress for next coder iteration
|
|
363
|
+
if (brainCtx?.enabled) {
|
|
364
|
+
const { processRoleOutput } = await import("./orchestrator/brain-coordinator.js");
|
|
365
|
+
processRoleOutput(brainCtx, { roleName: "security", output: securityResult.stageResult, iteration: i });
|
|
366
|
+
}
|
|
312
367
|
return { action: "continue" };
|
|
313
368
|
}
|
|
314
369
|
if (securityResult.stageResult) {
|
|
@@ -457,7 +512,7 @@ async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBa
|
|
|
457
512
|
}
|
|
458
513
|
|
|
459
514
|
|
|
460
|
-
async function runPreLoopStages({ config, logger, emitter, eventBase, session, flags, pipelineFlags, coderRole, trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults }) {
|
|
515
|
+
async function runPreLoopStages({ config, logger, emitter, eventBase, session, flags, pipelineFlags, coderRole, trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults, brainCtx }) {
|
|
461
516
|
// --- HU Reviewer (first stage, before everything else, opt-in) ---
|
|
462
517
|
const huFile = flags.huFile || null;
|
|
463
518
|
if (flags.enableHuReviewer !== undefined) pipelineFlags.huReviewerEnabled = Boolean(flags.enableHuReviewer);
|
|
@@ -626,7 +681,7 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
626
681
|
}
|
|
627
682
|
|
|
628
683
|
// --- Researcher → Planner ---
|
|
629
|
-
const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion });
|
|
684
|
+
const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion, brainCtx });
|
|
630
685
|
|
|
631
686
|
// --- Update .gitignore with stack-specific entries based on planner/architect output ---
|
|
632
687
|
const projectDir = updatedConfig.projectDir || process.cwd();
|
|
@@ -661,8 +716,8 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
661
716
|
return { plannedTask, updatedConfig };
|
|
662
717
|
}
|
|
663
718
|
|
|
664
|
-
async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i }) {
|
|
665
|
-
const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
|
|
719
|
+
async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i, brainCtx }) {
|
|
720
|
+
const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i, brainCtx });
|
|
666
721
|
if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
|
|
667
722
|
const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger, config });
|
|
668
723
|
if (coderStandby.handled) {
|
|
@@ -795,7 +850,7 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
|
|
|
795
850
|
return { action: "ok" };
|
|
796
851
|
}
|
|
797
852
|
|
|
798
|
-
async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion }) {
|
|
853
|
+
async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx }) {
|
|
799
854
|
if (!pipelineFlags.reviewerEnabled) {
|
|
800
855
|
return {
|
|
801
856
|
action: "ok",
|
|
@@ -805,7 +860,7 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
|
|
|
805
860
|
|
|
806
861
|
const reviewerResult = await runReviewerStage({
|
|
807
862
|
reviewerRole, config, logger, emitter, eventBase, session, trackBudget,
|
|
808
|
-
iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion
|
|
863
|
+
iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx
|
|
809
864
|
});
|
|
810
865
|
if (reviewerResult.action === "pause") return { action: "return", result: reviewerResult.result };
|
|
811
866
|
const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger, config, askQuestion });
|
|
@@ -833,11 +888,11 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
|
|
|
833
888
|
return { action: "ok", review: reviewerResult.review };
|
|
834
889
|
}
|
|
835
890
|
|
|
836
|
-
async function handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review, rtkTracker }) {
|
|
891
|
+
async function handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review, rtkTracker, brainCtx }) {
|
|
837
892
|
session.reviewer_retry_count = 0;
|
|
838
893
|
const postLoopResult = await handlePostLoopStages({
|
|
839
894
|
config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
|
|
840
|
-
ciEnabled: Boolean(config.ci?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger
|
|
895
|
+
ciEnabled: Boolean(config.ci?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger, brainCtx
|
|
841
896
|
});
|
|
842
897
|
if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
|
|
843
898
|
if (postLoopResult.action === "continue") return { action: "continue" };
|
|
@@ -846,8 +901,72 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
|
|
|
846
901
|
return { action: "return", result };
|
|
847
902
|
}
|
|
848
903
|
|
|
849
|
-
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker }) {
|
|
904
|
+
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker, brainCtx }) {
|
|
850
905
|
const budget = budgetSummary();
|
|
906
|
+
|
|
907
|
+
// Brain-owned decision: max_iterations is guidance, not a hard rule.
|
|
908
|
+
// Brain evaluates the feedback queue state to decide extend / finalize / escalate.
|
|
909
|
+
// Solomon is only consulted if Brain cannot decide on its own.
|
|
910
|
+
if (brainCtx?.enabled) {
|
|
911
|
+
const entries = brainCtx.feedbackQueue?.entries || [];
|
|
912
|
+
const pending = entries.map(e => ({ source: e.source, category: e.category, severity: e.severity, description: e.description }));
|
|
913
|
+
const hasSecurity = entries.some(e => e.category === "security" || e.source === "security");
|
|
914
|
+
const hasCorrectness = entries.some(e => ["correctness", "tests"].includes(e.category));
|
|
915
|
+
const hasStyleOnly = entries.length > 0 && !hasSecurity && !hasCorrectness;
|
|
916
|
+
|
|
917
|
+
if (hasSecurity) {
|
|
918
|
+
// Brain: security issues unresolved → cannot finalize, escalate
|
|
919
|
+
logger.warn(`Brain: max_iterations reached with ${entries.filter(e => e.category === "security" || e.source === "security").length} unresolved security issue(s) — cannot finalize`);
|
|
920
|
+
return { paused: true, sessionId: session.id, question: "Brain: unresolved security issues at max_iterations. Review manually or extend pipeline.", context: "brain_security_block", pending };
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (hasCorrectness) {
|
|
924
|
+
// Brain: correctness/test issues pending → extend iterations (Brain's decision, not a rule)
|
|
925
|
+
logger.info(`Brain: max_iterations reached with ${entries.filter(e => ["correctness", "tests"].includes(e.category)).length} correctness issue(s) pending — extending iterations`);
|
|
926
|
+
session.reviewer_retry_count = 0;
|
|
927
|
+
await saveSession(session);
|
|
928
|
+
return { approved: false, sessionId: session.id, reason: "max_iterations_extended", extraIterations: Math.ceil(config.max_iterations / 2) };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (entries.length === 0) {
|
|
932
|
+
// Brain: no pending feedback → last reviewer approved, finalize
|
|
933
|
+
logger.info("Brain: max_iterations reached with clean feedback queue — finalizing as approved");
|
|
934
|
+
return { approved: true, sessionId: session.id, reason: "brain_approved" };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// hasStyleOnly: genuine dilemma → Brain consults Solomon
|
|
938
|
+
logger.info(`Brain: max_iterations with ${entries.length} style-only issue(s) — consulting Solomon on dilemma`);
|
|
939
|
+
const { invokeSolomon: invokeSolomonAI } = await import("./orchestrator/solomon-escalation.js");
|
|
940
|
+
const solomonResult = await invokeSolomonAI({
|
|
941
|
+
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
942
|
+
iteration: config.max_iterations,
|
|
943
|
+
conflict: {
|
|
944
|
+
stage: "brain-max-iterations",
|
|
945
|
+
task,
|
|
946
|
+
iterationCount: config.max_iterations,
|
|
947
|
+
maxIterations: config.max_iterations,
|
|
948
|
+
budget_usd: budget?.total_cost_usd || 0,
|
|
949
|
+
dilemma: `Max iterations reached with ${entries.length} style-only issue(s) pending. Accept as-is or request more work?`,
|
|
950
|
+
pendingIssues: pending,
|
|
951
|
+
history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached" }]
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
// Brain applies Solomon's decision
|
|
955
|
+
if (solomonResult.action === "approve") {
|
|
956
|
+
logger.info("Brain: Solomon advised approve for style-only pending — finalizing");
|
|
957
|
+
return { approved: true, sessionId: session.id, reason: "brain_solomon_approved" };
|
|
958
|
+
}
|
|
959
|
+
if (solomonResult.action === "continue") {
|
|
960
|
+
return { approved: false, sessionId: session.id, reason: "max_iterations_extended", extraIterations: solomonResult.extraIterations || Math.ceil(config.max_iterations / 2) };
|
|
961
|
+
}
|
|
962
|
+
if (solomonResult.action === "pause") {
|
|
963
|
+
return { paused: true, sessionId: session.id, question: solomonResult.question, context: "brain_solomon_dilemma" };
|
|
964
|
+
}
|
|
965
|
+
// Fallback: escalate to human
|
|
966
|
+
return { paused: true, sessionId: session.id, question: `Brain+Solomon cannot resolve: ${entries.length} pending issue(s) at max_iterations`, context: "max_iterations" };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Legacy path (Brain disabled): original Solomon-driven flow
|
|
851
970
|
const solomonResult = await invokeSolomon({
|
|
852
971
|
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
853
972
|
iteration: config.max_iterations,
|
|
@@ -861,13 +980,11 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
|
|
|
861
980
|
}
|
|
862
981
|
});
|
|
863
982
|
|
|
864
|
-
// Solomon approved the work — treat as successful completion
|
|
865
983
|
if (solomonResult.action === "approve") {
|
|
866
984
|
logger.info("Solomon approved coder's work at max_iterations checkpoint");
|
|
867
985
|
return { approved: true, sessionId: session.id, reason: "solomon_approved" };
|
|
868
986
|
}
|
|
869
987
|
|
|
870
|
-
// Solomon says continue — extend iterations
|
|
871
988
|
if (solomonResult.action === "continue") {
|
|
872
989
|
if (solomonResult.humanGuidance) {
|
|
873
990
|
session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
|
|
@@ -1014,6 +1131,13 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1014
1131
|
ctx.session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
|
|
1015
1132
|
ctx.eventBase.sessionId = ctx.session.id;
|
|
1016
1133
|
|
|
1134
|
+
// Karajan Brain: initialize runtime context (opt-in via config.brain.enabled)
|
|
1135
|
+
const { createBrainContext, isBrainEnabled } = await import("./orchestrator/brain-coordinator.js");
|
|
1136
|
+
ctx.brainCtx = createBrainContext({ enabled: isBrainEnabled(config) });
|
|
1137
|
+
if (ctx.brainCtx.enabled) {
|
|
1138
|
+
logger.info("Karajan Brain enabled — feedback queue, verification, compression active");
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1017
1141
|
const { initPgAdapter } = await import("./planning-game/pipeline-adapter.js");
|
|
1018
1142
|
const pgAdapterResult = await initPgAdapter({ session: ctx.session, config, logger, pgTaskId, pgProject });
|
|
1019
1143
|
ctx.pgCard = pgAdapterResult.pgCard;
|
|
@@ -1030,7 +1154,7 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1030
1154
|
ctx.stageResults = {};
|
|
1031
1155
|
ctx.sonarState = { issuesInitial: null, issuesFinal: null };
|
|
1032
1156
|
|
|
1033
|
-
const preLoopResult = await runPreLoopStages({ config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session, flags, pipelineFlags: ctx.pipelineFlags, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults: ctx.stageResults });
|
|
1157
|
+
const preLoopResult = await runPreLoopStages({ config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session, flags, pipelineFlags: ctx.pipelineFlags, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults: ctx.stageResults, brainCtx: ctx.brainCtx });
|
|
1034
1158
|
ctx.plannedTask = preLoopResult.plannedTask;
|
|
1035
1159
|
ctx.config = preLoopResult.updatedConfig;
|
|
1036
1160
|
|
|
@@ -1094,7 +1218,7 @@ async function runSingleIteration(ctx) {
|
|
|
1094
1218
|
const crResult = await runCoderAndRefactorerStages({
|
|
1095
1219
|
coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole, refactorerRole: ctx.refactorerRole,
|
|
1096
1220
|
pipelineFlags: ctx.pipelineFlags, config, logger, emitter, eventBase, session,
|
|
1097
|
-
plannedTask: ctx.plannedTask, trackBudget: ctx.trackBudget, i
|
|
1221
|
+
plannedTask: ctx.plannedTask, trackBudget: ctx.trackBudget, i, brainCtx: ctx.brainCtx
|
|
1098
1222
|
});
|
|
1099
1223
|
if (crResult.action === "return" || crResult.action === "retry") return crResult;
|
|
1100
1224
|
|
|
@@ -1117,7 +1241,8 @@ async function runSingleIteration(ctx) {
|
|
|
1117
1241
|
const revResult = await runReviewerGateStage({
|
|
1118
1242
|
pipelineFlags: ctx.pipelineFlags, reviewerRole: ctx.reviewerRole, config, logger, emitter, eventBase,
|
|
1119
1243
|
session, trackBudget: ctx.trackBudget, i, reviewRules: ctx.reviewRules, task,
|
|
1120
|
-
repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary, askQuestion: ctx.askQuestion
|
|
1244
|
+
repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary, askQuestion: ctx.askQuestion,
|
|
1245
|
+
brainCtx: ctx.brainCtx
|
|
1121
1246
|
});
|
|
1122
1247
|
if (revResult.action === "return" || revResult.action === "retry") return revResult;
|
|
1123
1248
|
const review = revResult.review;
|
|
@@ -1143,7 +1268,7 @@ async function runSingleIteration(ctx) {
|
|
|
1143
1268
|
|
|
1144
1269
|
const solomonResult = await handleSolomonCheck({
|
|
1145
1270
|
config, session, emitter, eventBase, logger, task, i, askQuestion: ctx.askQuestion,
|
|
1146
|
-
ciEnabled, blockingIssues: review?.blocking_issues
|
|
1271
|
+
ciEnabled, blockingIssues: review?.blocking_issues, brainCtx: ctx.brainCtx
|
|
1147
1272
|
});
|
|
1148
1273
|
if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
|
|
1149
1274
|
|
|
@@ -1154,7 +1279,7 @@ async function runSingleIteration(ctx) {
|
|
|
1154
1279
|
config, session, emitter, eventBase, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, i, task,
|
|
1155
1280
|
stageResults: ctx.stageResults, pipelineFlags: ctx.pipelineFlags, askQuestion: ctx.askQuestion, logger,
|
|
1156
1281
|
gitCtx: ctx.gitCtx, budgetSummary: ctx.budgetSummary, pgCard: ctx.pgCard, pgProject: ctx.pgProject, review,
|
|
1157
|
-
rtkTracker: ctx.rtkTracker
|
|
1282
|
+
rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx
|
|
1158
1283
|
});
|
|
1159
1284
|
if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
|
|
1160
1285
|
}
|
|
@@ -1275,7 +1400,7 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
|
|
|
1275
1400
|
}
|
|
1276
1401
|
|
|
1277
1402
|
// Solomon decides whether to extend iterations or stop
|
|
1278
|
-
const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker });
|
|
1403
|
+
const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx });
|
|
1279
1404
|
|
|
1280
1405
|
// Solomon said "continue" — extend iterations and keep going
|
|
1281
1406
|
if (maxIterResult.reason === "max_iterations_extended") {
|
|
@@ -1313,7 +1438,7 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
|
|
|
1313
1438
|
}
|
|
1314
1439
|
|
|
1315
1440
|
// Extended iterations also exhausted — final Solomon call
|
|
1316
|
-
const finalResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker });
|
|
1441
|
+
const finalResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx });
|
|
1317
1442
|
return finalResult;
|
|
1318
1443
|
}
|
|
1319
1444
|
|
package/src/utils/display.js
CHANGED
|
@@ -45,6 +45,7 @@ const ICONS = {
|
|
|
45
45
|
"solomon:escalate": "\u26a0\ufe0f",
|
|
46
46
|
"coder:standby": "\u23f3",
|
|
47
47
|
"coder:standby_heartbeat": "\u23f3",
|
|
48
|
+
"agent:heartbeat": "\u23f3",
|
|
48
49
|
"coder:standby_resume": "\u25b6\ufe0f",
|
|
49
50
|
"budget:update": "\ud83d\udcb8",
|
|
50
51
|
"iteration:end": "\u23f1\ufe0f",
|
|
@@ -84,7 +85,8 @@ const BAR = `${ANSI.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u
|
|
|
84
85
|
|
|
85
86
|
export function printHeader({ task, config }) {
|
|
86
87
|
const version = DISPLAY_VERSION;
|
|
87
|
-
|
|
88
|
+
// Force banner: caller already gates on !jsonMode, so if we're here it's a human-readable run
|
|
89
|
+
printBanner(version, { force: true });
|
|
88
90
|
console.log(`${ANSI.bold}Task:${ANSI.reset} ${task}`);
|
|
89
91
|
console.log(
|
|
90
92
|
`${ANSI.bold}Coder:${ANSI.reset} ${config.roles?.coder?.provider || config.coder} ${ANSI.dim}|${ANSI.reset} ${ANSI.bold}Reviewer:${ANSI.reset} ${config.roles?.reviewer?.provider || config.reviewer}`
|
|
@@ -437,6 +439,15 @@ const EVENT_HANDLERS = {
|
|
|
437
439
|
console.log(` \u251c\u2500 ${ANSI.yellow}${icon} Standby: ${remaining}s remaining${ANSI.reset}`);
|
|
438
440
|
},
|
|
439
441
|
|
|
442
|
+
"agent:heartbeat": (event, icon) => {
|
|
443
|
+
const d = event.detail || {};
|
|
444
|
+
const provider = d.provider || "?";
|
|
445
|
+
const elapsed = d.elapsedMs ? Math.round(d.elapsedMs / 1000) : 0;
|
|
446
|
+
const silent = d.silenceMs ? Math.round(d.silenceMs / 1000) : 0;
|
|
447
|
+
const status = d.status === "waiting" ? `${ANSI.yellow}waiting (silent ${silent}s)` : `${ANSI.green}working`;
|
|
448
|
+
console.log(` \u251c\u2500 ${ANSI.dim}${icon} ${provider} ${status}${ANSI.dim} — ${elapsed}s elapsed${ANSI.reset}`);
|
|
449
|
+
},
|
|
450
|
+
|
|
440
451
|
"coder:standby_resume": (event, icon) => {
|
|
441
452
|
console.log(` \u251c\u2500 ${ANSI.green}${icon} Cooldown expired \u2014 resuming with ${event.detail?.coder || event.detail?.provider || "?"}${ANSI.reset}`);
|
|
442
453
|
},
|
|
@@ -593,12 +604,16 @@ const EVENT_HANDLERS = {
|
|
|
593
604
|
console.log(` \u251c\u2500 ${ANSI.green}\ud83d\ude80 CI PR created: ${url}${ANSI.reset}`);
|
|
594
605
|
},
|
|
595
606
|
|
|
596
|
-
"
|
|
597
|
-
console.log(` \u251c\u2500 ${ANSI.yellow}\
|
|
607
|
+
"brain:rules-alert": (event) => {
|
|
608
|
+
console.log(` \u251c\u2500 ${ANSI.yellow}\u26a0\ufe0f Rules alert: ${event.message || "anomaly detected"}${ANSI.reset}`);
|
|
598
609
|
},
|
|
599
610
|
|
|
600
611
|
"agent:output": (event) => {
|
|
601
612
|
console.log(` \u2502 ${ANSI.dim}${event.message}${ANSI.reset}`);
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
"agent:action": (event) => {
|
|
616
|
+
console.log(` \u2502 ${ANSI.dim}\u2937 ${event.message}${ANSI.reset}`);
|
|
602
617
|
}
|
|
603
618
|
};
|
|
604
619
|
|
|
@@ -608,7 +623,6 @@ const EVENT_HANDLERS = {
|
|
|
608
623
|
const QUIET_SUPPRESSED = new Set([
|
|
609
624
|
"agent:output",
|
|
610
625
|
"agent:stall",
|
|
611
|
-
"agent:heartbeat",
|
|
612
626
|
"pipeline:simplify",
|
|
613
627
|
"pipeline:analysis-only",
|
|
614
628
|
"policies:resolved",
|
package/src/utils/events.js
CHANGED
|
@@ -21,3 +21,15 @@ export function makeEvent(type, base, extra = {}) {
|
|
|
21
21
|
timestamp: new Date().toISOString()
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Standard agent output emitter. Routes tool invocations to agent:action
|
|
27
|
+
* (visible in quiet mode) and everything else to agent:output (verbose only).
|
|
28
|
+
*/
|
|
29
|
+
export function emitAgentOutput(emitter, eventBase, stage, provider, { stream, line, kind }) {
|
|
30
|
+
const eventType = kind === "tool" ? "agent:action" : "agent:output";
|
|
31
|
+
emitProgress(emitter, makeEvent(eventType, { ...eventBase, stage }, {
|
|
32
|
+
message: line,
|
|
33
|
+
detail: { stream, agent: provider, kind }
|
|
34
|
+
}));
|
|
35
|
+
}
|