karajan-code 1.59.1 → 2.0.1

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.
Files changed (115) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/scripts/install.js +18 -15
  4. package/src/agents/aider-agent.js +0 -7
  5. package/src/agents/claude-agent.js +1 -4
  6. package/src/agents/codex-agent.js +2 -6
  7. package/src/agents/gemini-agent.js +0 -4
  8. package/src/agents/opencode-agent.js +0 -4
  9. package/src/{becaria → ci}/dispatch.js +13 -13
  10. package/src/{becaria → ci}/pr-diff.js +1 -1
  11. package/src/cli.js +2 -2
  12. package/src/commands/agents.js +0 -1
  13. package/src/commands/board.js +8 -1
  14. package/src/commands/doctor.js +22 -40
  15. package/src/commands/init.js +8 -8
  16. package/src/commands/report.js +0 -13
  17. package/src/commands/review.js +17 -17
  18. package/src/commands/run.js +1 -2
  19. package/src/commands/status.js +1 -7
  20. package/src/config.js +17 -52
  21. package/src/git/automation.js +9 -9
  22. package/src/guards/output-guard.js +0 -17
  23. package/src/mcp/handlers/direct-handlers.js +1 -1
  24. package/src/mcp/handlers/run-handler.js +1 -2
  25. package/src/mcp/sovereignty-guard.js +1 -1
  26. package/src/mcp/tools.js +1 -1
  27. package/src/orchestrator/brain-coordinator.js +165 -0
  28. package/src/orchestrator/ci-integration.js +139 -0
  29. package/src/orchestrator/config-init.js +435 -0
  30. package/src/orchestrator/direct-actions.js +186 -0
  31. package/src/orchestrator/feedback-enrichment.js +140 -0
  32. package/src/orchestrator/feedback-queue.js +152 -0
  33. package/src/orchestrator/flow-control.js +159 -0
  34. package/src/orchestrator/pipeline-context.js +3 -0
  35. package/src/orchestrator/post-loop-stages.js +20 -15
  36. package/src/orchestrator/preflight-checks.js +7 -3
  37. package/src/orchestrator/role-output-compressor.js +190 -0
  38. package/src/orchestrator/session-journal.js +344 -0
  39. package/src/orchestrator/solomon-escalation.js +38 -5
  40. package/src/orchestrator/solomon-rules.js +32 -33
  41. package/src/orchestrator/stages/coder-stage.js +49 -4
  42. package/src/orchestrator/stages/reviewer-stage.js +52 -9
  43. package/src/orchestrator/stages/sonar-stage.js +24 -1
  44. package/src/orchestrator/verification-gate.js +146 -0
  45. package/src/orchestrator.js +481 -672
  46. package/src/planning-game/client.js +2 -2
  47. package/src/planning-game/pipeline-adapter.js +1 -1
  48. package/src/prompts/coder.js +17 -3
  49. package/src/prompts/reviewer.js +2 -2
  50. package/src/prompts/rtk-snippet.js +3 -5
  51. package/src/review/diff-generator.js +66 -17
  52. package/src/review/snapshot-diff.js +122 -0
  53. package/src/review/tdd-policy.js +4 -2
  54. package/src/roles/agent-role.js +135 -0
  55. package/src/roles/architect-role.js +61 -108
  56. package/src/roles/audit-role.js +25 -71
  57. package/src/roles/coder-role.js +32 -57
  58. package/src/roles/discover-role.js +30 -71
  59. package/src/roles/hu-reviewer-role.js +51 -99
  60. package/src/roles/impeccable-role.js +33 -88
  61. package/src/roles/index.js +2 -0
  62. package/src/roles/karajan-brain-role.js +134 -0
  63. package/src/roles/planner-role.js +40 -75
  64. package/src/roles/refactorer-role.js +22 -64
  65. package/src/roles/researcher-role.js +51 -115
  66. package/src/roles/reviewer-role.js +68 -125
  67. package/src/roles/security-role.js +38 -102
  68. package/src/roles/solomon-role.js +69 -11
  69. package/src/roles/tester-role.js +121 -95
  70. package/src/roles/triage-role.js +50 -111
  71. package/src/session-store.js +1 -1
  72. package/src/skills/skill-detector.js +11 -0
  73. package/src/sonar/config-resolver.js +20 -1
  74. package/src/sonar/credentials.js +17 -0
  75. package/src/sonar/project-key.js +1 -1
  76. package/src/sonar/scanner.js +1 -1
  77. package/src/utils/display.js +33 -14
  78. package/src/utils/injection-guard.js +23 -8
  79. package/src/utils/logger.js +6 -1
  80. package/src/utils/process.js +3 -4
  81. package/src/utils/role-assigner.js +131 -0
  82. package/src/utils/update-check.js +5 -1
  83. package/src/webperf/devtools-detect.js +70 -23
  84. package/templates/roles/coder.md +7 -0
  85. package/templates/roles/karajan-brain.md +95 -0
  86. package/templates/roles/solomon.md +67 -106
  87. package/templates/workflows/automerge.yml +4 -4
  88. package/templates/workflows/houston-override.yml +4 -4
  89. package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
  90. package/src/proxy/adapters/anthropic.js +0 -131
  91. package/src/proxy/adapters/gemini.js +0 -106
  92. package/src/proxy/adapters/index.js +0 -51
  93. package/src/proxy/adapters/openai.js +0 -71
  94. package/src/proxy/compression/ai-compressor.js +0 -209
  95. package/src/proxy/compression/dedup-cache.js +0 -100
  96. package/src/proxy/compression/deterministic/bash-build.js +0 -60
  97. package/src/proxy/compression/deterministic/bash-git.js +0 -101
  98. package/src/proxy/compression/deterministic/bash-infra.js +0 -64
  99. package/src/proxy/compression/deterministic/bash-misc.js +0 -53
  100. package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
  101. package/src/proxy/compression/deterministic/bash-test.js +0 -61
  102. package/src/proxy/compression/deterministic/glob.js +0 -26
  103. package/src/proxy/compression/deterministic/grep.js +0 -53
  104. package/src/proxy/compression/deterministic/index.js +0 -58
  105. package/src/proxy/compression/deterministic/read.js +0 -22
  106. package/src/proxy/compression/deterministic/utils.js +0 -33
  107. package/src/proxy/compression/pipeline.js +0 -270
  108. package/src/proxy/middleware/prompt-injector.js +0 -165
  109. package/src/proxy/middleware/response-interceptor.js +0 -223
  110. package/src/proxy/progress-monitor.js +0 -98
  111. package/src/proxy/proxy-lifecycle.js +0 -201
  112. package/src/proxy/proxy-server.js +0 -239
  113. package/src/proxy/security.js +0 -45
  114. /package/src/{becaria → ci}/index.js +0 -0
  115. /package/src/{becaria → ci}/repo.js +0 -0
@@ -1,8 +1,5 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
1
  import { createAgent } from "./agents/index.js";
