karajan-code 1.59.1 → 2.0.0

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 (112) 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/doctor.js +22 -40
  14. package/src/commands/init.js +8 -8
  15. package/src/commands/report.js +0 -13
  16. package/src/commands/review.js +17 -17
  17. package/src/commands/run.js +1 -2
  18. package/src/commands/status.js +1 -7
  19. package/src/config.js +10 -52
  20. package/src/git/automation.js +9 -9
  21. package/src/guards/output-guard.js +0 -17
  22. package/src/mcp/handlers/direct-handlers.js +1 -1
  23. package/src/mcp/handlers/run-handler.js +1 -2
  24. package/src/mcp/sovereignty-guard.js +1 -1
  25. package/src/mcp/tools.js +1 -1
  26. package/src/orchestrator/brain-coordinator.js +165 -0
  27. package/src/orchestrator/ci-integration.js +139 -0
  28. package/src/orchestrator/config-init.js +435 -0
  29. package/src/orchestrator/direct-actions.js +186 -0
  30. package/src/orchestrator/feedback-enrichment.js +140 -0
  31. package/src/orchestrator/feedback-queue.js +152 -0
  32. package/src/orchestrator/flow-control.js +159 -0
  33. package/src/orchestrator/post-loop-stages.js +20 -15
  34. package/src/orchestrator/preflight-checks.js +7 -3
  35. package/src/orchestrator/role-output-compressor.js +190 -0
  36. package/src/orchestrator/session-journal.js +344 -0
  37. package/src/orchestrator/solomon-escalation.js +38 -5
  38. package/src/orchestrator/solomon-rules.js +32 -33
  39. package/src/orchestrator/stages/reviewer-stage.js +16 -5
  40. package/src/orchestrator/stages/sonar-stage.js +24 -1
  41. package/src/orchestrator/verification-gate.js +146 -0
  42. package/src/orchestrator.js +428 -664
  43. package/src/planning-game/client.js +2 -2
  44. package/src/planning-game/pipeline-adapter.js +1 -1
  45. package/src/prompts/coder.js +17 -3
  46. package/src/prompts/reviewer.js +2 -2
  47. package/src/prompts/rtk-snippet.js +3 -5
  48. package/src/review/diff-generator.js +66 -17
  49. package/src/review/snapshot-diff.js +122 -0
  50. package/src/review/tdd-policy.js +4 -2
  51. package/src/roles/agent-role.js +135 -0
  52. package/src/roles/architect-role.js +61 -108
  53. package/src/roles/audit-role.js +25 -71
  54. package/src/roles/coder-role.js +32 -57
  55. package/src/roles/discover-role.js +30 -71
  56. package/src/roles/hu-reviewer-role.js +51 -99
  57. package/src/roles/impeccable-role.js +33 -88
  58. package/src/roles/index.js +2 -0
  59. package/src/roles/karajan-brain-role.js +134 -0
  60. package/src/roles/planner-role.js +40 -75
  61. package/src/roles/refactorer-role.js +22 -64
  62. package/src/roles/researcher-role.js +51 -115
  63. package/src/roles/reviewer-role.js +68 -125
  64. package/src/roles/security-role.js +38 -102
  65. package/src/roles/solomon-role.js +69 -11
  66. package/src/roles/tester-role.js +121 -95
  67. package/src/roles/triage-role.js +50 -111
  68. package/src/session-store.js +1 -1
  69. package/src/skills/skill-detector.js +11 -0
  70. package/src/sonar/config-resolver.js +20 -1
  71. package/src/sonar/credentials.js +17 -0
  72. package/src/sonar/project-key.js +1 -1
  73. package/src/sonar/scanner.js +1 -1
  74. package/src/utils/display.js +33 -14
  75. package/src/utils/injection-guard.js +23 -8
  76. package/src/utils/logger.js +6 -1
  77. package/src/utils/process.js +3 -4
  78. package/src/utils/role-assigner.js +131 -0
  79. package/src/utils/update-check.js +5 -1
  80. package/src/webperf/devtools-detect.js +70 -23
  81. package/templates/roles/coder.md +7 -0
  82. package/templates/roles/karajan-brain.md +95 -0
  83. package/templates/roles/solomon.md +67 -106
  84. package/templates/workflows/automerge.yml +4 -4
  85. package/templates/workflows/houston-override.yml +4 -4
  86. package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
  87. package/src/proxy/adapters/anthropic.js +0 -131
  88. package/src/proxy/adapters/gemini.js +0 -106
  89. package/src/proxy/adapters/index.js +0 -51
  90. package/src/proxy/adapters/openai.js +0 -71
  91. package/src/proxy/compression/ai-compressor.js +0 -209
  92. package/src/proxy/compression/dedup-cache.js +0 -100
  93. package/src/proxy/compression/deterministic/bash-build.js +0 -60
  94. package/src/proxy/compression/deterministic/bash-git.js +0 -101
  95. package/src/proxy/compression/deterministic/bash-infra.js +0 -64
  96. package/src/proxy/compression/deterministic/bash-misc.js +0 -53
  97. package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
  98. package/src/proxy/compression/deterministic/bash-test.js +0 -61
  99. package/src/proxy/compression/deterministic/glob.js +0 -26
  100. package/src/proxy/compression/deterministic/grep.js +0 -53
  101. package/src/proxy/compression/deterministic/index.js +0 -58
  102. package/src/proxy/compression/deterministic/read.js +0 -22
  103. package/src/proxy/compression/deterministic/utils.js +0 -33
  104. package/src/proxy/compression/pipeline.js +0 -270
  105. package/src/proxy/middleware/prompt-injector.js +0 -165
  106. package/src/proxy/middleware/response-interceptor.js +0 -223
  107. package/src/proxy/progress-monitor.js +0 -98
  108. package/src/proxy/proxy-lifecycle.js +0 -201
  109. package/src/proxy/proxy-server.js +0 -239
  110. package/src/proxy/security.js +0 -45
  111. /package/src/{becaria → ci}/index.js +0 -0
  112. /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
