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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0",
@@ -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 text messages
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
- onOutput({ stream, line: block.text.slice(0, 200) });
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: `[tool: ${block.name}]` });
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, 200)
165
+ ? obj.result.split("\n")[0].slice(0, 120)
139
166
  : "result received";
140
- onOutput({ stream, line: `[result] ${summary}` });
167
+ onOutput({ stream, line: `done: ${summary}` });
141
168
  return;
142
169
  }
143
170
  } catch { /* not JSON, forward raw */ }
@@ -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
- const child = spawn("node", [serverPath], {
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 = ({ stream, line }) => {
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
- const coderOnOutput = ({ stream, line }) => {
30
- emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "coder" }, {
31
- message: line,
32
- detail: { stream, agent: coderRole.provider }
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: session.last_reviewer_feedback,
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
- await addCheckpoint(session, { stage: "coder", iteration, note: "Coder applied changes", provider: coderRole.provider, model: coderRole.model || null });
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 = ({ stream, line }) => {
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 = ({ stream, line }) => {
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 = ({ stream, line }) => {
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 = ({ stream, line }) => {
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 = ({ stream, line }) => {
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
  });
@@ -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("solomon:alert", { ...eventBase, stage: "solomon" }, {
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(`Solomon alert [${alert.rule}]: ${alert.message}`);
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
- const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
224
- if (pauseResult) return pauseResult;
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
 
@@ -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
- printBanner(version);
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
- "solomon:alert": (event) => {
597
- console.log(` \u251c\u2500 ${ANSI.yellow}\u2696\ufe0f Solomon alert: ${event.message || "conflict detected"}${ANSI.reset}`);
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",
@@ -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
+ }