4
2
  import {
5
- createSession,
6
3
  loadSession,
7
4
  markSessionStatus,
8
5
  pauseSession,
@@ -10,28 +7,21 @@ import {
10
7
  saveSession,
11
8
  addCheckpoint
12
9
  } from "./session-store.js";
13
- import { computeBaseRef, generateDiff } from "./review/diff-generator.js";
14
- import { buildCoderPrompt } from "./prompts/coder.js";
15
- import { buildReviewerPrompt } from "./prompts/reviewer.js";
10
+ import { generateDiff } from "./review/diff-generator.js";
16
11
  import { resolveRole } from "./config.js";
12
+ import { resolveReviewProfile } from "./review/profiles.js";
13
+ import { msg, getLang } from "./utils/messages.js";
17
14
  import { RepeatDetector, getRepeatThreshold } from "./repeat-detector.js";
18
15
  import { emitProgress, makeEvent } from "./utils/events.js";
19
- import { BudgetTracker, extractUsageMetrics } from "./utils/budget.js";
20
16
  import {
21
17
  prepareGitAutomation,
22
- finalizeGitAutomation,
23
- earlyPrCreation,
24
- incrementalPush
18
+ finalizeGitAutomation
25
19
  } from "./git/automation.js";
26
- import { resolveRoleMdPath, loadFirstExisting } from "./roles/base-role.js";
27
- import { applyPolicies } from "./guards/policy-resolver.js";
28
20
  import { scanDiff } from "./guards/output-guard.js";
29
21
  import { scanPerfDiff } from "./guards/perf-guard.js";
30
22
  import { classifyIntent } from "./guards/intent-guard.js";
31
- import { resolveReviewProfile } from "./review/profiles.js";
32
23
  import { CoderRole } from "./roles/coder-role.js";
33
24
  import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
34
- import { msg, getLang } from "./utils/messages.js";
35
25
  import { PipelineContext } from "./orchestrator/pipeline-context.js";
36
26
  import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./orchestrator/pre-loop-stages.js";
37
27
  import { runDomainCuratorStage } from "./orchestrator/stages/domain-curator-stage.js";
@@ -44,267 +34,43 @@ import { detectTestFramework } from "./utils/project-detect.js";
44
34
  import { runPreflightChecks } from "./orchestrator/preflight-checks.js";
45
35
  import { detectRtk } from "./utils/rtk-detect.js";
46
36
  import { createRtkRunner, RtkSavingsTracker } from "./utils/rtk-wrapper.js";
47
- import { setRunner as setDiffRunner } from "./review/diff-generator.js";
37
+ import { setRunner as setDiffRunner, setProjectDir as setDiffProjectDir } from "./review/diff-generator.js";
48
38
  import { setRunner as setGitRunner } from "./utils/git.js";
49
39
  import { detectNeededSkills, autoInstallSkills, cleanupAutoInstalledSkills } from "./skills/skill-detector.js";
50
40
  import { isOpenSkillsAvailable } from "./skills/openskills-client.js";
51
- import { startProxy, stopProxy } from "./proxy/proxy-lifecycle.js";
52
-
53
-
54
- // --- Product Context loader ---
55
-
56
- /**
57
- * Load product context from well-known file locations.
58
- * Returns the file content or null if no file is found.
59
- * @param {string|null} projectDir
60
- * @returns {Promise<{content: string|null, source: string|null}>}
61
- */
62
- export async function loadProductContext(projectDir) {
63
- const base = projectDir || process.cwd();
64
- const candidates = [
65
- path.join(base, ".karajan", "context.md"),
66
- path.join(base, "product-vision.md")
67
- ];
68
- for (const file of candidates) {
69
- try {
70
- const content = await fs.readFile(file, "utf8");
71
- return { content, source: file };
72
- } catch { /* not found, try next */ }
73
- }
74
- return { content: null, source: null };
75
- }
76
-
77
- // --- Extracted helper functions (pure refactoring, zero behavior change) ---
78
-
79
- function resolvePipelineFlags(config) {
80
- return {
81
- plannerEnabled: Boolean(config.pipeline?.planner?.enabled),
82
- refactorerEnabled: Boolean(config.pipeline?.refactorer?.enabled),
83
- researcherEnabled: Boolean(config.pipeline?.researcher?.enabled),
84
- testerEnabled: Boolean(config.pipeline?.tester?.enabled),
85
- securityEnabled: Boolean(config.pipeline?.security?.enabled),
86
- impeccableEnabled: Boolean(config.pipeline?.impeccable?.enabled),
87
- reviewerEnabled: config.pipeline?.reviewer?.enabled !== false,
88
- discoverEnabled: Boolean(config.pipeline?.discover?.enabled),
89
- architectEnabled: Boolean(config.pipeline?.architect?.enabled),
90
- huReviewerEnabled: Boolean(config.pipeline?.hu_reviewer?.enabled),
91
- };
92
- }
93
-
94
- async function handleDryRun({ task, config, flags, emitter, pipelineFlags }) {
95
- const { plannerEnabled, refactorerEnabled, researcherEnabled, testerEnabled, securityEnabled, impeccableEnabled, reviewerEnabled, discoverEnabled, architectEnabled, huReviewerEnabled } = pipelineFlags;
96
- const plannerRole = resolveRole(config, "planner");
97
- const coderRole = resolveRole(config, "coder");
98
- const reviewerRole = resolveRole(config, "reviewer");
99
- const refactorerRole = resolveRole(config, "refactorer");
100
- const triageEnabled = true;
101
-
102
- const dryRunPolicies = applyPolicies({
103
- taskType: flags.taskType || config.taskType || null,
104
- policies: config.policies,
105
- });
106
- const projectDir = config.projectDir || process.cwd();
107
- const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
108
- const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
109
- const coderPrompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available), proxyEnabled: Boolean(config.proxy?.enabled), productContext: config.productContext || null });
110
- const reviewerPrompt = await buildReviewerPrompt({ task, diff: "(dry-run: no diff)", reviewRules, mode: config.review_mode, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available), proxyEnabled: Boolean(config.proxy?.enabled), productContext: config.productContext || null });
111
-
112
- const summary = {
113
- dry_run: true,
114
- task,
115
- policies: dryRunPolicies,
116
- roles: { planner: plannerRole, coder: coderRole, reviewer: reviewerRole, refactorer: refactorerRole },
117
- pipeline: {
118
- discover_enabled: discoverEnabled,
119
- architect_enabled: architectEnabled,
120
- triage_enabled: triageEnabled,
121
- planner_enabled: plannerEnabled,
122
- refactorer_enabled: refactorerEnabled,
123
- sonar_enabled: Boolean(config.sonarqube?.enabled),
124
- reviewer_enabled: reviewerEnabled,
125
- researcher_enabled: researcherEnabled,
126
- tester_enabled: testerEnabled,
127
- security_enabled: securityEnabled,
128
- impeccable_enabled: impeccableEnabled,
129
- solomon_enabled: Boolean(config.pipeline?.solomon?.enabled),
130
- hu_reviewer_enabled: huReviewerEnabled
131
- },
132
- limits: {
133
- max_iterations: config.max_iterations,
134
- max_iteration_minutes: config.session?.max_iteration_minutes,
135
- max_total_minutes: config.session?.max_total_minutes,
136
- max_sonar_retries: config.session?.max_sonar_retries,
137
- max_reviewer_retries: config.session?.max_reviewer_retries,
138
- max_tester_retries: config.session?.max_tester_retries,
139
- max_security_retries: config.session?.max_security_retries
140
- },
141
- prompts: { coder: coderPrompt, reviewer: reviewerPrompt },
142
- git: config.git
143
- };
144
41
 
145
- emitProgress(
146
- emitter,
147
- makeEvent("dry-run:summary", { sessionId: null, iteration: 0, stage: "dry-run", startedAt: Date.now() }, {
148
- message: "Dry-run complete — no changes made",
149
- detail: summary
150
- })
151
- );
152
-
153
- return summary;
154
- }
155
-
156
- function createBudgetManager({ config, emitter, eventBase }) {
157
- const budgetTracker = new BudgetTracker({ pricing: config?.budget?.pricing });
158
- const budgetLimit = Number(config?.max_budget_usd);
159
- const hasBudgetLimit = Number.isFinite(budgetLimit) && budgetLimit >= 0;
160
- const warnThresholdPct = Number(config?.budget?.warn_threshold_pct ?? 80);
161
- let stageCounter = 0;
162
-
163
- function budgetSummary() {
164
- const s = budgetTracker.summary();
165
- s.trace = budgetTracker.trace();
166
- return s;
167
- }
168
-
169
- function trackBudget({ role, provider, model, result, duration_ms, promptSize }) {
170
- // Attach promptSize to result if provided, so extractUsageMetrics can estimate tokens
171
- const enrichedResult = promptSize && result ? { ...result, promptSize } : result;
172
- const metrics = extractUsageMetrics(enrichedResult, model);
173
- budgetTracker.record({ role, provider, ...metrics, duration_ms, stage_index: stageCounter++ });
174
-
175
- if (!hasBudgetLimit) return;
176
- const totalCost = budgetTracker.total().cost_usd;
177
- const pctUsed = budgetLimit === 0 ? 100 : (totalCost / budgetLimit) * 100;
178
- const warnOrOk = pctUsed >= warnThresholdPct ? "paused" : "ok";
179
- const status = totalCost > budgetLimit ? "fail" : warnOrOk;
180
- emitProgress(
181
- emitter,
182
- makeEvent("budget:update", { ...eventBase, stage: role }, {
183
- status,
184
- message: `Budget: $${totalCost.toFixed(2)} / $${budgetLimit.toFixed(2)}`,
185
- detail: {
186
- ...budgetSummary(),
187
- max_budget_usd: budgetLimit,
188
- warn_threshold_pct: warnThresholdPct,
189
- pct_used: Number(pctUsed.toFixed(2)),
190
- remaining_usd: budgetTracker.remaining(budgetLimit),
191
- executorType: "system"
192
- }
193
- })
194
- );
195
- }
42
+ // Extracted modules
43
+ import {
44
+ loadProductContext as _loadProductContext,
45
+ resolvePipelineFlags, handleDryRun, createBudgetManager,
46
+ initializeSession, applyTriageOverrides, applyAutoSimplify,
47
+ applyFlagOverrides, resolvePipelinePolicies, autoInit,
48
+ updateGitignoreForStack
49
+ } from "./orchestrator/config-init.js";
50
+ import {
51
+ tryCiComment, handleCiEarlyPrOrPush, handleCiReviewDispatch,
52
+ formatBlockingIssues
53
+ } from "./orchestrator/ci-integration.js";
54
+ import {
55
+ shouldAutoContinueCheckpoint as _shouldAutoContinueCheckpoint,
56
+ parseCheckpointAnswer as _parseCheckpointAnswer,
57
+ handleCheckpoint, checkSessionTimeout, checkBudgetExceeded,
58
+ takeCheckpointSnapshot
59
+ } from "./orchestrator/flow-control.js";
60
+ import {
61
+ createJournalDir, writePreLoopJournal, writeIterationsJournal,
62
+ writeDecisionsJournal, writeTreeJournal, writeSummaryJournal,
63
+ formatIteration, formatDecision, buildPlanSummary
64
+ } from "./orchestrator/session-journal.js";
196
65
 
197
- return { budgetTracker, budgetLimit, budgetSummary, trackBudget };
198
- }
66
+ // Re-export for external consumers
67
+ export const loadProductContext = _loadProductContext;
68
+ export const shouldAutoContinueCheckpoint = _shouldAutoContinueCheckpoint;
69
+ export const parseCheckpointAnswer = _parseCheckpointAnswer;
199
70
 
200
- async function initializeSession({ task, config, flags, pgTaskId, pgProject }) {
201
- const baseRef = await computeBaseRef({ baseBranch: config.base_branch, baseRef: flags.baseRef || null });
202
- const sessionInit = {
203
- task,
204
- config_snapshot: config,
205
- base_ref: baseRef,
206
- session_start_sha: baseRef,
207
- last_reviewer_feedback: null,
208
- repeated_issue_count: 0,
209
- sonar_retry_count: 0,
210
- reviewer_retry_count: 0,
211
- standby_retry_count: 0,
212
- last_sonar_issue_signature: null,
213
- sonar_repeat_count: 0,
214
- last_reviewer_issue_signature: null,
215
- reviewer_repeat_count: 0,
216
- deferred_issues: []
217
- };
218
- if (pgTaskId) sessionInit.pg_task_id = pgTaskId;
219
- if (pgProject) sessionInit.pg_project_id = pgProject;
220
- return createSession(sessionInit);
221
- }
222
71
 
223
72
  // PG card "In Progress" logic moved to src/planning-game/pipeline-adapter.js → initPgAdapter()
224
73
 