-
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
41
 
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
115
 
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
116
 
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
-
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
-
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
155
 
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
-
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
-
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
188
 
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 }) {
633
214
  if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
634
215
 
635
216
  try {
@@ -643,15 +224,15 @@ async function handleSolomonCheck({ config, session, emitter, eventBase, logger,
643
224
  if (pauseResult) return pauseResult;
644
225
  }
645
226
 
646
- if (becariaEnabled && session.becaria_pr_number) {
227
+ if (ciEnabled && session.ci_pr_number) {
647
228
  const alerts = rulesResult.alerts || [];
648
229
  const alertMsg = alerts.length > 0
649
230
  ? alerts.map(a => `- [${a.severity}] ${a.message}`).join("\n")
650
231
  : "No anomalies detected";
651
- await tryBecariaComment({
232
+ await tryCiComment({
652
233
  config, session, logger,
653
234
  agent: "Solomon",
654
- body: `Supervisor check iteración ${i}: ${alertMsg}`
235
+ body: `Supervisor check iteration ${i}: ${alertMsg}`
655
236
  });
656
237
  }
657
238
  } catch (err) {
@@ -694,59 +275,8 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
694
275
  return null;
695
276
  }
696
277
 
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
278
 
749
- async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, becariaEnabled, testerEnabled, securityEnabled, askQuestion, logger }) {
279
+ async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, ciEnabled, testerEnabled, securityEnabled, askQuestion, logger }) {
750
280
  const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
751
281
 
752
282
  if (testerEnabled) {
@@ -755,10 +285,16 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
755
285
  iteration: i, task, diff: postLoopDiff, askQuestion
756
286
  });
757
287
  if (testerResult.action === "pause") return { action: "return", result: testerResult.result };
758
- if (testerResult.action === "continue") return { action: "continue" };
288
+ if (testerResult.action === "continue") {
289
+ const summary = testerResult.stageResult?.summary || "Tester found issues";
290
+ session.last_reviewer_feedback = `Tester FAILED — fix these issues:\n${summary}`;
291
+ await saveSession(session);
292
+ if (testerResult.stageResult) stageResults.tester = testerResult.stageResult;
293
+ return { action: "continue" };
294
+ }
759
295
  if (testerResult.stageResult) {
760
296
  stageResults.tester = testerResult.stageResult;
761
- await tryBecariaComment({ config, session, logger, agent: "Tester", body: `Tests: ${testerResult.stageResult.summary || "completed"}` });
297
+ await tryCiComment({ config, session, logger, agent: "Tester", body: `Tests: ${testerResult.stageResult.summary || "completed"}` });
762
298
  }
763
299
  }
764
300
 
@@ -768,10 +304,16 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
768
304
  iteration: i, task, diff: postLoopDiff, askQuestion
769
305
  });
770
306
  if (securityResult.action === "pause") return { action: "return", result: securityResult.result };
