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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "karajan-code",
3
- "version": "2.0.1",
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 */ }
@@ -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,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 = ({ stream, line }) => {
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 = ({ stream, line }) => {
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 = ({ 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";
@@ -226,12 +226,7 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
226
226
  };
227
227
  }
228
228
 
229
- const reviewerOnOutput = ({ stream, line }) => {
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 = ({ 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,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("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
 
@@ -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
 
@@ -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
+ }