225
- function applyTriageOverrides(pipelineFlags, roleOverrides) {
226
- const keys = ["plannerEnabled", "researcherEnabled", "architectEnabled", "refactorerEnabled", "reviewerEnabled", "testerEnabled", "securityEnabled", "impeccableEnabled"];
227
- for (const key of keys) {
228
- if (roleOverrides[key] !== undefined) {
229
- pipelineFlags[key] = roleOverrides[key];
230
- }
231
- }
232
- }
233
-
234
- const SIMPLE_LEVELS = new Set(["trivial", "simple"]);
235
-
236
- function applyAutoSimplify({ pipelineFlags, triageLevel, config, flags, logger, emitter, eventBase }) {
237
- if (!config.pipeline?.auto_simplify) return false;
238
- if (!triageLevel || !SIMPLE_LEVELS.has(triageLevel)) return false;
239
- if (flags.mode) return false;
240
- if (flags.enableReviewer !== undefined || flags.enableTester !== undefined) return false;
241
-
242
- pipelineFlags.reviewerEnabled = false;
243
- pipelineFlags.testerEnabled = false;
244
-
245
- const disabledRoles = ["reviewer", "tester"];
246
- logger.info(`Simple task (${triageLevel}) — lightweight pipeline (disabled: ${disabledRoles.join(", ")})`);
247
- emitProgress(
248
- emitter,
249
- makeEvent("pipeline:simplify", { ...eventBase, stage: "triage" }, {
250
- message: `Simple task (${triageLevel}) — lightweight pipeline`,
251
- detail: { level: triageLevel, disabledRoles }
252
- })
253
- );
254
- return true;
255
- }
256
-
257
- // PG decomposition logic moved to src/planning-game/pipeline-adapter.js → handlePgDecomposition()
258
-
259
- function applyFlagOverrides(pipelineFlags, flags) {
260
- if (flags.enablePlanner !== undefined) pipelineFlags.plannerEnabled = Boolean(flags.enablePlanner);
261
- if (flags.enableResearcher !== undefined) pipelineFlags.researcherEnabled = Boolean(flags.enableResearcher);
262
- if (flags.enableArchitect !== undefined) pipelineFlags.architectEnabled = Boolean(flags.enableArchitect);
263
- if (flags.enableRefactorer !== undefined) pipelineFlags.refactorerEnabled = Boolean(flags.enableRefactorer);
264
- if (flags.enableReviewer !== undefined) pipelineFlags.reviewerEnabled = Boolean(flags.enableReviewer);
265
- if (flags.enableTester !== undefined) pipelineFlags.testerEnabled = Boolean(flags.enableTester);
266
- if (flags.enableSecurity !== undefined) pipelineFlags.securityEnabled = Boolean(flags.enableSecurity);
267
- if (flags.enableImpeccable !== undefined) pipelineFlags.impeccableEnabled = Boolean(flags.enableImpeccable);
268
-
269
- // --design flag: force-enable impeccable in refactoring mode
270
- if (flags.design) {
271
- pipelineFlags.impeccableEnabled = true;
272
- pipelineFlags.impeccableMode = "refactoring";
273
- }
274
- }
275
-
276
- function resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags }) {
277
- const resolvedPolicies = applyPolicies({
278
- taskType: flags.taskType || config.taskType || stageResults.triage?.taskType || stageResults.intent?.taskType || null,
279
- policies: config.policies,
280
- });
281
- session.resolved_policies = resolvedPolicies;
282
-
283
- let updatedConfig = config;
284
- if (!resolvedPolicies.tdd) {
285
- updatedConfig = { ...updatedConfig, development: { ...updatedConfig.development, methodology: "standard", require_test_changes: false } };
286
- }
287
- if (!resolvedPolicies.sonar) {
288
- updatedConfig = { ...updatedConfig, sonarqube: { ...updatedConfig.sonarqube, enabled: false } };
289
- }
290
- if (!resolvedPolicies.reviewer) {
291
- pipelineFlags.reviewerEnabled = false;
292
- }
293
- if (resolvedPolicies.coderRequired === false) {
294
- pipelineFlags.coderRequired = false;
295
- }
296
-
297
- emitProgress(
298
- emitter,
299
- makeEvent("policies:resolved", eventBase, {
300
- message: `Policies resolved for taskType="${resolvedPolicies.taskType}"`,
301
- detail: resolvedPolicies
302
- })
303
- );
304
-
305
- return updatedConfig;
306
- }
307
-
308
74
  async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion }) {
309
75
  let researchContext = null;
310
76
  let plannedTask = task;
@@ -336,7 +102,7 @@ async function runPlanningPhases({ config, logger, emitter, eventBase, session,
336
102
  plannedTask = plannerResult.plannedTask;
337
103
  stageResults.planner = plannerResult.stageResult;
338
104
 
339
- await tryBecariaComment({
105
+ await tryCiComment({
340
106
  config, session, logger,
341
107
  agent: "Planner",
342
108
  body: `Plan: ${plannerResult.stageResult?.summary || plannedTask}`
@@ -346,277 +112,92 @@ async function runPlanningPhases({ config, logger, emitter, eventBase, session,
346
112
  return { plannedTask };
347
113
  }
348
114
 
349
- async function tryBecariaComment({ config, session, logger, agent, body }) {
350
- if (!config.becaria?.enabled || !session.becaria_pr_number) return;
351
- try {
352
- const { dispatchComment } = await import("./becaria/dispatch.js");
353
- const { detectRepo } = await import("./becaria/repo.js");
354
- const repo = await detectRepo();
355
- if (repo) {
356
- await dispatchComment({
357
- repo, prNumber: session.becaria_pr_number, agent,
358
- body, becariaConfig: config.becaria
359
- });
360
- }
361
- } catch { /* non-blocking */ }
362
- }
363
-
364
- function detectCheckpointProgress(session, lastCheckpointSnapshot) {
365
- if (!lastCheckpointSnapshot) return true; // First checkpoint — assume progress
366
- const currentIteration = session.reviewer_retry_count ?? 0;
367
- const currentStages = Object.keys(session.resolved_policies || {}).length;
368
- const currentCheckpoints = (session.checkpoints || []).length;
369
-
370
- const iterationAdvanced = currentIteration !== lastCheckpointSnapshot.iteration;
371
- const stagesChanged = currentStages !== lastCheckpointSnapshot.stagesCount;
372
- const checkpointsChanged = currentCheckpoints !== lastCheckpointSnapshot.checkpointsCount;
373
-
374
- return iterationAdvanced || stagesChanged || checkpointsChanged;
375
- }
376
-
377
- function takeCheckpointSnapshot(session) {
378
- return {
379
- iteration: session.reviewer_retry_count ?? 0,
380
- stagesCount: Object.keys(session.resolved_policies || {}).length,
381
- checkpointsCount: (session.checkpoints || []).length
382
- };
383
- }
384
-
385
- /**
386
- * Determine if checkpoint should auto-continue without asking the user.
387
- * Exported for testing.
388
- */
389
- export function shouldAutoContinueCheckpoint(session, hasProgress) {
390
- if (hasProgress) {
391
- session._checkpoint_stall_count = 0;
392
- return { autoContinue: true, reason: "progress_detected" };
393
- }
394
- const wasRateLimited = (session.standby_retry_count || 0) > 0;
395
- const consecutiveStalls = (session._checkpoint_stall_count || 0) + 1;
396
- session._checkpoint_stall_count = consecutiveStalls;
397
- if (wasRateLimited && consecutiveStalls < 3) {
398
- return { autoContinue: true, reason: "recoverable_stall" };
399
- }
400
- return { autoContinue: false, reason: consecutiveStalls >= 3 ? "max_stalls_reached" : "no_progress" };
401
- }
402
-
403
- async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes, i, config, budgetTracker, stageResults, emitter, eventBase, session, budgetSummary, lastCheckpointSnapshot }) {
404
- if (checkpointDisabled || !askQuestion || (Date.now() - lastCheckpointAt) < checkpointIntervalMs) {
405
- return { action: "continue_loop", checkpointDisabled, lastCheckpointAt, lastCheckpointSnapshot };
406
- }
407
-
408
- const elapsedStr = elapsedMinutes.toFixed(1);
409
- const stagesCompleted = Object.keys(stageResults).join(", ") || "none";
410
115
 
411
- // Decide whether to auto-continue or ask the user
412
- const hasProgress = detectCheckpointProgress(session, lastCheckpointSnapshot);
413
- const newSnapshot = takeCheckpointSnapshot(session);
414
- const decision = shouldAutoContinueCheckpoint(session, hasProgress);
415
116
 
416
- if (decision.autoContinue) {
417
- emitProgress(
418
- emitter,
419
- makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
420
- message: `Checkpoint: ${decision.reason === "progress_detected" ? "progress detected" : "stall caused by rate limit/cooldown"}, auto-continuing (${elapsedStr} min elapsed)`,
421
- detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: true, reason: decision.reason }
422
- })
423
- );
424
- return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now(), lastCheckpointSnapshot: newSnapshot };
117
+ async function handleStandbyResult({ stageResult, session, emitter, eventBase, i, stage, logger, config, askQuestion }) {
118
+ if (stageResult?.action !== "standby") {
119
+ return { handled: false };
425
120
  }
426
121
 
427
- // No progress and not recoverable — ask human
428
- const iterInfo = `${i - 1}/${config.max_iterations} iterations completed`;
429
- const budgetInfo = budgetTracker.total().cost_usd > 0 ? ` | Budget: $${budgetTracker.total().cost_usd.toFixed(2)}` : "";
430
- const checkpointMsg = `Checkpoint — ${elapsedStr} min elapsed | ${iterInfo}${budgetInfo} | Stages completed: ${stagesCompleted}. No progress since last checkpoint. What would you like to do?`;
431
-
432
- emitProgress(
433
- emitter,
434
- makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
435
- message: `Interactive checkpoint at ${elapsedStr} min (stalled)`,
436
- detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: false }
437
- })
438
- );
439
-
440
- const lang = getLang(config);
441
- const answer = await askQuestion(
442
- `${checkpointMsg}\n\n${msg("checkpoint_options", lang)}`
443
- );
444
-
445
- await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
446
-
447
- const trimmedAnswer = (answer || "").trim();
448
- const isExplicitStop = trimmedAnswer === "4" || trimmedAnswer.toLowerCase().startsWith("stop");
449
-
450
- if (isExplicitStop) {
451
- await markSessionStatus(session, "stopped");
452
- emitProgress(
453
- emitter,
454
- makeEvent("session:end", { ...eventBase, iteration: i, stage: "user-stop" }, {
455
- status: "stopped",
456
- message: "Session stopped by user at checkpoint",
457
- detail: { approved: false, reason: "user_stopped", elapsed_minutes: Number(elapsedStr), budget: budgetSummary() }
458
- })
459
- );
460
- return { action: "stop", result: { approved: false, sessionId: session.id, reason: "user_stopped", elapsed_minutes: Number(elapsedStr) } };
461
- }
122
+ const isOutage = stageResult.standbyInfo.isProviderOutage;
123
+ const agent = stageResult.standbyInfo.agent;
124
+ const cooldownUntil = stageResult.standbyInfo.cooldownUntil;
125
+ const cooldownMs = stageResult.standbyInfo.cooldownMs;
462
126
 
463
- const parsed = parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config });
464
- parsed.lastCheckpointSnapshot = newSnapshot;
465
- return parsed;
466
- }
127
+ // Rate limit = out of normal flow → Solomon decides immediately
128
+ const solomonResult = await invokeSolomon({
129
+ config, logger, emitter, eventBase, stage: `${stage}_rate_limit`, askQuestion, session,
130
+ iteration: i,
131
+ conflict: {
132
+ stage: `${stage}_rate_limit`,
133
+ task: session.task,
134
+ iterationCount: i,
135
+ maxIterations: config?.max_iterations || 5,
136
+ cooldownUntil,
137
+ cooldownMs,
138
+ history: [{
139
+ agent: stage,
140
+ feedback: `Agent "${agent}" rate-limited. ${isOutage ? "Provider outage (5xx)." : "API rate limit (429)."} ${cooldownUntil ? `Cooldown until ${cooldownUntil}.` : ""}`
141
+ }]
142
+ }
143
+ });
467
144
 