771
- if (securityResult.action === "continue") return { action: "continue" };
307
+ if (securityResult.action === "continue") {
308
+ const summary = securityResult.stageResult?.summary || "Security found issues";
309
+ session.last_reviewer_feedback = `Security FAILED — fix these issues:\n${summary}`;
310
+ await saveSession(session);
311
+ if (securityResult.stageResult) stageResults.security = securityResult.stageResult;
312
+ return { action: "continue" };
313
+ }
772
314
  if (securityResult.stageResult) {
773
315
  stageResults.security = securityResult.stageResult;
774
- await tryBecariaComment({ config, session, logger, agent: "Security", body: `Security scan: ${securityResult.stageResult.summary || "completed"}` });
316
+ await tryCiComment({ config, session, logger, agent: "Security", body: `Security scan: ${securityResult.stageResult.summary || "completed"}` });
775
317
  }
776
318
  }
777
319
 
@@ -782,7 +324,7 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
782
324
  });
783
325
  if (auditResult.stageResult) {
784
326
  stageResults.audit = auditResult.stageResult;
785
- await tryBecariaComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
327
+ await tryCiComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
786
328
  }
787
329
  if (auditResult.action === "retry") {
788
330
  // Audit found actionable issues — loop back to coder
@@ -817,6 +359,31 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
817
359
  if (rtkSavings) session.rtk_savings = rtkSavings;
818
360
  await saveSession(session);
819
361
 
362
+ // --- Journal: write final files ---
363
+ const journalDir = session._journalDir;
364
+ if (journalDir) {
365
+ try {
366
+ await writeIterationsJournal(journalDir, session._journalIterations || []);
367
+ await writeDecisionsJournal(journalDir, session._journalDecisions || []);
368
+ const hasTree = await writeTreeJournal(journalDir, session.session_start_sha);
369
+
370
+ const journalFiles = [...(session._journalFiles || [])];
371
+ if (session._journalIterations?.length) journalFiles.push("iterations.md");
372
+ if (session._journalDecisions?.length) journalFiles.push("decisions.md");
373
+ if (hasTree) journalFiles.push("tree.txt");
374
+
375
+ await writeSummaryJournal(journalDir, {
376
+ task: session.task, result: "APPROVED", sessionId: session.id,
377
+ iterations: i, durationMs: Date.now() - (session._startedAt || Date.now()),
378
+ budget: budgetSummary(), stages: stageResults,
379
+ commits: gitResult?.commits || [], files: journalFiles
380
+ });
381
+ logger.info(`Session journal written to ${journalDir}`);
382
+ } catch (err) {
383
+ logger.warn(`Journal write failed (non-blocking): ${err.message}`);
384
+ }
385
+ }
386
+
820
387
  const endDetail = { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues };
821
388
  if (rtkSavings) endDetail.rtk_savings = rtkSavings;
822
389
  emitProgress(
@@ -837,8 +404,13 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
837
404
 
838
405
  async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion }) {
839
406
  session.last_reviewer_feedback = review.blocking_issues
840
- .map((x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`)
841
- .join("\n");
407
+ .map((x) => {
408
+ const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
409
+ if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
410
+ if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
411
+ return parts.join("\n");
412
+ })
413
+ .join("\n\n");
842
414
  session.reviewer_retry_count = (session.reviewer_retry_count || 0) + 1;
843
415
  await saveSession(session);
844
416
 
@@ -918,29 +490,6 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
918
490
  applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
919
491
  stageResults.triage = triageResult.stageResult;
920
492
 
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
493
  // --- Persist inline domain if provided via --domain flag ---
945
494
  if (flags?.domain) {
946
495
  try {
@@ -1079,13 +628,43 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
1079
628
  // --- Researcher → Planner ---
1080
629
  const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion });
1081
630
 
631
+ // --- Update .gitignore with stack-specific entries based on planner/architect output ---
632
+ const projectDir = updatedConfig.projectDir || process.cwd();
633
+ await updateGitignoreForStack(projectDir, { stageResults, task, logger });
634
+
635
+ // --- Auto-install skills based on task + planner output + project detection ---
636
+ // Runs AFTER triage and planner so that the planned task text (which includes
637
+ // planner output like implementation steps) is available for keyword detection.
638
+ // This ensures greenfield projects with no package.json still get correct skills.
639
+ const skillProjectDir = updatedConfig.projectDir || process.cwd();
640
+ try {
641
+ const osAvailable = await isOpenSkillsAvailable();
642
+ if (osAvailable) {
643
+ const neededSkills = await detectNeededSkills(plannedTask, skillProjectDir);
644
+ if (neededSkills.length > 0) {
645
+ const skillResult = await autoInstallSkills(neededSkills, skillProjectDir);
646
+ if (skillResult.installed.length > 0) {
647
+ session.autoInstalledSkills = skillResult.installed;
648
+ }
649
+ emitProgress(emitter, makeEvent("skills:auto-install", { ...eventBase, stage: "skills" }, {
650
+ message: skillResult.installed.length > 0
651
+ ? `Auto-installed ${skillResult.installed.length} skill(s): ${skillResult.installed.join(", ")}`
652
+ : `Skills detected (${neededSkills.join(", ")}) — all already installed or unavailable`,
653
+ detail: skillResult
654
+ }));
655
+ }
656
+ }
657
+ } catch (err) {
658
+ logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
659
+ }
660
+
1082
661
  return { plannedTask, updatedConfig };
1083
662
  }
1084
663
 
1085
664
  async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i }) {
1086
665
  const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
1087
666
  if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
1088
- const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger });
667
+ const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger, config });
1089
668
  if (coderStandby.handled) {
1090
669
  return coderStandby.action === "return"
1091
670
  ? { action: "return", result: coderStandby.result }
@@ -1095,7 +674,7 @@ async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refac
1095
674
  if (pipelineFlags.refactorerEnabled) {
1096
675
  const refResult = await runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
1097
676
  if (refResult?.action === "pause") return { action: "return", result: refResult.result };
1098
- const refStandby = await handleStandbyResult({ stageResult: refResult, session, emitter, eventBase, i, stage: "refactorer", logger });
677
+ const refStandby = await handleStandbyResult({ stageResult: refResult, session, emitter, eventBase, i, stage: "refactorer", logger, config });
1099
678
  if (refStandby.handled) {
1100
679
  return refStandby.action === "return"
1101
680
  ? { action: "return", result: refStandby.result }
@@ -1188,7 +767,7 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
1188
767
  if (sonarResult.action === "continue") return { action: "continue" };
1189
768
  if (sonarResult.stageResult) {
1190
769
  stageResults.sonar = sonarResult.stageResult;
1191
- await tryBecariaComment({ config, session, logger, agent: "Sonar", body: `SonarQube scan: ${sonarResult.stageResult.summary || "completed"}` });
770
+ await tryCiComment({ config, session, logger, agent: "Sonar", body: `SonarQube scan: ${sonarResult.stageResult.summary || "completed"}` });
1192
771
  }
1193
772
  }
1194
773
 
@@ -1229,11 +808,26 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
1229
808
  iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion
1230
809
  });
1231
810
  if (reviewerResult.action === "pause") return { action: "return", result: reviewerResult.result };
1232
- const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger });
811
+ const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger, config, askQuestion });
1233
812
  if (revStandby.handled) {
1234
- return revStandby.action === "return"
1235
- ? { action: "return", result: revStandby.result }
1236
- : { action: "retry" };
813
+ if (revStandby.action === "return") return { action: "return", result: revStandby.result };
814
+ if (revStandby.action === "skip") {
815
+ // Solomon said skip review — treat as approved
816
+ return { action: "ok", review: { approved: true, blocking_issues: [], non_blocking_suggestions: [], summary: "Review skipped (agent rate-limited, Solomon approved)", confidence: 0.7 } };
817
+ }
818
+ if (revStandby.action === "retry_reviewer_only") {
819
+ // Retry just the reviewer — use alternative agent if Solomon recommended one
820
+ let retryReviewerRole = reviewerRole;
821
+ const alt = session._alternative_agent;
822
+ if (alt?.stage === "reviewer" && alt?.provider) {
823
+ const { createAgent } = await import("./agents/index.js");
824
+ retryReviewerRole = { provider: alt.provider, model: null };
825
+ logger.info(`Retrying reviewer with alternative agent: ${alt.provider}`);
826
+ delete session._alternative_agent;
827
+ }
828
+ return runReviewerGateStage({ pipelineFlags: { reviewerEnabled: true }, reviewerRole: retryReviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion });
829
+ }
830
+ return { action: "retry" };
1237
831
  }
1238
832
  if (reviewerResult.stalled) return { action: "return", result: reviewerResult.stalledResult };
1239
833
  return { action: "ok", review: reviewerResult.review };
@@ -1243,7 +837,7 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
1243
837
  session.reviewer_retry_count = 0;
1244
838
  const postLoopResult = await handlePostLoopStages({
1245
839
  config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
1246
- becariaEnabled: Boolean(config.becaria?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger
840
+ ciEnabled: Boolean(config.ci?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger
1247
841
  });
1248
842
  if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
1249
843
  if (postLoopResult.action === "continue") return { action: "continue" };
@@ -1253,7 +847,7 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
1253
847
  }
1254
848
 
1255
849
  async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker }) {
1256
- // Escalate to Solomon / human before giving up
850
+ const budget = budgetSummary();
1257
851
  const solomonResult = await invokeSolomon({
1258
852
  config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
1259
853
  iteration: config.max_iterations,
@@ -1262,17 +856,26 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
1262
856
  task,
1263
857
  iterationCount: config.max_iterations,
1264
858
  maxIterations: config.max_iterations,
859
+ budget_usd: budget?.total_cost_usd || 0,
1265
860
  history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached without reviewer approval" }]
1266
861
  }
1267
862
  });
1268
863
 
864
+ // Solomon approved the work — treat as successful completion
865
+ if (solomonResult.action === "approve") {
866
+ logger.info("Solomon approved coder's work at max_iterations checkpoint");
867
+ return { approved: true, sessionId: session.id, reason: "solomon_approved" };
868
+ }
869
+
870
+ // Solomon says continue — extend iterations
1269
871
  if (solomonResult.action === "continue") {
1270
872
  if (solomonResult.humanGuidance) {
1271
- session.last_reviewer_feedback = `User guidance: ${solomonResult.humanGuidance}`;
873
+ session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
1272
874
  }
1273
875
  session.reviewer_retry_count = 0;
1274
876
  await saveSession(session);
1275
- return { approved: false, sessionId: session.id, reason: "max_iterations_extended", humanGuidance: solomonResult.humanGuidance };
877
+ const extraIterations = solomonResult.extraIterations || config.max_iterations;
878
+ return { approved: false, sessionId: session.id, reason: "max_iterations_extended", humanGuidance: solomonResult.humanGuidance, extraIterations };
1276
879
  }
1277
880
 
1278
881
  if (solomonResult.action === "pause") {
@@ -1283,7 +886,7 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
1283
886
  return { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "max_iterations_subtask" };
1284
887
  }
1285
888
 
1286
- // Solomon also couldn't resolve — fail
889
+ // Solomon couldn't resolve — fail
1287
890
  session.budget = budgetSummary();
1288
891
  const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
1289
892
  if (rtkSavings) session.rtk_savings = rtkSavings;
@@ -1320,6 +923,44 @@ async function tryAutoStartBoard(config, logger, emitter, eventBase) {
1320
923
  }
1321
924
 
1322
925
  async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
926
+ // Auto-init .karajan/ if missing (copies coder-rules, review-rules, role templates)
927
+ const initProjectDir = config.projectDir || process.cwd();
928
+ await autoInit(initProjectDir, logger);
929
+
930
+ // Smart role assignment: detect installed AIs and assign to roles
931
+ // Only runs if: (a) no roles configured AND (b) not in test environment
932
+ const needsAssignment = !config.roles?.coder?.provider && !config.coder && process.env.NODE_ENV !== "test" && !process.env.VITEST;
933
+ if (needsAssignment) {
934
+ try {
935
+ const { autoAssignRoles, applyRoleAssignments } = await import("./utils/role-assigner.js");
936
+ const { assignments } = await autoAssignRoles(logger);
937
+ if (assignments) config = applyRoleAssignments({ ...config }, assignments);
938
+ } catch { /* non-blocking — defaults will be used */ }
939
+ }
940
+
941
+ // Scope all git diffs to projectDir (prevents leaking unrelated branch changes)
942
+ // When running from a subdirectory of a git repo, use relative path as scope
943
+ let diffScope = config.projectDir || null;
944
+ if (!diffScope) {
945
+ try {
946
+ const { execSync } = await import("node:child_process");
947
+ const repoRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
948
+ const cwd = process.cwd();
949
+ if (cwd !== repoRoot && cwd.startsWith(repoRoot)) {
950
+ diffScope = cwd.slice(repoRoot.length + 1);
951
+ logger.info(`Running from subdirectory — diff scoped to ${diffScope}/`);
952
+ }
953
+ } catch { /* git not available */ }
954
+ }
955
+ setDiffProjectDir(diffScope);
956
+
957
+ // Auto-detect Chrome DevTools MCP
958
+ const { detectDevToolsMcp } = await import("./webperf/devtools-detect.js");
959
+ const devToolsAvailable = await detectDevToolsMcp(logger);
960
+ if (devToolsAvailable) {
961
+ config = { ...config, webperf: { ...config.webperf, devtools_mcp: true } };
962
+ }
963
+
1323
964
  const ctx = new PipelineContext({ config, session: null, logger, emitter, task, flags });
1324
965
  ctx.askQuestion = askQuestion;
1325
966
  ctx.pgTaskId = pgTaskId;
@@ -1340,23 +981,19 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1340
981
  ctx.trackBudget = trackBudget;
1341
982
 
1342
983
  // --- 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
- }
984
+ const rtkResult = await detectRtk();
985
+ if (rtkResult.available) {
986
+ config = { ...config, rtk: { available: true, version: rtkResult.version } };
987
+ const rtkTracker = new RtkSavingsTracker();
988
+ const rtkRunner = createRtkRunner(true, rtkTracker);
989
+ setDiffRunner(rtkRunner);
990
+ setGitRunner(rtkRunner);
991
+ ctx.rtkTracker = rtkTracker;
992
+ logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
993
+ emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
994
+ message: "RTK detected — internal commands wrapped for token optimization",
995
+ detail: { version: rtkResult.version, executorType: "local" }
996
+ }));
1360
997
  }
1361
998
 
1362
999
  // --- HU Board auto-start ---
@@ -1382,21 +1019,6 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1382
1019
  ctx.pgCard = pgAdapterResult.pgCard;
1383
1020
  ctx.session.pg_card = ctx.pgCard || null;
1384
1021
 
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
1022
  emitProgress(
1401
1023
  emitter,
1402
1024
  makeEvent("session:start", ctx.eventBase, {
@@ -1412,6 +1034,38 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1412
1034
  ctx.plannedTask = preLoopResult.plannedTask;
1413
1035
  ctx.config = preLoopResult.updatedConfig;
1414
1036
 
1037
+ // --- Session Journal: persist pre-loop outputs + display plan summary ---
1038
+ const reportDir = ctx.config.output?.report_dir || ".reviews";
1039
+ try {
1040
+ ctx.journalDir = await createJournalDir(reportDir, ctx.session.id);
1041
+ const journalFiles = await writePreLoopJournal(ctx.journalDir, ctx.stageResults);
1042
+ ctx.journalFiles = journalFiles;
1043
+ ctx.journalIterations = [];
1044
+ ctx.journalDecisions = [];
1045
+
1046
+ // Attach journal state to session so finalizeApprovedSession can access it
1047
+ ctx.session._journalDir = ctx.journalDir;
1048
+ ctx.session._journalFiles = journalFiles;
1049
+ ctx.session._journalIterations = ctx.journalIterations;
1050
+ ctx.session._journalDecisions = ctx.journalDecisions;
1051
+ ctx.session._startedAt = ctx.startedAt;
1052
+
1053
+ // Display plan summary in console before iteration loop
1054
+ const planSummary = buildPlanSummary({
1055
+ pipelineFlags: ctx.pipelineFlags,
1056
+ config: ctx.config,
1057
+ stageResults: ctx.stageResults,
1058
+ task
1059
+ });
1060
+ console.log(planSummary);
1061
+ } catch (err) {
1062
+ logger.warn(`Journal init failed (non-blocking): ${err.message}`);
1063
+ ctx.journalDir = null;
1064
+ ctx.journalFiles = [];
1065
+ ctx.journalIterations = [];
1066
+ ctx.journalDecisions = [];
1067
+ }
1068
+
1415
1069
  ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session });
1416
1070
  const projectDir = ctx.config.projectDir || process.cwd();
1417
1071
  ctx.reviewRules = (await resolveReviewProfile({ mode: ctx.config.review_mode, projectDir })).rules;
@@ -1424,7 +1078,7 @@ async function runSingleIteration(ctx) {
1424
1078
  const { config, logger, emitter, eventBase, session, task, iteration: i } = ctx;
1425
1079
 
1426
1080
  const iterStart = Date.now();
1427
- const becariaEnabled = Boolean(config.becaria?.enabled) && ctx.gitCtx?.enabled;
1081
+ const ciEnabled = Boolean(config.ci?.enabled) && ctx.gitCtx?.enabled;
1428
1082
  logger.setContext({ iteration: i, stage: "iteration" });
1429
1083
 
1430
1084
  const reviewerRetryCount = session.reviewer_retry_count || 0;
@@ -1455,8 +1109,8 @@ async function runSingleIteration(ctx) {
1455
1109
  });
1456
1110
  if (qgResult.action === "return" || qgResult.action === "continue") return qgResult;
1457
1111
 
1458
- await handleBecariaEarlyPrOrPush({
1459
- becariaEnabled, config, session, emitter, eventBase, gitCtx: ctx.gitCtx, task, logger,
1112
+ await handleCiEarlyPrOrPush({
1113
+ ciEnabled, config, session, emitter, eventBase, gitCtx: ctx.gitCtx, task, logger,
1460
1114
  stageResults: ctx.stageResults, i
1461
1115
  });
1462
1116
 
@@ -1474,13 +1128,26 @@ async function runSingleIteration(ctx) {
1474
1128
  }));
1475
1129
  session.standby_retry_count = 0;
1476
1130
 
1131
+ // --- Journal: record iteration ---
1132
+ if (ctx.journalIterations) {
1133
+ ctx.journalIterations.push(formatIteration({
1134
+ iteration: i,
1135
+ coderSummary: ctx.stageResults.coder?.summary || null,
1136
+ reviewerSummary: review?.approved ? `Approved: ${review.raw_summary || ""}` : `Rejected: ${(review?.blocking_issues || []).length} blocking issue(s)`,
1137
+ sonarSummary: ctx.stageResults.sonar?.summary || null,
1138
+ testerSummary: ctx.stageResults.tester?.summary || null,
1139
+ securitySummary: ctx.stageResults.security?.summary || null,
1140
+ durationMs: iterDuration
1141
+ }));
1142
+ }
1143
+
1477
1144
  const solomonResult = await handleSolomonCheck({
1478
1145
  config, session, emitter, eventBase, logger, task, i, askQuestion: ctx.askQuestion,
1479
- becariaEnabled, blockingIssues: review?.blocking_issues
1146
+ ciEnabled, blockingIssues: review?.blocking_issues
1480
1147
  });
1481
1148
  if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
1482
1149
 
1483
- await handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger });
1150
+ await handleCiReviewDispatch({ ciEnabled, config, session, review, i, logger });
1484
1151
 
1485
1152
  if (review.approved) {
1486
1153
  const approvedResult = await handleApprovedReview({
@@ -1500,8 +1167,13 @@ async function runSingleIteration(ctx) {
1500
1167
  } else {
1501
1168
  // Solomon is enabled — feed back the blocking issues for the next coder iteration
1502
1169
  session.last_reviewer_feedback = review.blocking_issues
1503
- .map((x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`)
1504
- .join("\n");
1170
+ .map((x) => {
1171
+ const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
1172
+ if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
1173
+ if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
1174
+ return parts.join("\n");
1175
+ })
1176
+ .join("\n\n");
1505
1177
  await saveSession(session);
1506
1178
  }
1507
1179
 
@@ -1559,14 +1231,92 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
1559
1231
  ctx.eventBase.iteration = i;
1560
1232
  ctx.iteration = i;
1561
1233
 
1562
- const iterResult = await runSingleIteration(ctx);
1234
+ let iterResult;
1235
+ try {
1236
+ iterResult = await runSingleIteration(ctx);
1237
+ } catch (stageError) {
1238
+ // ANY unhandled error in a stage = out of normal flow → Solomon decides
1239
+ logger.warn(`Stage error caught — escalating to Solomon: ${stageError.message}`);
1240
+ const solomonResult = await invokeSolomon({
1241
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
1242
+ stage: "stage_error", askQuestion, session: ctx.session, iteration: i,
1243
+ conflict: {
1244
+ stage: "stage_error",
1245
+ task: loopTask,
1246
+ iterationCount: i,
1247
+ maxIterations: ctx.config.max_iterations,
1248
+ budget_usd: ctx.budgetSummary()?.total_cost_usd || 0,
1249
+ history: [{ agent: "pipeline", feedback: `Stage threw: ${stageError.message}` }]
1250
+ }
1251
+ });
1252
+
1253
+ if (solomonResult.action === "approve") {
1254
+ logger.info("Solomon approved despite stage error");
1255
+ return { approved: true, sessionId: ctx.session.id, reason: "solomon_approved_after_error" };
1256
+ }
1257
+ if (solomonResult.action === "continue") {
1258
+ if (solomonResult.humanGuidance) {
1259
+ ctx.session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
1260
+ }
1261
+ continue; // next iteration
1262
+ }
1263
+ if (solomonResult.action === "pause") {
1264
+ return { paused: true, sessionId: ctx.session.id, question: solomonResult.question, context: "stage_error" };
1265
+ }
1266
+ // Solomon couldn't resolve — fail
1267
+ await markSessionStatus(ctx.session, "failed");
1268
+ return { approved: false, sessionId: ctx.session.id, reason: "stage_error", error: stageError.message };
1269
+ }
1270
+
1563
1271
  if (iterResult.action === "return") {
1564
1272
  return iterResult.result;
1565
1273
  }
1566
1274
  if (iterResult.action === "retry") { i -= 1; }
1567
1275
  }
