karajan-code 2.0.1 → 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/orchestrator/stages/architect-stage.js +2 -7
- package/src/orchestrator/stages/coder-stage.js +3 -13
- 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 +2 -7
- package/src/orchestrator/stages/triage-stage.js +2 -7
- package/src/orchestrator.js +95 -15
- 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 */ }
|
|
@@ -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,7 +10,7 @@ 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";
|
|
@@ -37,12 +37,7 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const coderOnOutput = (
|
|
41
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "coder" }, {
|
|
42
|
-
message: line,
|
|
43
|
-
detail: { stream, agent: coderRole.provider }
|
|
44
|
-
}));
|
|
45
|
-
};
|
|
40
|
+
const coderOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "coder", coderRole.provider, payload);
|
|
46
41
|
const coderStall = createStallDetector({
|
|
47
42
|
onOutput: coderOnOutput, emitter, eventBase, stage: "coder", provider: coderRole.provider
|
|
48
43
|
});
|
|
@@ -187,12 +182,7 @@ export async function runRefactorerStage({ refactorerRole, config, logger, emitt
|
|
|
187
182
|
detail: { refactorer: refactorerRole.provider, provider: refactorerRole.provider, executorType: "agent" }
|
|
188
183
|
})
|
|
189
184
|
);
|
|
190
|
-
const refactorerOnOutput = (
|
|
191
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "refactorer" }, {
|
|
192
|
-
message: line,
|
|
193
|
-
detail: { stream, agent: refactorerRole.provider }
|
|
194
|
-
}));
|
|
195
|
-
};
|
|
185
|
+
const refactorerOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "refactorer", refactorerRole.provider, payload);
|
|
196
186
|
const refactorerStall = createStallDetector({
|
|
197
187
|
onOutput: refactorerOnOutput, emitter, eventBase, stage: "refactorer", provider: refactorerRole.provider
|
|
198
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";
|
|
@@ -226,12 +226,7 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
226
226
|
};
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
const reviewerOnOutput = (
|
|
230
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "reviewer" }, {
|
|
231
|
-
message: line,
|
|
232
|
-
detail: { stream, agent: reviewerRole.provider }
|
|
233
|
-
}));
|
|
234
|
-
};
|
|
229
|
+
const reviewerOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "reviewer", reviewerRole.provider, payload);
|
|
235
230
|
const reviewerStall = createStallDetector({
|
|
236
231
|
onOutput: reviewerOnOutput, emitter, eventBase, stage: "reviewer", provider: reviewerRole.provider
|
|
237
232
|
});
|
|
@@ -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,12 +209,12 @@ 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
|
|
|
@@ -313,7 +321,7 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
|
|
|
313
321
|
}
|
|
314
322
|
|
|
315
323
|
|
|
316
|
-
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 }) {
|
|
317
325
|
const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
|
|
318
326
|
|
|
319
327
|
if (testerEnabled) {
|
|
@@ -327,6 +335,11 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
327
335
|
session.last_reviewer_feedback = `Tester FAILED — fix these issues:\n${summary}`;
|
|
328
336
|
await saveSession(session);
|
|
329
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
|
+
}
|
|
330
343
|
return { action: "continue" };
|
|
331
344
|
}
|
|
332
345
|
if (testerResult.stageResult) {
|
|
@@ -346,6 +359,11 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
346
359
|
session.last_reviewer_feedback = `Security FAILED — fix these issues:\n${summary}`;
|
|
347
360
|
await saveSession(session);
|
|
348
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
|
+
}
|
|
349
367
|
return { action: "continue" };
|
|
350
368
|
}
|
|
351
369
|
if (securityResult.stageResult) {
|
|
@@ -494,7 +512,7 @@ async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBa
|
|
|
494
512
|
}
|
|
495
513
|
|
|
496
514
|
|
|
497
|
-
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 }) {
|
|
498
516
|
// --- HU Reviewer (first stage, before everything else, opt-in) ---
|
|
499
517
|
const huFile = flags.huFile || null;
|
|
500
518
|
if (flags.enableHuReviewer !== undefined) pipelineFlags.huReviewerEnabled = Boolean(flags.enableHuReviewer);
|
|
@@ -663,7 +681,7 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
663
681
|
}
|
|
664
682
|
|
|
665
683
|
// --- Researcher → Planner ---
|
|
666
|
-
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 });
|
|
667
685
|
|
|
668
686
|
// --- Update .gitignore with stack-specific entries based on planner/architect output ---
|
|
669
687
|
const projectDir = updatedConfig.projectDir || process.cwd();
|
|
@@ -870,11 +888,11 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
|
|
|
870
888
|
return { action: "ok", review: reviewerResult.review };
|
|
871
889
|
}
|
|
872
890
|
|
|
873
|
-
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 }) {
|
|
874
892
|
session.reviewer_retry_count = 0;
|
|
875
893
|
const postLoopResult = await handlePostLoopStages({
|
|
876
894
|
config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
|
|
877
|
-
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
|
|
878
896
|
});
|
|
879
897
|
if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
|
|
880
898
|
if (postLoopResult.action === "continue") return { action: "continue" };
|
|
@@ -883,8 +901,72 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
|
|
|
883
901
|
return { action: "return", result };
|
|
884
902
|
}
|
|
885
903
|
|
|
886
|
-
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 }) {
|
|
887
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
|
|
888
970
|
const solomonResult = await invokeSolomon({
|
|
889
971
|
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
890
972
|
iteration: config.max_iterations,
|
|
@@ -898,13 +980,11 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
|
|
|
898
980
|
}
|
|
899
981
|
});
|
|
900
982
|
|
|
901
|
-
// Solomon approved the work — treat as successful completion
|
|
902
983
|
if (solomonResult.action === "approve") {
|
|
903
984
|
logger.info("Solomon approved coder's work at max_iterations checkpoint");
|
|
904
985
|
return { approved: true, sessionId: session.id, reason: "solomon_approved" };
|
|
905
986
|
}
|
|
906
987
|
|
|
907
|
-
// Solomon says continue — extend iterations
|
|
908
988
|
if (solomonResult.action === "continue") {
|
|
909
989
|
if (solomonResult.humanGuidance) {
|
|
910
990
|
session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
|
|
@@ -1074,7 +1154,7 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1074
1154
|
ctx.stageResults = {};
|
|
1075
1155
|
ctx.sonarState = { issuesInitial: null, issuesFinal: null };
|
|
1076
1156
|
|
|
1077
|
-
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 });
|
|
1078
1158
|
ctx.plannedTask = preLoopResult.plannedTask;
|
|
1079
1159
|
ctx.config = preLoopResult.updatedConfig;
|
|
1080
1160
|
|
|
@@ -1199,7 +1279,7 @@ async function runSingleIteration(ctx) {
|
|
|
1199
1279
|
config, session, emitter, eventBase, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, i, task,
|
|
1200
1280
|
stageResults: ctx.stageResults, pipelineFlags: ctx.pipelineFlags, askQuestion: ctx.askQuestion, logger,
|
|
1201
1281
|
gitCtx: ctx.gitCtx, budgetSummary: ctx.budgetSummary, pgCard: ctx.pgCard, pgProject: ctx.pgProject, review,
|
|
1202
|
-
rtkTracker: ctx.rtkTracker
|
|
1282
|
+
rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx
|
|
1203
1283
|
});
|
|
1204
1284
|
if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
|
|
1205
1285
|
}
|
|
@@ -1320,7 +1400,7 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
|
|
|
1320
1400
|
}
|
|
1321
1401
|
|
|
1322
1402
|
// Solomon decides whether to extend iterations or stop
|
|
1323
|
-
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 });
|
|
1324
1404
|
|
|
1325
1405
|
// Solomon said "continue" — extend iterations and keep going
|
|
1326
1406
|
if (maxIterResult.reason === "max_iterations_extended") {
|
|
@@ -1358,7 +1438,7 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
|
|
|
1358
1438
|
}
|
|
1359
1439
|
|
|
1360
1440
|
// Extended iterations also exhausted — final Solomon call
|
|
1361
|
-
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 });
|
|
1362
1442
|
return finalResult;
|
|
1363
1443
|
}
|
|
1364
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
|
+
}
|