468
- export function parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config }) {
469
- if (!trimmedAnswer) {
470
- return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
471
- }
472
- if (trimmedAnswer === "2" || trimmedAnswer.toLowerCase().startsWith("continue until")) {
473
- return { action: "continue_loop", checkpointDisabled: true, lastCheckpointAt: Date.now() };
474
- }
475
- if (trimmedAnswer === "1" || trimmedAnswer.toLowerCase().includes("5 m")) {
476
- return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
477
- }
478
- const customMinutes = Number.parseInt(trimmedAnswer.replaceAll(/\D/g, ""), 10);
479
- if (customMinutes > 0) {
480
- config.session.checkpoint_interval_minutes = customMinutes;
481
- return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
145
+ if (solomonResult.action === "approve") {
146
+ // Solomon says skip is safe — only allowed after exhausting alternatives
147
+ logger.info(`Solomon: skip ${stage} after evaluating risk (agent "${agent}" rate-limited)`);
148
+ emitProgress(emitter, makeEvent(`${stage}:rate_limit`, { ...eventBase, stage }, {
149
+ status: "ok",
150
+ message: `Solomon: skip ${stage} (low risk, agent "${agent}" unavailable)`,
151
+ detail: { agent, solomonAction: "approve" }
152
+ }));
153
+ return { handled: true, action: "skip" };
482
154
  }
483
- return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
484
- }
485
-
486
- async function checkSessionTimeout({ askQuestion, elapsedMinutes, config, session, emitter, eventBase, i, budgetSummary }) {
487
- if (askQuestion || elapsedMinutes <= config.session.max_total_minutes) return;
488
-
489
- await markSessionStatus(session, "failed");
490
- emitProgress(
491
- emitter,
492
- makeEvent("session:end", { ...eventBase, iteration: i, stage: "timeout" }, {
493
- status: "fail",
494
- message: "Session timed out",
495
- detail: { approved: false, reason: "timeout", budget: budgetSummary() }
496
- })
497
- );
498
- throw new Error("Session timed out");
499
- }
500
155
 
501
- async function checkBudgetExceeded({ budgetTracker, config, session, emitter, eventBase, i, budgetLimit, budgetSummary }) {
502
- if (!budgetTracker.isOverBudget(config?.max_budget_usd)) return;
156
+ if (solomonResult.action === "continue") {
157
+ // Solomon says: wait for cooldown, or retry with alternative agent
158
+ const altAgent = solomonResult.alternativeAgent;
159
+ const waitTarget = solomonResult.waitUntil;
160
+
161
+ if (waitTarget) {
162
+ const waitMs = Math.max(0, new Date(waitTarget).getTime() - Date.now());
163
+ if (waitMs > 0 && waitMs < 10 * 60 * 1000) {
164
+ logger.info(`Solomon: wait ${Math.round(waitMs / 1000)}s for cooldown, then retry ${stage}`);
165
+ await new Promise(r => setTimeout(r, waitMs));
166
+ }
167
+ }
503
168
 
504
- await markSessionStatus(session, "failed");
505
- const totalCost = budgetTracker.total().cost_usd;
506
- const message = `Budget exceeded: $${totalCost.toFixed(2)} > $${budgetLimit.toFixed(2)}`;
507
- emitProgress(
508
- emitter,
509
- makeEvent("session:end", { ...eventBase, iteration: i, stage: "budget" }, {
510
- status: "fail",
511
- message,
512
- detail: { approved: false, reason: "budget_exceeded", budget: budgetSummary(), max_budget_usd: budgetLimit }
513
- })
514
- );
515
- throw new Error(message);
516
- }
169
+ if (altAgent) {
170
+ logger.info(`Solomon: retry ${stage} with alternative agent "${altAgent}"`);
171
+ session._alternative_agent = { stage, provider: altAgent };
172
+ }
517
173
 
518
- async function handleStandbyResult({ stageResult, session, emitter, eventBase, i, stage, logger }) {
519
- if (stageResult?.action !== "standby") {
520
- return { handled: false };
174
+ if (solomonResult.humanGuidance) {
175
+ session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
176
+ }
177
+ await saveSession(session);
178
+ return { handled: true, action: "retry_reviewer_only" };
521
179
  }
522
180
 
523
- const standbyRetries = session.standby_retry_count || 0;
524
- const isOutage = stageResult.standbyInfo.isProviderOutage;
525
- const pauseReason = isOutage
526
- ? `Provider outage (${stageResult.standbyInfo.message || "5xx/connection error"}) — retried ${standbyRetries} times. This is NOT a KJ or code problem.`
527
- : `Rate limit standby exhausted after ${standbyRetries} retries. Agent: ${stageResult.standbyInfo.agent}`;
528
-
529
- if (standbyRetries >= MAX_STANDBY_RETRIES) {
530
- session.last_reviewer_feedback = isOutage
531
- ? "IMPORTANT: The previous interruption was caused by a provider outage (API 500 error), NOT by a problem in your code or in Karajan. Continue from where you left off."
532
- : session.last_reviewer_feedback;
533
- await pauseSession(session, {
534
- question: pauseReason,
535
- context: { iteration: i, stage, reason: isOutage ? "provider_outage" : "standby_exhausted" }
536
- });
537
- emitProgress(emitter, makeEvent(`${stage}:rate_limit`, { ...eventBase, stage }, {
538
- status: "paused",
539
- message: `Standby exhausted after ${standbyRetries} retries`,
540
- detail: { agent: stageResult.standbyInfo.agent, sessionId: session.id }
541
- }));
181
+ if (solomonResult.action === "pause") {
542
182
  return {
543
183
  handled: true,
544
184
  action: "return",
545
- result: { paused: true, sessionId: session.id, question: `Rate limit standby exhausted after ${standbyRetries} retries`, context: "standby_exhausted" }
185
+ result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "rate_limit" }
546
186
  };
547
187
  }
548
- session.standby_retry_count = standbyRetries + 1;
549
- await saveSession(session);
550
- await waitForCooldown({ ...stageResult.standbyInfo, retryCount: standbyRetries, emitter, eventBase, logger, session });
551
- return { handled: true, action: "retry" };
552
- }
553
188
 
554
- function formatCommitList(commits) {
555
- return commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
556
- }
557
-
558
- async function becariaIncrementalPush({ config, session, gitCtx, task, logger, repo, dispatchComment }) {
559
- const pushResult = await incrementalPush({ gitCtx, task, logger, session });
560
- if (!pushResult) return;
561
-
562
- // Accumulate commits for PG card lifecycle tracking
563
- const { accumulateCommit } = await import("./planning-game/pipeline-adapter.js");
564
- for (const c of pushResult.commits) accumulateCommit(session, c);
565
-
566
- session.becaria_commits = [...(session.becaria_commits ?? []), ...pushResult.commits];
567
- await saveSession(session);
568
-
569
- if (!repo) return;
570
- const feedback = session.last_reviewer_feedback || "N/A";
571
- await dispatchComment({
572
- repo, prNumber: session.becaria_pr_number, agent: "Coder",
573
- body: `Issues corregidos:\n${feedback}\n\nCommits:\n${formatCommitList(pushResult.commits)}`,
574
- becariaConfig: config.becaria
575
- });
576
- }
577
-
578
- async function becariaCreateEarlyPr({ config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i, repo, dispatchComment }) {
579
- const earlyPr = await earlyPrCreation({ gitCtx, task, logger, session, stageResults });
580
- if (!earlyPr) return;
581
-
582
- // Accumulate commits for PG card lifecycle tracking
583
- const { accumulateCommit } = await import("./planning-game/pipeline-adapter.js");
584
- for (const c of earlyPr.commits) accumulateCommit(session, c);
585
-
586
- session.becaria_pr_number = earlyPr.prNumber;
587
- session.becaria_pr_url = earlyPr.prUrl;
588
- session.becaria_commits = earlyPr.commits;
589
- await saveSession(session);
590
- emitProgress(emitter, makeEvent("becaria:pr-created", { ...eventBase, stage: "becaria" }, {
591
- message: `Early PR created: #${earlyPr.prNumber}`,
592
- detail: { prNumber: earlyPr.prNumber, prUrl: earlyPr.prUrl }
593
- }));
594
-
595
- if (!repo) return;
596
- await dispatchComment({
597
- repo, prNumber: earlyPr.prNumber, agent: "Coder",
598
- body: `Iteración ${i} completada.\n\nCommits:\n${formatCommitList(earlyPr.commits)}`,
599
- becariaConfig: config.becaria
189
+ // Solomon couldn't resolve — pause
190
+ await pauseSession(session, {
191
+ question: `Agent "${agent}" rate-limited. Solomon could not resolve.`,
192
+ context: { iteration: i, stage, reason: "rate_limit" }
600
193
  });
194
+ return {
195
+ handled: true,
196
+ action: "return",
197
+ result: { paused: true, sessionId: session.id, question: `Agent "${agent}" rate-limited`, context: "rate_limit" }
198
+ };
601
199
  }
602
200
 
603
- async function handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i }) {
604
- if (!becariaEnabled) return;
605
-
606
- try {
607
- const { dispatchComment } = await import("./becaria/dispatch.js");
608
- const { detectRepo } = await import("./becaria/repo.js");
609
- const repo = await detectRepo();
610
-
611
- if (session.becaria_pr_number) {
612
- await becariaIncrementalPush({ config, session, gitCtx, task, logger, repo, dispatchComment });
613
- } else {
614
- await becariaCreateEarlyPr({ config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i, repo, dispatchComment });
615
- }
616
- } catch (err) {
617
- logger.warn(`BecarIA early PR/push failed (non-blocking): ${err.message}`);
618
- }
619
- }
620
201
 
621
202
  function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
622
203
  for (const alert of alerts) {
@@ -629,7 +210,7 @@ function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
629
210
  }
630
211
  }
631
212
 
632
- async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, becariaEnabled, blockingIssues }) {
213
+ async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, ciEnabled, blockingIssues, brainCtx }) {
633
214
  if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
634
215
 
635
216
  try {
@@ -639,19 +220,56 @@ async function handleSolomonCheck({ config, session, emitter, eventBase, logger,
639
220
 
640
221
  if (rulesResult.alerts.length > 0) {
641
222
  emitSolomonAlerts(rulesResult.alerts, emitter, eventBase, logger);
642
- const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
643
- if (pauseResult) return pauseResult;
223
+ // Brain gateway: when Brain is the orchestrator, rule alerts are telemetry.
224
+ // On critical alerts Brain consults Solomon (AI judge). Only if Solomon can't
225
+ // resolve does Brain escalate to human. Solomon-rules never prompts directly.
226
+ if (!brainCtx?.enabled) {
227
+ const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
228
+ if (pauseResult) return pauseResult;
229
+ } else if (rulesResult.hasCritical) {
230
+ const criticalAlerts = rulesResult.alerts.filter(a => a.severity === "critical");
231
+ brainCtx.ruleAlerts = brainCtx.ruleAlerts || [];
232
+ brainCtx.ruleAlerts.push(...criticalAlerts);
233
+ logger.info(`Brain: ${criticalAlerts.length} critical rule alert(s) — consulting Solomon`);
234
+
235
+ const { invokeSolomon } = await import("./orchestrator/solomon-escalation.js");
236
+ const alertSummary = criticalAlerts.map(a => a.message).join("; ");
237
+ const solomonOpinion = await invokeSolomon({
238
+ config, logger, emitter, eventBase, stage: "brain-dilemma", askQuestion, session, iteration: i,
239
+ conflict: {
240
+ stage: "brain-dilemma",
241
+ task,
242
+ iterationCount: i,
243
+ maxIterations: config.max_iterations,
244
+ dilemma: `Brain detected critical rule alerts: ${alertSummary}. Should we continue iterating, pause for human, or stop?`,
245
+ ruleAlerts: criticalAlerts,
246
+ blockingIssues: blockingIssues || [],
247
+ history: (session.checkpoints || []).filter(cp => cp.stage === "reviewer").slice(-5).map(cp => ({ iteration: cp.iteration, feedback: cp.note || "" }))
248
+ }
249
+ });
250
+
251
+ if (solomonOpinion.action === "pause") {
252
+ logger.info("Brain: Solomon advised pause — escalating to human");
253
+ return { action: "return", result: { paused: true, sessionId: session.id, question: solomonOpinion.question || `Brain+Solomon paused: ${alertSummary}`, context: "brain_solomon_dilemma" } };
254
+ }
255
+ if (solomonOpinion.action === "approve") {
256
+ logger.info("Brain: Solomon advised proceeding — treating as approved");
257
+ return { action: "continue", approved: true };
258
+ }
259
+ // action === "continue" | "subtask" | fallback → Brain continues the loop
260
+ logger.info(`Brain: Solomon said '${solomonOpinion.action}' — continuing loop`);
261
+ }
644
262
  }
645
263
 
646
- if (becariaEnabled && session.becaria_pr_number) {
264
+ if (ciEnabled && session.ci_pr_number) {
647
265
  const alerts = rulesResult.alerts || [];
648
266
  const alertMsg = alerts.length > 0
649
267
  ? alerts.map(a => `- [${a.severity}] ${a.message}`).join("\n")
650
268
  : "No anomalies detected";
651
- await tryBecariaComment({
269
+ await tryCiComment({
652
270
  config, session, logger,
653
271
  agent: "Solomon",
654
- body: `Supervisor check iteración ${i}: ${alertMsg}`
272
+ body: `Supervisor check iteration ${i}: ${alertMsg}`
655
273
  });
656
274
  }
657
275
  } catch (err) {
@@ -694,59 +312,8 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
694
312
  return null;
695
313
  }
696
314
 
697
- function formatBlockingIssues(issues) {
698
- return issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
699
- }
700
-
701
- function formatSuggestions(suggestions) {
702
- return suggestions?.map((s) => {
703
- const detail = typeof s === "string" ? s : `${s.id || ""} ${s.description || s}`;
704
- return `- ${detail}`;
705
- }).join("\n") || "";
706
- }
707
-
708
- function buildReviewCommentBody(review, i) {
709
- const status = review.approved ? "APPROVED" : "REQUEST_CHANGES";
710
- const blocking = formatBlockingIssues(review.blocking_issues);
711
- const suggestions = formatSuggestions(review.non_blocking_suggestions);
712
- let body = `Review iteración ${i}: ${status}`;
713
- if (blocking) body += `\n\n**Blocking:**\n${blocking}`;
714
- if (suggestions) body += `\n\n**Suggestions:**\n${suggestions}`;
715
- return body;
716
- }
717
-
718
- async function handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger }) {
719
- if (!becariaEnabled || !session.becaria_pr_number) return;
720
-
721
- try {
722
- const { dispatchReview, dispatchComment } = await import("./becaria/dispatch.js");
723
- const { detectRepo } = await import("./becaria/repo.js");
724
- const repo = await detectRepo();
725
- if (!repo) return;
726
-
727
- const bc = config.becaria;
728
- const reviewEvent = review.approved ? "APPROVE" : "REQUEST_CHANGES";
729
- const reviewBody = review.approved
730
- ? (review.summary || "Approved")
731
- : (formatBlockingIssues(review.blocking_issues) || review.summary || "Changes requested");
732
-
733
- await dispatchReview({
734
- repo, prNumber: session.becaria_pr_number,
735
- event: reviewEvent, body: reviewBody, agent: "Reviewer", becariaConfig: bc
736
- });
737
-
738
- await dispatchComment({
739
- repo, prNumber: session.becaria_pr_number, agent: "Reviewer",
740
- body: buildReviewCommentBody(review, i), becariaConfig: bc
741
- });
742
-
743
- logger.info(`BecarIA: dispatched review for PR #${session.becaria_pr_number}`);
744
- } catch (err) {
745
- logger.warn(`BecarIA dispatch failed (non-blocking): ${err.message}`);
746
- }
747
- }
748
315
 
749
- async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, becariaEnabled, testerEnabled, securityEnabled, askQuestion, logger }) {
316
+ async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, ciEnabled, testerEnabled, securityEnabled, askQuestion, logger }) {
750
317
  const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
751
318
 
752
319
  if (testerEnabled) {
@@ -755,10 +322,16 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
755
322
  iteration: i, task, diff: postLoopDiff, askQuestion
756
323
  });
757
324
  if (testerResult.action === "pause") return { action: "return", result: testerResult.result };
758
- if (testerResult.action === "continue") return { action: "continue" };
325
+ if (testerResult.action === "continue") {
326
+ const summary = testerResult.stageResult?.summary || "Tester found issues";
327
+ session.last_reviewer_feedback = `Tester FAILED — fix these issues:\n${summary}`;
328
+ await saveSession(session);
329
+ if (testerResult.stageResult) stageResults.tester = testerResult.stageResult;
330
+ return { action: "continue" };
331
+ }
759
332
  if (testerResult.stageResult) {
760
333
  stageResults.tester = testerResult.stageResult;
761
- await tryBecariaComment({ config, session, logger, agent: "Tester", body: `Tests: ${testerResult.stageResult.summary || "completed"}` });
334
+ await tryCiComment({ config, session, logger, agent: "Tester", body: `Tests: ${testerResult.stageResult.summary || "completed"}` });
762
335
  }
763
336
  }