1568
1276
 
1277
+ // Solomon decides whether to extend iterations or stop
1569
1278
  const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker });
1279
+
1280
+ // Solomon said "continue" — extend iterations and keep going
1281
+ if (maxIterResult.reason === "max_iterations_extended") {
1282
+ const extra = maxIterResult.extraIterations || ctx.config.max_iterations;
1283
+ ctx.config.max_iterations += extra;
1284
+ logger.info(`Solomon extended pipeline by ${extra} iterations (new max: ${ctx.config.max_iterations})`);
1285
+
1286
+ if (maxIterResult.humanGuidance) {
1287
+ ctx.session.last_reviewer_feedback = `Solomon guidance: ${maxIterResult.humanGuidance}`;
1288
+ }
1289
+
1290
+ // Continue the loop
1291
+ while (i < ctx.config.max_iterations) {
1292
+ i += 1;
1293
+ const elapsedMinutes = (Date.now() - ctx.startedAt) / 60000;
1294
+
1295
+ const cpResult = await handleCheckpoint({
1296
+ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
1297
+ i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary, lastCheckpointSnapshot
1298
+ });
1299
+ if (cpResult.action === "stop") return cpResult.result;
1300
+ checkpointDisabled = cpResult.checkpointDisabled;
1301
+ lastCheckpointAt = cpResult.lastCheckpointAt;
1302
+ if (cpResult.lastCheckpointSnapshot !== undefined) lastCheckpointSnapshot = cpResult.lastCheckpointSnapshot;
1303
+
1304
+ await checkSessionTimeout({ askQuestion, elapsedMinutes, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
1305
+ await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
1306
+
1307
+ ctx.eventBase.iteration = i;
1308
+ ctx.iteration = i;
1309
+
1310
+ const iterResult = await runSingleIteration(ctx);
1311
+ if (iterResult.action === "return") return iterResult.result;
1312
+ if (iterResult.action === "retry") { i -= 1; }
1313
+ }
1314
+
1315
+ // Extended iterations also exhausted — final Solomon call
1316
+ const finalResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker });
1317
+ return finalResult;
1318
+ }
1319
+
1570
1320
  return maxIterResult;