764
337
 
@@ -768,10 +341,16 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
768
341
  iteration: i, task, diff: postLoopDiff, askQuestion
769
342
  });
770
343
  if (securityResult.action === "pause") return { action: "return", result: securityResult.result };
771
- if (securityResult.action === "continue") return { action: "continue" };
344
+ if (securityResult.action === "continue") {
345
+ const summary = securityResult.stageResult?.summary || "Security found issues";
346
+ session.last_reviewer_feedback = `Security FAILED — fix these issues:\n${summary}`;
347
+ await saveSession(session);
348
+ if (securityResult.stageResult) stageResults.security = securityResult.stageResult;
349
+ return { action: "continue" };
350
+ }
772
351
  if (securityResult.stageResult) {
773
352
  stageResults.security = securityResult.stageResult;
774
- await tryBecariaComment({ config, session, logger, agent: "Security", body: `Security scan: ${securityResult.stageResult.summary || "completed"}` });
353
+ await tryCiComment({ config, session, logger, agent: "Security", body: `Security scan: ${securityResult.stageResult.summary || "completed"}` });
775
354
  }
776
355
  }
777
356
 
@@ -782,7 +361,7 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
782
361
  });
783
362
  if (auditResult.stageResult) {
784
363
  stageResults.audit = auditResult.stageResult;
785
- await tryBecariaComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
364
+ await tryCiComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
786
365
  }
787
366
  if (auditResult.action === "retry") {
788
367
  // Audit found actionable issues — loop back to coder
@@ -817,6 +396,31 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
817
396
  if (rtkSavings) session.rtk_savings = rtkSavings;
818
397
  await saveSession(session);
819
398
 
399
+ // --- Journal: write final files ---
400
+ const journalDir = session._journalDir;
401
+ if (journalDir) {
402
+ try {
403
+ await writeIterationsJournal(journalDir, session._journalIterations || []);
404
+ await writeDecisionsJournal(journalDir, session._journalDecisions || []);
405
+ const hasTree = await writeTreeJournal(journalDir, session.session_start_sha);
406
+
407
+ const journalFiles = [...(session._journalFiles || [])];
408
+ if (session._journalIterations?.length) journalFiles.push("iterations.md");
409
+ if (session._journalDecisions?.length) journalFiles.push("decisions.md");
410
+ if (hasTree) journalFiles.push("tree.txt");
411
+
412
+ await writeSummaryJournal(journalDir, {
413
+ task: session.task, result: "APPROVED", sessionId: session.id,
414
+ iterations: i, durationMs: Date.now() - (session._startedAt || Date.now()),
415
+ budget: budgetSummary(), stages: stageResults,
416
+ commits: gitResult?.commits || [], files: journalFiles
417
+ });
418
+ logger.info(`Session journal written to ${journalDir}`);
419
+ } catch (err) {
420
+ logger.warn(`Journal write failed (non-blocking): ${err.message}`);
421
+ }
422
+ }
423
+
820
424
  const endDetail = { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues };
821
425
  if (rtkSavings) endDetail.rtk_savings = rtkSavings;
822
426
  emitProgress(
@@ -837,8 +441,13 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
837
441
 
838
442
  async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion }) {
839
443
  session.last_reviewer_feedback = review.blocking_issues
840
- .map((x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`)
841
- .join("\n");
444
+ .map((x) => {
445
+ const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
446
+ if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
447
+ if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
448
+ return parts.join("\n");
449
+ })
450
+ .join("\n\n");
842
451
  session.reviewer_retry_count = (session.reviewer_retry_count || 0) + 1;
843
452
  await saveSession(session);
844
453
 
@@ -918,29 +527,6 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
918
527
  applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
919
528
  stageResults.triage = triageResult.stageResult;
920
529
 
921
- // --- Auto-install skills based on task + project detection ---
922
- const projectDir = config.projectDir || process.cwd();
923
- try {
924
- const osAvailable = await isOpenSkillsAvailable();
925
- if (osAvailable) {
926
- const neededSkills = await detectNeededSkills(task, projectDir);
927
- if (neededSkills.length > 0) {
928
- const skillResult = await autoInstallSkills(neededSkills, projectDir);
929
- if (skillResult.installed.length > 0) {
930
- session.autoInstalledSkills = skillResult.installed;
931
- }
932
- emitProgress(emitter, makeEvent("skills:auto-install", { ...eventBase, stage: "triage" }, {
933
- message: skillResult.installed.length > 0
934
- ? `Auto-installed ${skillResult.installed.length} skill(s): ${skillResult.installed.join(", ")}`
935
- : `Skills detected (${neededSkills.join(", ")}) — all already installed or unavailable`,
936
- detail: skillResult
937
- }));
938
- }
939
- }
940
- } catch (err) {
941
- logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
942
- }
943
-
944
530
  // --- Persist inline domain if provided via --domain flag ---
945
531
  if (flags?.domain) {
946
532
  try {
@@ -1079,13 +665,43 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
1079
665
  // --- Researcher → Planner ---
1080
666
  const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion });
1081
667
 
668
+ // --- Update .gitignore with stack-specific entries based on planner/architect output ---
669
+ const projectDir = updatedConfig.projectDir || process.cwd();
670
+ await updateGitignoreForStack(projectDir, { stageResults, task, logger });
671
+
672
+ // --- Auto-install skills based on task + planner output + project detection ---
673
+ // Runs AFTER triage and planner so that the planned task text (which includes
674
+ // planner output like implementation steps) is available for keyword detection.
675
+ // This ensures greenfield projects with no package.json still get correct skills.
676
+ const skillProjectDir = updatedConfig.projectDir || process.cwd();
677
+ try {
678
+ const osAvailable = await isOpenSkillsAvailable();
679
+ if (osAvailable) {
680
+ const neededSkills = await detectNeededSkills(plannedTask, skillProjectDir);
681
+ if (neededSkills.length > 0) {
682
+ const skillResult = await autoInstallSkills(neededSkills, skillProjectDir);
683
+ if (skillResult.installed.length > 0) {
684
+ session.autoInstalledSkills = skillResult.installed;
685
+ }
686
+ emitProgress(emitter, makeEvent("skills:auto-install", { ...eventBase, stage: "skills" }, {
687
+ message: skillResult.installed.length > 0
688
+ ? `Auto-installed ${skillResult.installed.length} skill(s): ${skillResult.installed.join(", ")}`
689
+ : `Skills detected (${neededSkills.join(", ")}) — all already installed or unavailable`,
690
+ detail: skillResult
691
+ }));
692
+ }
693
+ }
694
+ } catch (err) {
695
+ logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
696
+ }
697
+
1082
698
  return { plannedTask, updatedConfig };
1083
699
  }
1084
700
 
1085
- async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i }) {
1086
- const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
701
+ async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i, brainCtx }) {
702
+ const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i, brainCtx });
1087
703
  if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
1088
- const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger });
704
+ const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger, config });
1089
705
  if (coderStandby.handled) {
1090
706
  return coderStandby.action === "return"
1091
707
  ? { action: "return", result: coderStandby.result }
@@ -1095,7 +711,7 @@ async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refac
1095
711
  if (pipelineFlags.refactorerEnabled) {
1096
712
  const refResult = await runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
1097
713
  if (refResult?.action === "pause") return { action: "return", result: refResult.result };
1098
- const refStandby = await handleStandbyResult({ stageResult: refResult, session, emitter, eventBase, i, stage: "refactorer", logger });
714
+ const refStandby = await handleStandbyResult({ stageResult: refResult, session, emitter, eventBase, i, stage: "refactorer", logger, config });
1099
715
  if (refStandby.handled) {
1100
716
  return refStandby.action === "return"
1101
717
  ? { action: "return", result: refStandby.result }
@@ -1188,7 +804,7 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
1188
804
  if (sonarResult.action === "continue") return { action: "continue" };
1189
805
  if (sonarResult.stageResult) {
1190
806
  stageResults.sonar = sonarResult.stageResult;
1191
- await tryBecariaComment({ config, session, logger, agent: "Sonar", body: `SonarQube scan: ${sonarResult.stageResult.summary || "completed"}` });
807
+ await tryCiComment({ config, session, logger, agent: "Sonar", body: `SonarQube scan: ${sonarResult.stageResult.summary || "completed"}` });
1192
808
  }
1193
809
  }
1194
810
 
@@ -1216,7 +832,7 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
1216
832
  return { action: "ok" };
1217
833
  }
1218
834
 
1219
- async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion }) {
835
+ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx }) {
1220
836
  if (!pipelineFlags.reviewerEnabled) {
1221
837
  return {
1222
838
  action: "ok",
@@ -1226,14 +842,29 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
1226
842
 
1227
843
  const reviewerResult = await runReviewerStage({
1228
844
  reviewerRole, config, logger, emitter, eventBase, session, trackBudget,
1229
- iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion
845
+ iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx
1230
846
  });
1231
847
  if (reviewerResult.action === "pause") return { action: "return", result: reviewerResult.result };
1232
- const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger });
848
+ const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger, config, askQuestion });
1233
849
  if (revStandby.handled) {
1234
- return revStandby.action === "return"
1235
- ? { action: "return", result: revStandby.result }
1236
- : { action: "retry" };
850
+ if (revStandby.action === "return") return { action: "return", result: revStandby.result };
851
+ if (revStandby.action === "skip") {
852
+ // Solomon said skip review — treat as approved
853
+ return { action: "ok", review: { approved: true, blocking_issues: [], non_blocking_suggestions: [], summary: "Review skipped (agent rate-limited, Solomon approved)", confidence: 0.7 } };
854
+ }
855
+ if (revStandby.action === "retry_reviewer_only") {
856
+ // Retry just the reviewer — use alternative agent if Solomon recommended one
857
+ let retryReviewerRole = reviewerRole;
858
+ const alt = session._alternative_agent;
859
+ if (alt?.stage === "reviewer" && alt?.provider) {
860
+ const { createAgent } = await import("./agents/index.js");
861
+ retryReviewerRole = { provider: alt.provider, model: null };
862
+ logger.info(`Retrying reviewer with alternative agent: ${alt.provider}`);
863
+ delete session._alternative_agent;
864
+ }
865
+ return runReviewerGateStage({ pipelineFlags: { reviewerEnabled: true }, reviewerRole: retryReviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion });
866
+ }
867
+ return { action: "retry" };
1237
868
  }
1238
869
  if (reviewerResult.stalled) return { action: "return", result: reviewerResult.stalledResult };
1239
870
  return { action: "ok", review: reviewerResult.review };
@@ -1243,7 +874,7 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
1243
874
  session.reviewer_retry_count = 0;
1244
875
  const postLoopResult = await handlePostLoopStages({
1245
876
  config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
1246
- becariaEnabled: Boolean(config.becaria?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger
877
+ ciEnabled: Boolean(config.ci?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger
1247
878
  });
1248
879
  if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
1249
880
  if (postLoopResult.action === "continue") return { action: "continue" };
@@ -1253,7 +884,7 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
1253
884
  }
1254
885
 
1255
886
  async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker }) {
1256
- // Escalate to Solomon / human before giving up
887
+ const budget = budgetSummary();
1257
888
  const solomonResult = await invokeSolomon({
1258
889
  config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
1259
890
  iteration: config.max_iterations,
@@ -1262,17 +893,26 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
1262
893
  task,
1263
894
  iterationCount: config.max_iterations,
1264
895
  maxIterations: config.max_iterations,
896
+ budget_usd: budget?.total_cost_usd || 0,
1265
897
  history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached without reviewer approval" }]
1266
898
  }
1267
899
  });
1268
900
 
901
+ // Solomon approved the work — treat as successful completion
902
+ if (solomonResult.action === "approve") {
903
+ logger.info("Solomon approved coder's work at max_iterations checkpoint");
904
+ return { approved: true, sessionId: session.id, reason: "solomon_approved" };
905
+ }
906
+
907
+ // Solomon says continue — extend iterations
1269
908
  if (solomonResult.action === "continue") {
1270
909
  if (solomonResult.humanGuidance) {
1271
- session.last_reviewer_feedback = `User guidance: ${solomonResult.humanGuidance}`;
910
+ session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
1272
911
  }
1273
912
  session.reviewer_retry_count = 0;
1274
913
  await saveSession(session);
1275
- return { approved: false, sessionId: session.id, reason: "max_iterations_extended", humanGuidance: solomonResult.humanGuidance };
914
+ const extraIterations = solomonResult.extraIterations || config.max_iterations;
915
+ return { approved: false, sessionId: session.id, reason: "max_iterations_extended", humanGuidance: solomonResult.humanGuidance, extraIterations };
1276
916
  }
1277
917
 
1278
918
  if (solomonResult.action === "pause") {
@@ -1283,7 +923,7 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
1283
923
  return { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "max_iterations_subtask" };
1284
924
  }
1285
925
 
1286
- // Solomon also couldn't resolve — fail
926
+ // Solomon couldn't resolve — fail
1287
927
  session.budget = budgetSummary();
1288
928
  const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
1289
929
  if (rtkSavings) session.rtk_savings = rtkSavings;
@@ -1320,6 +960,44 @@ async function tryAutoStartBoard(config, logger, emitter, eventBase) {
1320
960
  }
1321
961
 
1322
962
  async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
963
+ // Auto-init .karajan/ if missing (copies coder-rules, review-rules, role templates)
964
+ const initProjectDir = config.projectDir || process.cwd();
965
+ await autoInit(initProjectDir, logger);
966
+
967
+ // Smart role assignment: detect installed AIs and assign to roles
968
+ // Only runs if: (a) no roles configured AND (b) not in test environment
969
+ const needsAssignment = !config.roles?.coder?.provider && !config.coder && process.env.NODE_ENV !== "test" && !process.env.VITEST;
970
+ if (needsAssignment) {
971
+ try {
972
+ const { autoAssignRoles, applyRoleAssignments } = await import("./utils/role-assigner.js");
973
+ const { assignments } = await autoAssignRoles(logger);
974
+ if (assignments) config = applyRoleAssignments({ ...config }, assignments);
975
+ } catch { /* non-blocking — defaults will be used */ }
976
+ }
977
+
978
+ // Scope all git diffs to projectDir (prevents leaking unrelated branch changes)
979
+ // When running from a subdirectory of a git repo, use relative path as scope
980
+ let diffScope = config.projectDir || null;
981
+ if (!diffScope) {
982
+ try {
983
+ const { execSync } = await import("node:child_process");
984
+ const repoRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
985
+ const cwd = process.cwd();
986
+ if (cwd !== repoRoot && cwd.startsWith(repoRoot)) {
987
+ diffScope = cwd.slice(repoRoot.length + 1);
988
+ logger.info(`Running from subdirectory — diff scoped to ${diffScope}/`);
989
+ }
990
+ } catch { /* git not available */ }
991
+ }
992
+ setDiffProjectDir(diffScope);
993
+
994
+ // Auto-detect Chrome DevTools MCP
995
+ const { detectDevToolsMcp } = await import("./webperf/devtools-detect.js");
996
+ const devToolsAvailable = await detectDevToolsMcp(logger);
997
+ if (devToolsAvailable) {
998
+ config = { ...config, webperf: { ...config.webperf, devtools_mcp: true } };
999
+ }
1000
+
1323
1001
  const ctx = new PipelineContext({ config, session: null, logger, emitter, task, flags });
1324
1002
  ctx.askQuestion = askQuestion;
1325
1003
  ctx.pgTaskId = pgTaskId;
@@ -1340,23 +1018,19 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1340
1018
  ctx.trackBudget = trackBudget;
1341
1019
 
1342
1020
  // --- RTK detection ---
1343
- if (config.proxy?.enabled) {
1344
- logger.info("Proxy compression active, RTK not needed");
1345
- } else {
1346
- const rtkResult = await detectRtk();
1347
- if (rtkResult.available) {
1348
- config = { ...config, rtk: { available: true, version: rtkResult.version } };
1349
- const rtkTracker = new RtkSavingsTracker();
1350
- const rtkRunner = createRtkRunner(true, rtkTracker);
1351
- setDiffRunner(rtkRunner);
1352
- setGitRunner(rtkRunner);
1353
- ctx.rtkTracker = rtkTracker;
1354
- logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
1355
- emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
1356
- message: "RTK detected — internal commands wrapped for token optimization",
1357
- detail: { version: rtkResult.version, executorType: "local" }
1358
- }));
1359
- }
1021
+ const rtkResult = await detectRtk();
1022
+ if (rtkResult.available) {
1023
+ config = { ...config, rtk: { available: true, version: rtkResult.version } };
1024
+ const rtkTracker = new RtkSavingsTracker();
1025
+ const rtkRunner = createRtkRunner(true, rtkTracker);
1026
+ setDiffRunner(rtkRunner);
1027
+ setGitRunner(rtkRunner);
1028
+ ctx.rtkTracker = rtkTracker;
1029
+ logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
1030
+ emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
1031
+ message: "RTK detected — internal commands wrapped for token optimization",
1032
+ detail: { version: rtkResult.version, executorType: "local" }
1033
+ }));
1360
1034
  }
1361
1035
 
1362
1036
  // --- HU Board auto-start ---
@@ -1377,26 +1051,18 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1377
1051
  ctx.session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
1378
1052
  ctx.eventBase.sessionId = ctx.session.id;
1379
1053
 
1054
+ // Karajan Brain: initialize runtime context (opt-in via config.brain.enabled)
1055
+ const { createBrainContext, isBrainEnabled } = await import("./orchestrator/brain-coordinator.js");
1056
+ ctx.brainCtx = createBrainContext({ enabled: isBrainEnabled(config) });
1057
+ if (ctx.brainCtx.enabled) {
1058
+ logger.info("Karajan Brain enabled — feedback queue, verification, compression active");
1059
+ }
1060
+
1380
1061
  const { initPgAdapter } = await import("./planning-game/pipeline-adapter.js");
1381
1062
  const pgAdapterResult = await initPgAdapter({ session: ctx.session, config, logger, pgTaskId, pgProject });
1382
1063
  ctx.pgCard = pgAdapterResult.pgCard;
1383
1064
  ctx.session.pg_card = ctx.pgCard || null;
1384
1065
 
1385
- // --- Proxy startup ---
1386
- if (config.proxy?.enabled !== false) {
1387
- try {
1388
- const proxyResult = await startProxy({ config: config.proxy || {}, sessionId: ctx.session.id });
1389
- ctx.proxyPort = proxyResult.port;
1390
- logger.info(`Proxy started on 127.0.0.1:${proxyResult.port}`);
1391
- emitProgress(emitter, makeEvent("proxy:started", ctx.eventBase, {
1392
- message: `Proxy started on port ${proxyResult.port}`,
1393
- detail: { port: proxyResult.port, baseUrls: proxyResult.baseUrls }
1394
- }));
1395
- } catch (err) {
1396
- logger.warn(`Proxy startup failed (non-blocking): ${err.message}`);
1397
- }
1398
- }
1399
-
1400
1066
  emitProgress(
1401
1067
  emitter,
1402
1068
  makeEvent("session:start", ctx.eventBase, {
@@ -1412,6 +1078,38 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1412
1078
  ctx.plannedTask = preLoopResult.plannedTask;
1413
1079
  ctx.config = preLoopResult.updatedConfig;
1414
1080
 
1081
+ // --- Session Journal: persist pre-loop outputs + display plan summary ---
1082
+ const reportDir = ctx.config.output?.report_dir || ".reviews";
1083
+ try {
1084
+ ctx.journalDir = await createJournalDir(reportDir, ctx.session.id);
1085
+ const journalFiles = await writePreLoopJournal(ctx.journalDir, ctx.stageResults);
1086
+ ctx.journalFiles = journalFiles;
1087
+ ctx.journalIterations = [];
1088
+ ctx.journalDecisions = [];
1089
+
1090
+ // Attach journal state to session so finalizeApprovedSession can access it
1091
+ ctx.session._journalDir = ctx.journalDir;
1092
+ ctx.session._journalFiles = journalFiles;
1093
+ ctx.session._journalIterations = ctx.journalIterations;
1094
+ ctx.session._journalDecisions = ctx.journalDecisions;
1095
+ ctx.session._startedAt = ctx.startedAt;
1096
+
1097
+ // Display plan summary in console before iteration loop
1098
+ const planSummary = buildPlanSummary({
1099
+ pipelineFlags: ctx.pipelineFlags,
1100
+ config: ctx.config,
1101
+ stageResults: ctx.stageResults,
1102
+ task
1103
+ });
1104
+ console.log(planSummary);
1105
+ } catch (err) {
1106
+ logger.warn(`Journal init failed (non-blocking): ${err.message}`);
1107
+ ctx.journalDir = null;
1108
+ ctx.journalFiles = [];
1109
+ ctx.journalIterations = [];
1110
+ ctx.journalDecisions = [];
1111
+ }
1112
+
1415
1113
  ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session });
1416
1114
  const projectDir = ctx.config.projectDir || process.cwd();
1417
1115
  ctx.reviewRules = (await resolveReviewProfile({ mode: ctx.config.review_mode, projectDir })).rules;
@@ -1424,7 +1122,7 @@ async function runSingleIteration(ctx) {
1424
1122
  const { config, logger, emitter, eventBase, session, task, iteration: i } = ctx;
1425
1123
 
1426
1124
  const iterStart = Date.now();
1427
- const becariaEnabled = Boolean(config.becaria?.enabled) && ctx.gitCtx?.enabled;
1125
+ const ciEnabled = Boolean(config.ci?.enabled) && ctx.gitCtx?.enabled;
1428
1126
  logger.setContext({ iteration: i, stage: "iteration" });
1429
1127
 
1430
1128
  const reviewerRetryCount = session.reviewer_retry_count || 0;
@@ -1440,7 +1138,7 @@ async function runSingleIteration(ctx) {
1440
1138
  const crResult = await runCoderAndRefactorerStages({
1441
1139
  coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole, refactorerRole: ctx.refactorerRole,
1442
1140
  pipelineFlags: ctx.pipelineFlags, config, logger, emitter, eventBase, session,
1443
- plannedTask: ctx.plannedTask, trackBudget: ctx.trackBudget, i
1141
+ plannedTask: ctx.plannedTask, trackBudget: ctx.trackBudget, i, brainCtx: ctx.brainCtx
1444
1142
  });
1445
1143
  if (crResult.action === "return" || crResult.action === "retry") return crResult;
1446
1144
 
@@ -1455,15 +1153,16 @@ async function runSingleIteration(ctx) {
1455
1153
  });
1456
1154
  if (qgResult.action === "return" || qgResult.action === "continue") return qgResult;
1457
1155
 
1458
- await handleBecariaEarlyPrOrPush({
1459
- becariaEnabled, config, session, emitter, eventBase, gitCtx: ctx.gitCtx, task, logger,
1156
+ await handleCiEarlyPrOrPush({
1157
+ ciEnabled, config, session, emitter, eventBase, gitCtx: ctx.gitCtx, task, logger,
1460
1158
  stageResults: ctx.stageResults, i
1461
1159
  });
1462
1160
 
1463
1161
  const revResult = await runReviewerGateStage({
1464
1162
  pipelineFlags: ctx.pipelineFlags, reviewerRole: ctx.reviewerRole, config, logger, emitter, eventBase,
1465
1163
  session, trackBudget: ctx.trackBudget, i, reviewRules: ctx.reviewRules, task,
1466
- repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary, askQuestion: ctx.askQuestion
1164
+ repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary, askQuestion: ctx.askQuestion,
1165
+ brainCtx: ctx.brainCtx
1467
1166
  });
1468
1167
  if (revResult.action === "return" || revResult.action === "retry") return revResult;
1469
1168
  const review = revResult.review;
@@ -1474,13 +1173,26 @@ async function runSingleIteration(ctx) {
1474
1173
  }));
1475
1174
  session.standby_retry_count = 0;
1476
1175
 
1176
+ // --- Journal: record iteration ---
1177
+ if (ctx.journalIterations) {
1178
+ ctx.journalIterations.push(formatIteration({
1179
+ iteration: i,
1180
+ coderSummary: ctx.stageResults.coder?.summary || null,
1181
+ reviewerSummary: review?.approved ? `Approved: ${review.raw_summary || ""}` : `Rejected: ${(review?.blocking_issues || []).length} blocking issue(s)`,
1182
+ sonarSummary: ctx.stageResults.sonar?.summary || null,
1183
+ testerSummary: ctx.stageResults.tester?.summary || null,
1184
+ securitySummary: ctx.stageResults.security?.summary || null,
1185
+ durationMs: iterDuration
1186
+ }));
1187
+ }
1188
+
1477
1189
  const solomonResult = await handleSolomonCheck({
1478
1190
  config, session, emitter, eventBase, logger, task, i, askQuestion: ctx.askQuestion,
1479
- becariaEnabled, blockingIssues: review?.blocking_issues
1191
+ ciEnabled, blockingIssues: review?.blocking_issues, brainCtx: ctx.brainCtx
1480
1192
  });
1481
1193
  if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
1482
1194
 
1483
- await handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger });
1195
+ await handleCiReviewDispatch({ ciEnabled, config, session, review, i, logger });
1484
1196
 
1485
1197
  if (review.approved) {
1486
1198
  const approvedResult = await handleApprovedReview({
@@ -1500,8 +1212,13 @@ async function runSingleIteration(ctx) {
1500
1212
  } else {
1501
1213
  // Solomon is enabled — feed back the blocking issues for the next coder iteration
1502
1214
  session.last_reviewer_feedback = review.blocking_issues
1503
- .map((x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`)
1504
- .join("\n");
1215
+ .map((x) => {
1216
+ const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
1217
+ if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
1218
+ if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
1219
+ return parts.join("\n");
1220
+ })
1221
+ .join("\n\n");
1505
1222
  await saveSession(session);
1506
1223
  }
1507
1224
 
@@ -1559,14 +1276,92 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
1559
1276
  ctx.eventBase.iteration = i;
1560
1277
  ctx.iteration = i;
1561
1278
 
1562
- const iterResult = await runSingleIteration(ctx);
1279
+ let iterResult;
1280
+ try {
1281
+ iterResult = await runSingleIteration(ctx);
1282
+ } catch (stageError) {
1283
+ // ANY unhandled error in a stage = out of normal flow → Solomon decides
1284
+ logger.warn(`Stage error caught — escalating to Solomon: ${stageError.message}`);
1285
+ const solomonResult = await invokeSolomon({
1286
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
1287
+ stage: "stage_error", askQuestion, session: ctx.session, iteration: i,
1288
+ conflict: {
1289
+ stage: "stage_error",
1290
+ task: loopTask,
1291
+ iterationCount: i,
1292
+ maxIterations: ctx.config.max_iterations,
1293
+ budget_usd: ctx.budgetSummary()?.total_cost_usd || 0,
1294
+ history: [{ agent: "pipeline", feedback: `Stage threw: ${stageError.message}` }]
1295
+ }
1296
+ });
1297
+
1298
+ if (solomonResult.action === "approve") {
1299
+ logger.info("Solomon approved despite stage error");
1300
+ return { approved: true, sessionId: ctx.session.id, reason: "solomon_approved_after_error" };
1301
+ }
1302
+ if (solomonResult.action === "continue") {
1303
+ if (solomonResult.humanGuidance) {
1304
+ ctx.session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
1305
+ }
1306
+ continue; // next iteration
1307
+ }
1308
+ if (solomonResult.action === "pause") {
1309
+ return { paused: true, sessionId: ctx.session.id, question: solomonResult.question, context: "stage_error" };
1310
+ }
1311
+ // Solomon couldn't resolve — fail
1312
+ await markSessionStatus(ctx.session, "failed");
1313
+ return { approved: false, sessionId: ctx.session.id, reason: "stage_error", error: stageError.message };
1314
+ }
1315
+
1563
1316
  if (iterResult.action === "return") {
1564
1317
  return iterResult.result;
1565
1318
  }
1566
1319
  if (iterResult.action === "retry") { i -= 1; }
1567
1320
  }