1571
1321
  }
1572
1322
 
@@ -1577,7 +1327,29 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1577
1327
  return handleDryRun({ task, config, flags, emitter, pipelineFlags });
1578
1328
  }
1579
1329
 
1580
- const ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
1330
+ let ctx;
1331
+ try {
1332
+ ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
1333
+ } catch (initError) {
1334
+ // Pre-loop stage failure → Solomon decides
1335
+ logger.warn(`Init/pre-loop error — escalating to Solomon: ${initError.message}`);
1336
+ const tempSession = { id: "init-error", task, status: "failed" };
1337
+ const solomonResult = await invokeSolomon({
1338
+ config, logger, emitter, eventBase: { sessionId: "init-error", iteration: 0, stage: "init", startedAt: Date.now() },
1339
+ stage: "init_error", askQuestion, session: tempSession, iteration: 0,
1340
+ conflict: {
1341
+ stage: "init_error",
1342
+ task,
1343
+ iterationCount: 0,
1344
+ maxIterations: config.max_iterations || 5,
1345
+ history: [{ agent: "pipeline", feedback: `Initialization failed: ${initError.message}` }]
1346
+ }
1347
+ });
1348
+ if (solomonResult.action === "pause") {
1349
+ return { paused: true, sessionId: "init-error", question: solomonResult.question, context: "init_error" };
1350
+ }
1351
+ throw initError; // Solomon couldn't resolve — propagate
1352
+ }
1581
1353
 
1582
1354
  try {
1583
1355
  // --- Analysis-only flow: skip coder/reviewer when coderRequired === false ---
@@ -1667,14 +1439,6 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1667
1439
  await writeHistoryRecord({ sessionId: ctx.session.id, task, result, logger });
1668
1440
  return result;
1669
1441
  } 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
1442
  // --- Cleanup auto-installed skills ---
1679
1443
  const autoSkills = ctx.session?.autoInstalledSkills;
1680
1444
  if (autoSkills && autoSkills.length > 0) {