1568
1321
 
1322
+ // Solomon decides whether to extend iterations or stop
1569
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 });
1324
+
1325
+ // Solomon said "continue" — extend iterations and keep going
1326
+ if (maxIterResult.reason === "max_iterations_extended") {
1327
+ const extra = maxIterResult.extraIterations || ctx.config.max_iterations;
1328
+ ctx.config.max_iterations += extra;
1329
+ logger.info(`Solomon extended pipeline by ${extra} iterations (new max: ${ctx.config.max_iterations})`);
1330
+
1331
+ if (maxIterResult.humanGuidance) {
1332
+ ctx.session.last_reviewer_feedback = `Solomon guidance: ${maxIterResult.humanGuidance}`;
1333
+ }
1334
+
1335
+ // Continue the loop
1336
+ while (i < ctx.config.max_iterations) {
1337
+ i += 1;
1338
+ const elapsedMinutes = (Date.now() - ctx.startedAt) / 60000;
1339
+
1340
+ const cpResult = await handleCheckpoint({
1341
+ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
1342
+ i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary, lastCheckpointSnapshot
1343
+ });
1344
+ if (cpResult.action === "stop") return cpResult.result;
1345
+ checkpointDisabled = cpResult.checkpointDisabled;
1346
+ lastCheckpointAt = cpResult.lastCheckpointAt;
1347
+ if (cpResult.lastCheckpointSnapshot !== undefined) lastCheckpointSnapshot = cpResult.lastCheckpointSnapshot;
1348
+
1349
+ await checkSessionTimeout({ askQuestion, elapsedMinutes, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
1350
+ await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
1351
+
1352
+ ctx.eventBase.iteration = i;
1353
+ ctx.iteration = i;
1354
+
1355
+ const iterResult = await runSingleIteration(ctx);
1356
+ if (iterResult.action === "return") return iterResult.result;
1357
+ if (iterResult.action === "retry") { i -= 1; }
1358
+ }
1359
+
1360
+ // 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 });
1362
+ return finalResult;
1363
+ }
1364
+
1570
1365
  return maxIterResult;
1571
1366
  }
1572
1367
 
@@ -1577,7 +1372,29 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1577
1372
  return handleDryRun({ task, config, flags, emitter, pipelineFlags });
1578
1373
  }
1579
1374
 
1580
- const ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
1375
+ let ctx;
1376
+ try {
1377
+ ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
1378
+ } catch (initError) {
1379
+ // Pre-loop stage failure → Solomon decides
1380
+ logger.warn(`Init/pre-loop error — escalating to Solomon: ${initError.message}`);
1381
+ const tempSession = { id: "init-error", task, status: "failed" };
1382
+ const solomonResult = await invokeSolomon({
1383
+ config, logger, emitter, eventBase: { sessionId: "init-error", iteration: 0, stage: "init", startedAt: Date.now() },
1384
+ stage: "init_error", askQuestion, session: tempSession, iteration: 0,
1385
+ conflict: {
1386
+ stage: "init_error",
1387
+ task,
1388
+ iterationCount: 0,
1389
+ maxIterations: config.max_iterations || 5,
1390
+ history: [{ agent: "pipeline", feedback: `Initialization failed: ${initError.message}` }]
1391
+ }
1392
+ });
1393
+ if (solomonResult.action === "pause") {
1394
+ return { paused: true, sessionId: "init-error", question: solomonResult.question, context: "init_error" };
1395
+ }
1396
+ throw initError; // Solomon couldn't resolve — propagate
1397
+ }
1581
1398
 
1582
1399
  try {
1583
1400
  // --- Analysis-only flow: skip coder/reviewer when coderRequired === false ---
@@ -1667,14 +1484,6 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1667
1484
  await writeHistoryRecord({ sessionId: ctx.session.id, task, result, logger });
1668
1485
  return result;
1669
1486
  } finally {
1670
- // --- Proxy shutdown ---
1671
- try {
1672
- await stopProxy();
1673
- if (ctx.proxyPort) {
1674
- logger.info("Proxy stopped");
1675
- }
1676
- } catch { /* non-blocking */ }
1677
-
1678
1487
  // --- Cleanup auto-installed skills ---
1679
1488
  const autoSkills = ctx.session?.autoInstalledSkills;
1680
1489
  if (autoSkills && autoSkills.length > 0) {