karajan-code 1.59.0 → 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 (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 +34 -6
  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 +3 -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/domains/domain-loader.js +85 -4
  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 +2 -2
  26. package/src/mcp/tools.js +3 -2
  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/post-loop-stages.js +20 -15
  35. package/src/orchestrator/preflight-checks.js +7 -3
  36. package/src/orchestrator/role-output-compressor.js +190 -0
  37. package/src/orchestrator/session-journal.js +344 -0
  38. package/src/orchestrator/solomon-escalation.js +38 -5
  39. package/src/orchestrator/solomon-rules.js +32 -33
  40. package/src/orchestrator/stages/reviewer-stage.js +16 -5
  41. package/src/orchestrator/stages/sonar-stage.js +24 -1
  42. package/src/orchestrator/verification-gate.js +146 -0
  43. package/src/orchestrator.js +435 -661
  44. package/src/planning-game/client.js +2 -2
  45. package/src/planning-game/pipeline-adapter.js +1 -1
  46. package/src/prompts/coder.js +17 -3
  47. package/src/prompts/reviewer.js +2 -2
  48. package/src/prompts/rtk-snippet.js +3 -5
  49. package/src/review/diff-generator.js +66 -17
  50. package/src/review/snapshot-diff.js +122 -0
  51. package/src/review/tdd-policy.js +4 -2
  52. package/src/roles/agent-role.js +135 -0
  53. package/src/roles/architect-role.js +61 -108
  54. package/src/roles/audit-role.js +25 -71
  55. package/src/roles/coder-role.js +32 -57
  56. package/src/roles/discover-role.js +30 -71
  57. package/src/roles/hu-reviewer-role.js +51 -99
  58. package/src/roles/impeccable-role.js +34 -91
  59. package/src/roles/index.js +2 -0
  60. package/src/roles/karajan-brain-role.js +134 -0
  61. package/src/roles/planner-role.js +40 -75
  62. package/src/roles/refactorer-role.js +22 -64
  63. package/src/roles/researcher-role.js +52 -118
  64. package/src/roles/reviewer-role.js +69 -126
  65. package/src/roles/security-role.js +38 -102
  66. package/src/roles/solomon-role.js +71 -15
  67. package/src/roles/tester-role.js +121 -95
  68. package/src/roles/triage-role.js +50 -111
  69. package/src/session-store.js +1 -1
  70. package/src/skills/skill-detector.js +11 -0
  71. package/src/sonar/config-resolver.js +20 -1
  72. package/src/sonar/credentials.js +17 -0
  73. package/src/sonar/project-key.js +1 -1
  74. package/src/sonar/scanner.js +1 -1
  75. package/src/utils/banner.js +66 -0
  76. package/src/utils/display.js +35 -17
  77. package/src/utils/injection-guard.js +23 -8
  78. package/src/utils/logger.js +6 -1
  79. package/src/utils/process.js +3 -4
  80. package/src/utils/role-assigner.js +131 -0
  81. package/src/utils/update-check.js +5 -1
  82. package/src/utils/welcome.js +3 -8
  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 -198
  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,31 +7,25 @@ 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";
28
+ import { persistInlineDomain } from "./domains/domain-loader.js";
38
29
  import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
39
30
  import { runTesterStage, runSecurityStage, runImpeccableStage, runFinalAuditStage } from "./orchestrator/post-loop-stages.js";
40
31
  import { needsSubPipeline, runHuSubPipeline } from "./orchestrator/hu-sub-pipeline.js";
@@ -43,267 +34,43 @@ import { detectTestFramework } from "./utils/project-detect.js";
43
34
  import { runPreflightChecks } from "./orchestrator/preflight-checks.js";
44
35
  import { detectRtk } from "./utils/rtk-detect.js";
45
36
  import { createRtkRunner, RtkSavingsTracker } from "./utils/rtk-wrapper.js";
46
- import { setRunner as setDiffRunner } from "./review/diff-generator.js";
37
+ import { setRunner as setDiffRunner, setProjectDir as setDiffProjectDir } from "./review/diff-generator.js";
47
38
  import { setRunner as setGitRunner } from "./utils/git.js";
48
39
  import { detectNeededSkills, autoInstallSkills, cleanupAutoInstalledSkills } from "./skills/skill-detector.js";
49
40
  import { isOpenSkillsAvailable } from "./skills/openskills-client.js";
50
- import { startProxy, stopProxy } from "./proxy/proxy-lifecycle.js";
51
-
52
-
53
- // --- Product Context loader ---
54
-
55
- /**
56
- * Load product context from well-known file locations.
57
- * Returns the file content or null if no file is found.
58
- * @param {string|null} projectDir
59
- * @returns {Promise<{content: string|null, source: string|null}>}
60
- */
61
- export async function loadProductContext(projectDir) {
62
- const base = projectDir || process.cwd();
63
- const candidates = [
64
- path.join(base, ".karajan", "context.md"),
65
- path.join(base, "product-vision.md")
66
- ];
67
- for (const file of candidates) {
68
- try {
69
- const content = await fs.readFile(file, "utf8");
70
- return { content, source: file };
71
- } catch { /* not found, try next */ }
72
- }
73
- return { content: null, source: null };
74
- }
75
-
76
- // --- Extracted helper functions (pure refactoring, zero behavior change) ---
77
-
78
- function resolvePipelineFlags(config) {
79
- return {
80
- plannerEnabled: Boolean(config.pipeline?.planner?.enabled),
81
- refactorerEnabled: Boolean(config.pipeline?.refactorer?.enabled),
82
- researcherEnabled: Boolean(config.pipeline?.researcher?.enabled),
83
- testerEnabled: Boolean(config.pipeline?.tester?.enabled),
84
- securityEnabled: Boolean(config.pipeline?.security?.enabled),
85
- impeccableEnabled: Boolean(config.pipeline?.impeccable?.enabled),
86
- reviewerEnabled: config.pipeline?.reviewer?.enabled !== false,
87
- discoverEnabled: Boolean(config.pipeline?.discover?.enabled),
88
- architectEnabled: Boolean(config.pipeline?.architect?.enabled),
89
- huReviewerEnabled: Boolean(config.pipeline?.hu_reviewer?.enabled),
90
- };
91
- }
92
-
93
- async function handleDryRun({ task, config, flags, emitter, pipelineFlags }) {
94
- const { plannerEnabled, refactorerEnabled, researcherEnabled, testerEnabled, securityEnabled, impeccableEnabled, reviewerEnabled, discoverEnabled, architectEnabled, huReviewerEnabled } = pipelineFlags;
95
- const plannerRole = resolveRole(config, "planner");
96
- const coderRole = resolveRole(config, "coder");
97
- const reviewerRole = resolveRole(config, "reviewer");
98
- const refactorerRole = resolveRole(config, "refactorer");
99
- const triageEnabled = true;
100
-
101
- const dryRunPolicies = applyPolicies({
102
- taskType: flags.taskType || config.taskType || null,
103
- policies: config.policies,
104
- });
105
- const projectDir = config.projectDir || process.cwd();
106
- const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
107
- const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
108
- 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 });
109
- 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 });
110
-
111
- const summary = {
112
- dry_run: true,
113
- task,
114
- policies: dryRunPolicies,
115
- roles: { planner: plannerRole, coder: coderRole, reviewer: reviewerRole, refactorer: refactorerRole },
116
- pipeline: {
117
- discover_enabled: discoverEnabled,
118
- architect_enabled: architectEnabled,
119
- triage_enabled: triageEnabled,
120
- planner_enabled: plannerEnabled,
121
- refactorer_enabled: refactorerEnabled,
122
- sonar_enabled: Boolean(config.sonarqube?.enabled),
123
- reviewer_enabled: reviewerEnabled,
124
- researcher_enabled: researcherEnabled,
125
- tester_enabled: testerEnabled,
126
- security_enabled: securityEnabled,
127
- impeccable_enabled: impeccableEnabled,
128
- solomon_enabled: Boolean(config.pipeline?.solomon?.enabled),
129
- hu_reviewer_enabled: huReviewerEnabled
130
- },
131
- limits: {
132
- max_iterations: config.max_iterations,
133
- max_iteration_minutes: config.session?.max_iteration_minutes,
134
- max_total_minutes: config.session?.max_total_minutes,
135
- max_sonar_retries: config.session?.max_sonar_retries,
136
- max_reviewer_retries: config.session?.max_reviewer_retries,
137
- max_tester_retries: config.session?.max_tester_retries,
138
- max_security_retries: config.session?.max_security_retries
139
- },
140
- prompts: { coder: coderPrompt, reviewer: reviewerPrompt },
141
- git: config.git
142
- };
143
-
144
- emitProgress(
145
- emitter,
146
- makeEvent("dry-run:summary", { sessionId: null, iteration: 0, stage: "dry-run", startedAt: Date.now() }, {
147
- message: "Dry-run complete — no changes made",
148
- detail: summary
149
- })
150
- );
151
-
152
- return summary;
153
- }
154
41
 
155
- function createBudgetManager({ config, emitter, eventBase }) {
156
- const budgetTracker = new BudgetTracker({ pricing: config?.budget?.pricing });
157
- const budgetLimit = Number(config?.max_budget_usd);
158
- const hasBudgetLimit = Number.isFinite(budgetLimit) && budgetLimit >= 0;
159
- const warnThresholdPct = Number(config?.budget?.warn_threshold_pct ?? 80);
160
- let stageCounter = 0;
161
-
162
- function budgetSummary() {
163
- const s = budgetTracker.summary();
164
- s.trace = budgetTracker.trace();
165
- return s;
166
- }
167
-
168
- function trackBudget({ role, provider, model, result, duration_ms, promptSize }) {
169
- // Attach promptSize to result if provided, so extractUsageMetrics can estimate tokens
170
- const enrichedResult = promptSize && result ? { ...result, promptSize } : result;
171
- const metrics = extractUsageMetrics(enrichedResult, model);
172
- budgetTracker.record({ role, provider, ...metrics, duration_ms, stage_index: stageCounter++ });
173
-
174
- if (!hasBudgetLimit) return;
175
- const totalCost = budgetTracker.total().cost_usd;
176
- const pctUsed = budgetLimit === 0 ? 100 : (totalCost / budgetLimit) * 100;
177
- const warnOrOk = pctUsed >= warnThresholdPct ? "paused" : "ok";
178
- const status = totalCost > budgetLimit ? "fail" : warnOrOk;
179
- emitProgress(
180
- emitter,
181
- makeEvent("budget:update", { ...eventBase, stage: role }, {
182
- status,
183
- message: `Budget: $${totalCost.toFixed(2)} / $${budgetLimit.toFixed(2)}`,
184
- detail: {
185
- ...budgetSummary(),
186
- max_budget_usd: budgetLimit,
187
- warn_threshold_pct: warnThresholdPct,
188
- pct_used: Number(pctUsed.toFixed(2)),
189
- remaining_usd: budgetTracker.remaining(budgetLimit),
190
- executorType: "system"
191
- }
192
- })
193
- );
194
- }
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";
195
65
 
196
- return { budgetTracker, budgetLimit, budgetSummary, trackBudget };
197
- }
66
+ // Re-export for external consumers
67
+ export const loadProductContext = _loadProductContext;
68
+ export const shouldAutoContinueCheckpoint = _shouldAutoContinueCheckpoint;
69
+ export const parseCheckpointAnswer = _parseCheckpointAnswer;
198
70
 
199
- async function initializeSession({ task, config, flags, pgTaskId, pgProject }) {
200
- const baseRef = await computeBaseRef({ baseBranch: config.base_branch, baseRef: flags.baseRef || null });
201
- const sessionInit = {
202
- task,
203
- config_snapshot: config,
204
- base_ref: baseRef,
205
- session_start_sha: baseRef,
206
- last_reviewer_feedback: null,
207
- repeated_issue_count: 0,
208
- sonar_retry_count: 0,
209
- reviewer_retry_count: 0,
210
- standby_retry_count: 0,
211
- last_sonar_issue_signature: null,
212
- sonar_repeat_count: 0,
213
- last_reviewer_issue_signature: null,
214
- reviewer_repeat_count: 0,
215
- deferred_issues: []
216
- };
217
- if (pgTaskId) sessionInit.pg_task_id = pgTaskId;
218
- if (pgProject) sessionInit.pg_project_id = pgProject;
219
- return createSession(sessionInit);
220
- }
221
71
 
222
72
  // PG card "In Progress" logic moved to src/planning-game/pipeline-adapter.js → initPgAdapter()
223
73
 
224
- function applyTriageOverrides(pipelineFlags, roleOverrides) {
225
- const keys = ["plannerEnabled", "researcherEnabled", "architectEnabled", "refactorerEnabled", "reviewerEnabled", "testerEnabled", "securityEnabled", "impeccableEnabled"];
226
- for (const key of keys) {
227
- if (roleOverrides[key] !== undefined) {
228
- pipelineFlags[key] = roleOverrides[key];
229
- }
230
- }
231
- }
232
-
233
- const SIMPLE_LEVELS = new Set(["trivial", "simple"]);
234
-
235
- function applyAutoSimplify({ pipelineFlags, triageLevel, config, flags, logger, emitter, eventBase }) {
236
- if (!config.pipeline?.auto_simplify) return false;
237
- if (!triageLevel || !SIMPLE_LEVELS.has(triageLevel)) return false;
238
- if (flags.mode) return false;
239
- if (flags.enableReviewer !== undefined || flags.enableTester !== undefined) return false;
240
-
241
- pipelineFlags.reviewerEnabled = false;
242
- pipelineFlags.testerEnabled = false;
243
-
244
- const disabledRoles = ["reviewer", "tester"];
245
- logger.info(`Simple task (${triageLevel}) — lightweight pipeline (disabled: ${disabledRoles.join(", ")})`);
246
- emitProgress(
247
- emitter,
248
- makeEvent("pipeline:simplify", { ...eventBase, stage: "triage" }, {
249
- message: `Simple task (${triageLevel}) — lightweight pipeline`,
250
- detail: { level: triageLevel, disabledRoles }
251
- })
252
- );
253
- return true;
254
- }
255
-
256
- // PG decomposition logic moved to src/planning-game/pipeline-adapter.js → handlePgDecomposition()
257
-
258
- function applyFlagOverrides(pipelineFlags, flags) {
259
- if (flags.enablePlanner !== undefined) pipelineFlags.plannerEnabled = Boolean(flags.enablePlanner);
260
- if (flags.enableResearcher !== undefined) pipelineFlags.researcherEnabled = Boolean(flags.enableResearcher);
261
- if (flags.enableArchitect !== undefined) pipelineFlags.architectEnabled = Boolean(flags.enableArchitect);
262
- if (flags.enableRefactorer !== undefined) pipelineFlags.refactorerEnabled = Boolean(flags.enableRefactorer);
263
- if (flags.enableReviewer !== undefined) pipelineFlags.reviewerEnabled = Boolean(flags.enableReviewer);
264
- if (flags.enableTester !== undefined) pipelineFlags.testerEnabled = Boolean(flags.enableTester);
265
- if (flags.enableSecurity !== undefined) pipelineFlags.securityEnabled = Boolean(flags.enableSecurity);
266
- if (flags.enableImpeccable !== undefined) pipelineFlags.impeccableEnabled = Boolean(flags.enableImpeccable);
267
-
268
- // --design flag: force-enable impeccable in refactoring mode
269
- if (flags.design) {
270
- pipelineFlags.impeccableEnabled = true;
271
- pipelineFlags.impeccableMode = "refactoring";
272
- }
273
- }
274
-
275
- function resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags }) {
276
- const resolvedPolicies = applyPolicies({
277
- taskType: flags.taskType || config.taskType || stageResults.triage?.taskType || stageResults.intent?.taskType || null,
278
- policies: config.policies,
279
- });
280
- session.resolved_policies = resolvedPolicies;
281
-
282
- let updatedConfig = config;
283
- if (!resolvedPolicies.tdd) {
284
- updatedConfig = { ...updatedConfig, development: { ...updatedConfig.development, methodology: "standard", require_test_changes: false } };
285
- }
286
- if (!resolvedPolicies.sonar) {
287
- updatedConfig = { ...updatedConfig, sonarqube: { ...updatedConfig.sonarqube, enabled: false } };
288
- }
289
- if (!resolvedPolicies.reviewer) {
290
- pipelineFlags.reviewerEnabled = false;
291
- }
292
- if (resolvedPolicies.coderRequired === false) {
293
- pipelineFlags.coderRequired = false;
294
- }
295
-
296
- emitProgress(
297
- emitter,
298
- makeEvent("policies:resolved", eventBase, {
299
- message: `Policies resolved for taskType="${resolvedPolicies.taskType}"`,
300
- detail: resolvedPolicies
301
- })
302
- );
303
-
304
- return updatedConfig;
305
- }
306
-
307
74
  async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion }) {
308
75
  let researchContext = null;
309
76
  let plannedTask = task;
@@ -335,7 +102,7 @@ async function runPlanningPhases({ config, logger, emitter, eventBase, session,
335
102
  plannedTask = plannerResult.plannedTask;
336
103
  stageResults.planner = plannerResult.stageResult;
337
104
 
338
- await tryBecariaComment({
105
+ await tryCiComment({
339
106
  config, session, logger,
340
107
  agent: "Planner",
341
108
  body: `Plan: ${plannerResult.stageResult?.summary || plannedTask}`
@@ -345,277 +112,92 @@ async function runPlanningPhases({ config, logger, emitter, eventBase, session,
345
112
  return { plannedTask };
346
113
  }
347
114
 
348
- async function tryBecariaComment({ config, session, logger, agent, body }) {
349
- if (!config.becaria?.enabled || !session.becaria_pr_number) return;
350
- try {
351
- const { dispatchComment } = await import("./becaria/dispatch.js");
352
- const { detectRepo } = await import("./becaria/repo.js");
353
- const repo = await detectRepo();
354
- if (repo) {
355
- await dispatchComment({
356
- repo, prNumber: session.becaria_pr_number, agent,
357
- body, becariaConfig: config.becaria
358
- });
359
- }
360
- } catch { /* non-blocking */ }
361
- }
362
-
363
- function detectCheckpointProgress(session, lastCheckpointSnapshot) {
364
- if (!lastCheckpointSnapshot) return true; // First checkpoint — assume progress
365
- const currentIteration = session.reviewer_retry_count ?? 0;
366
- const currentStages = Object.keys(session.resolved_policies || {}).length;
367
- const currentCheckpoints = (session.checkpoints || []).length;
368
-
369
- const iterationAdvanced = currentIteration !== lastCheckpointSnapshot.iteration;
370
- const stagesChanged = currentStages !== lastCheckpointSnapshot.stagesCount;
371
- const checkpointsChanged = currentCheckpoints !== lastCheckpointSnapshot.checkpointsCount;
372
-
373
- return iterationAdvanced || stagesChanged || checkpointsChanged;
374
- }
375
-
376
- function takeCheckpointSnapshot(session) {
377
- return {
378
- iteration: session.reviewer_retry_count ?? 0,
379
- stagesCount: Object.keys(session.resolved_policies || {}).length,
380
- checkpointsCount: (session.checkpoints || []).length
381
- };
382
- }
383
115
 
384
- /**
385
- * Determine if checkpoint should auto-continue without asking the user.
386
- * Exported for testing.
387
- */
388
- export function shouldAutoContinueCheckpoint(session, hasProgress) {
389
- if (hasProgress) {
390
- session._checkpoint_stall_count = 0;
391
- return { autoContinue: true, reason: "progress_detected" };
392
- }
393
- const wasRateLimited = (session.standby_retry_count || 0) > 0;
394
- const consecutiveStalls = (session._checkpoint_stall_count || 0) + 1;
395
- session._checkpoint_stall_count = consecutiveStalls;
396
- if (wasRateLimited && consecutiveStalls < 3) {
397
- return { autoContinue: true, reason: "recoverable_stall" };
398
- }
399
- return { autoContinue: false, reason: consecutiveStalls >= 3 ? "max_stalls_reached" : "no_progress" };
400
- }
401
116
 
402
- async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes, i, config, budgetTracker, stageResults, emitter, eventBase, session, budgetSummary, lastCheckpointSnapshot }) {
403
- if (checkpointDisabled || !askQuestion || (Date.now() - lastCheckpointAt) < checkpointIntervalMs) {
404
- return { action: "continue_loop", checkpointDisabled, lastCheckpointAt, lastCheckpointSnapshot };
405
- }
406
-
407
- const elapsedStr = elapsedMinutes.toFixed(1);
408
- const stagesCompleted = Object.keys(stageResults).join(", ") || "none";
409
-
410
- // Decide whether to auto-continue or ask the user
411
- const hasProgress = detectCheckpointProgress(session, lastCheckpointSnapshot);
412
- const newSnapshot = takeCheckpointSnapshot(session);
413
- const decision = shouldAutoContinueCheckpoint(session, hasProgress);
414
-
415
- if (decision.autoContinue) {
416
- emitProgress(
417
- emitter,
418
- makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
419
- message: `Checkpoint: ${decision.reason === "progress_detected" ? "progress detected" : "stall caused by rate limit/cooldown"}, auto-continuing (${elapsedStr} min elapsed)`,
420
- detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: true, reason: decision.reason }
421
- })
422
- );
423
- 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 };
424
120
  }
425
121
 
426
- // No progress and not recoverable — ask human
427
- const iterInfo = `${i - 1}/${config.max_iterations} iterations completed`;
428
- const budgetInfo = budgetTracker.total().cost_usd > 0 ? ` | Budget: $${budgetTracker.total().cost_usd.toFixed(2)}` : "";
429
- const checkpointMsg = `Checkpoint — ${elapsedStr} min elapsed | ${iterInfo}${budgetInfo} | Stages completed: ${stagesCompleted}. No progress since last checkpoint. What would you like to do?`;
430
-
431
- emitProgress(
432
- emitter,
433
- makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
434
- message: `Interactive checkpoint at ${elapsedStr} min (stalled)`,
435
- detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: false }
436
- })
437
- );
438
-
439
- const lang = getLang(config);
440
- const answer = await askQuestion(
441
- `${checkpointMsg}\n\n${msg("checkpoint_options", lang)}`
442
- );
443
-
444
- await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
445
-
446
- const trimmedAnswer = (answer || "").trim();
447
- const isExplicitStop = trimmedAnswer === "4" || trimmedAnswer.toLowerCase().startsWith("stop");
448
-
449
- if (isExplicitStop) {
450
- await markSessionStatus(session, "stopped");
451
- emitProgress(
452
- emitter,
453
- makeEvent("session:end", { ...eventBase, iteration: i, stage: "user-stop" }, {
454
- status: "stopped",
455
- message: "Session stopped by user at checkpoint",
456
- detail: { approved: false, reason: "user_stopped", elapsed_minutes: Number(elapsedStr), budget: budgetSummary() }
457
- })
458
- );
459
- return { action: "stop", result: { approved: false, sessionId: session.id, reason: "user_stopped", elapsed_minutes: Number(elapsedStr) } };
460
- }
122
+ const isOutage = stageResult.standbyInfo.isProviderOutage;
123
+ const agent = stageResult.standbyInfo.agent;
124
+ const cooldownUntil = stageResult.standbyInfo.cooldownUntil;
125
+ const cooldownMs = stageResult.standbyInfo.cooldownMs;
461
126
 
462
- const parsed = parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config });
463
- parsed.lastCheckpointSnapshot = newSnapshot;
464
- return parsed;
465
- }
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
+ });
466
144
 
467
- export function parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config }) {
468
- if (!trimmedAnswer) {
469
- return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
470
- }
471
- if (trimmedAnswer === "2" || trimmedAnswer.toLowerCase().startsWith("continue until")) {
472
- return { action: "continue_loop", checkpointDisabled: true, lastCheckpointAt: Date.now() };
473
- }
474
- if (trimmedAnswer === "1" || trimmedAnswer.toLowerCase().includes("5 m")) {
475
- return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
476
- }
477
- const customMinutes = Number.parseInt(trimmedAnswer.replaceAll(/\D/g, ""), 10);
478
- if (customMinutes > 0) {
479
- config.session.checkpoint_interval_minutes = customMinutes;
480
- 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" };
481
154
  }
482
- return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
483
- }
484
-
485
- async function checkSessionTimeout({ askQuestion, elapsedMinutes, config, session, emitter, eventBase, i, budgetSummary }) {
486
- if (askQuestion || elapsedMinutes <= config.session.max_total_minutes) return;
487
155
 
488
- await markSessionStatus(session, "failed");
489
- emitProgress(
490
- emitter,
491
- makeEvent("session:end", { ...eventBase, iteration: i, stage: "timeout" }, {
492
- status: "fail",
493
- message: "Session timed out",
494
- detail: { approved: false, reason: "timeout", budget: budgetSummary() }
495
- })
496
- );
497
- throw new Error("Session timed out");
498
- }
499
-
500
- async function checkBudgetExceeded({ budgetTracker, config, session, emitter, eventBase, i, budgetLimit, budgetSummary }) {
501
- 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
+ }
502
168
 
503
- await markSessionStatus(session, "failed");
504
- const totalCost = budgetTracker.total().cost_usd;
505
- const message = `Budget exceeded: $${totalCost.toFixed(2)} > $${budgetLimit.toFixed(2)}`;
506
- emitProgress(
507
- emitter,
508
- makeEvent("session:end", { ...eventBase, iteration: i, stage: "budget" }, {
509
- status: "fail",
510
- message,
511
- detail: { approved: false, reason: "budget_exceeded", budget: budgetSummary(), max_budget_usd: budgetLimit }
512
- })
513
- );
514
- throw new Error(message);
515
- }
169
+ if (altAgent) {
170
+ logger.info(`Solomon: retry ${stage} with alternative agent "${altAgent}"`);
171
+ session._alternative_agent = { stage, provider: altAgent };
172
+ }
516
173
 
517
- async function handleStandbyResult({ stageResult, session, emitter, eventBase, i, stage, logger }) {
518
- if (stageResult?.action !== "standby") {
519
- 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" };
520
179
  }
521
180
 
522
- const standbyRetries = session.standby_retry_count || 0;
523
- const isOutage = stageResult.standbyInfo.isProviderOutage;
524
- const pauseReason = isOutage
525
- ? `Provider outage (${stageResult.standbyInfo.message || "5xx/connection error"}) — retried ${standbyRetries} times. This is NOT a KJ or code problem.`
526
- : `Rate limit standby exhausted after ${standbyRetries} retries. Agent: ${stageResult.standbyInfo.agent}`;
527
-
528
- if (standbyRetries >= MAX_STANDBY_RETRIES) {
529
- session.last_reviewer_feedback = isOutage
530
- ? "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."
531
- : session.last_reviewer_feedback;
532
- await pauseSession(session, {
533
- question: pauseReason,
534
- context: { iteration: i, stage, reason: isOutage ? "provider_outage" : "standby_exhausted" }
535
- });
536
- emitProgress(emitter, makeEvent(`${stage}:rate_limit`, { ...eventBase, stage }, {
537
- status: "paused",
538
- message: `Standby exhausted after ${standbyRetries} retries`,
539
- detail: { agent: stageResult.standbyInfo.agent, sessionId: session.id }
540
- }));
181
+ if (solomonResult.action === "pause") {
541
182
  return {
542
183
  handled: true,
543
184
  action: "return",
544
- 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" }
545
186
  };
546
187
  }
547
- session.standby_retry_count = standbyRetries + 1;
548
- await saveSession(session);
549
- await waitForCooldown({ ...stageResult.standbyInfo, retryCount: standbyRetries, emitter, eventBase, logger, session });
550
- return { handled: true, action: "retry" };
551
- }
552
188
 
553
- function formatCommitList(commits) {
554
- return commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
555
- }
556
-
557
- async function becariaIncrementalPush({ config, session, gitCtx, task, logger, repo, dispatchComment }) {
558
- const pushResult = await incrementalPush({ gitCtx, task, logger, session });
559
- if (!pushResult) return;
560
-
561
- // Accumulate commits for PG card lifecycle tracking
562
- const { accumulateCommit } = await import("./planning-game/pipeline-adapter.js");
563
- for (const c of pushResult.commits) accumulateCommit(session, c);
564
-
565
- session.becaria_commits = [...(session.becaria_commits ?? []), ...pushResult.commits];
566
- await saveSession(session);
567
-
568
- if (!repo) return;
569
- const feedback = session.last_reviewer_feedback || "N/A";
570
- await dispatchComment({
571
- repo, prNumber: session.becaria_pr_number, agent: "Coder",
572
- body: `Issues corregidos:\n${feedback}\n\nCommits:\n${formatCommitList(pushResult.commits)}`,
573
- becariaConfig: config.becaria
574
- });
575
- }
576
-
577
- async function becariaCreateEarlyPr({ config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i, repo, dispatchComment }) {
578
- const earlyPr = await earlyPrCreation({ gitCtx, task, logger, session, stageResults });
579
- if (!earlyPr) return;
580
-
581
- // Accumulate commits for PG card lifecycle tracking
582
- const { accumulateCommit } = await import("./planning-game/pipeline-adapter.js");
583
- for (const c of earlyPr.commits) accumulateCommit(session, c);
584
-
585
- session.becaria_pr_number = earlyPr.prNumber;
586
- session.becaria_pr_url = earlyPr.prUrl;
587
- session.becaria_commits = earlyPr.commits;
588
- await saveSession(session);
589
- emitProgress(emitter, makeEvent("becaria:pr-created", { ...eventBase, stage: "becaria" }, {
590
- message: `Early PR created: #${earlyPr.prNumber}`,
591
- detail: { prNumber: earlyPr.prNumber, prUrl: earlyPr.prUrl }
592
- }));
593
-
594
- if (!repo) return;
595
- await dispatchComment({
596
- repo, prNumber: earlyPr.prNumber, agent: "Coder",
597
- body: `Iteración ${i} completada.\n\nCommits:\n${formatCommitList(earlyPr.commits)}`,
598
- 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" }
599
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
+ };
600
199
  }
601
200
 
602
- async function handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i }) {
603
- if (!becariaEnabled) return;
604
-
605
- try {
606
- const { dispatchComment } = await import("./becaria/dispatch.js");
607
- const { detectRepo } = await import("./becaria/repo.js");
608
- const repo = await detectRepo();
609
-
610
- if (session.becaria_pr_number) {
611
- await becariaIncrementalPush({ config, session, gitCtx, task, logger, repo, dispatchComment });
612
- } else {
613
- await becariaCreateEarlyPr({ config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i, repo, dispatchComment });
614
- }
615
- } catch (err) {
616
- logger.warn(`BecarIA early PR/push failed (non-blocking): ${err.message}`);
617
- }
618
- }
619
201
 
620
202
  function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
621
203
  for (const alert of alerts) {
@@ -628,7 +210,7 @@ function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
628
210
  }
629
211
  }
630
212
 
631
- 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 }) {
632
214
  if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
633
215
 
634
216
  try {
@@ -642,15 +224,15 @@ async function handleSolomonCheck({ config, session, emitter, eventBase, logger,
642
224
  if (pauseResult) return pauseResult;
643
225
  }
644
226
 
645
- if (becariaEnabled && session.becaria_pr_number) {
227
+ if (ciEnabled && session.ci_pr_number) {
646
228
  const alerts = rulesResult.alerts || [];
647
229
  const alertMsg = alerts.length > 0
648
230
  ? alerts.map(a => `- [${a.severity}] ${a.message}`).join("\n")
649
231
  : "No anomalies detected";
650
- await tryBecariaComment({
232
+ await tryCiComment({
651
233
  config, session, logger,
652
234
  agent: "Solomon",
653
- body: `Supervisor check iteración ${i}: ${alertMsg}`
235
+ body: `Supervisor check iteration ${i}: ${alertMsg}`
654
236
  });
655
237
  }
656
238
  } catch (err) {
@@ -693,59 +275,8 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
693
275
  return null;
694
276
  }
695
277
 
696
- function formatBlockingIssues(issues) {
697
- return issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
698
- }
699
-
700
- function formatSuggestions(suggestions) {
701
- return suggestions?.map((s) => {
702
- const detail = typeof s === "string" ? s : `${s.id || ""} ${s.description || s}`;
703
- return `- ${detail}`;
704
- }).join("\n") || "";
705
- }
706
-
707
- function buildReviewCommentBody(review, i) {
708
- const status = review.approved ? "APPROVED" : "REQUEST_CHANGES";
709
- const blocking = formatBlockingIssues(review.blocking_issues);
710
- const suggestions = formatSuggestions(review.non_blocking_suggestions);
711
- let body = `Review iteración ${i}: ${status}`;
712
- if (blocking) body += `\n\n**Blocking:**\n${blocking}`;
713
- if (suggestions) body += `\n\n**Suggestions:**\n${suggestions}`;
714
- return body;
715
- }
716
-
717
- async function handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger }) {
718
- if (!becariaEnabled || !session.becaria_pr_number) return;
719
-
720
- try {
721
- const { dispatchReview, dispatchComment } = await import("./becaria/dispatch.js");
722
- const { detectRepo } = await import("./becaria/repo.js");
723
- const repo = await detectRepo();
724
- if (!repo) return;
725
-
726
- const bc = config.becaria;
727
- const reviewEvent = review.approved ? "APPROVE" : "REQUEST_CHANGES";
728
- const reviewBody = review.approved
729
- ? (review.summary || "Approved")
730
- : (formatBlockingIssues(review.blocking_issues) || review.summary || "Changes requested");
731
-
732
- await dispatchReview({
733
- repo, prNumber: session.becaria_pr_number,
734
- event: reviewEvent, body: reviewBody, agent: "Reviewer", becariaConfig: bc
735
- });
736
-
737
- await dispatchComment({
738
- repo, prNumber: session.becaria_pr_number, agent: "Reviewer",
739
- body: buildReviewCommentBody(review, i), becariaConfig: bc
740
- });
741
-
742
- logger.info(`BecarIA: dispatched review for PR #${session.becaria_pr_number}`);
743
- } catch (err) {
744
- logger.warn(`BecarIA dispatch failed (non-blocking): ${err.message}`);
745
- }
746
- }
747
278
 
748
- 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 }) {
749
280
  const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
750
281
 
751
282
  if (testerEnabled) {
@@ -754,10 +285,16 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
754
285
  iteration: i, task, diff: postLoopDiff, askQuestion
755
286
  });
756
287
  if (testerResult.action === "pause") return { action: "return", result: testerResult.result };
757
- 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
+ }
758
295
  if (testerResult.stageResult) {
759
296
  stageResults.tester = testerResult.stageResult;
760
- 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"}` });
761
298
  }
762
299
  }
763
300
 
@@ -767,10 +304,16 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
767
304
  iteration: i, task, diff: postLoopDiff, askQuestion
768
305
  });
769
306
  if (securityResult.action === "pause") return { action: "return", result: securityResult.result };
770
- 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
+ }
771
314
  if (securityResult.stageResult) {
772
315
  stageResults.security = securityResult.stageResult;
773
- 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"}` });
774
317
  }
775
318
  }
776
319
 
@@ -781,7 +324,7 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
781
324
  });
782
325
  if (auditResult.stageResult) {
783
326
  stageResults.audit = auditResult.stageResult;
784
- 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"}` });
785
328
  }
786
329
  if (auditResult.action === "retry") {
787
330
  // Audit found actionable issues — loop back to coder
@@ -816,6 +359,31 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
816
359
  if (rtkSavings) session.rtk_savings = rtkSavings;
817
360
  await saveSession(session);
818
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
+
819
387
  const endDetail = { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues };
820
388
  if (rtkSavings) endDetail.rtk_savings = rtkSavings;
821
389
  emitProgress(
@@ -836,8 +404,13 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
836
404
 
837
405
  async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion }) {
838
406
  session.last_reviewer_feedback = review.blocking_issues
839
- .map((x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`)
840
- .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");
841
414
  session.reviewer_retry_count = (session.reviewer_retry_count || 0) + 1;
842
415
  await saveSession(session);
843
416
 
@@ -917,27 +490,13 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
917
490
  applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
918
491
  stageResults.triage = triageResult.stageResult;
919
492
 
920
- // --- Auto-install skills based on task + project detection ---
921
- const projectDir = config.projectDir || process.cwd();
922
- try {
923
- const osAvailable = await isOpenSkillsAvailable();
924
- if (osAvailable) {
925
- const neededSkills = await detectNeededSkills(task, projectDir);
926
- if (neededSkills.length > 0) {
927
- const skillResult = await autoInstallSkills(neededSkills, projectDir);
928
- if (skillResult.installed.length > 0) {
929
- session.autoInstalledSkills = skillResult.installed;
930
- }
931
- emitProgress(emitter, makeEvent("skills:auto-install", { ...eventBase, stage: "triage" }, {
932
- message: skillResult.installed.length > 0
933
- ? `Auto-installed ${skillResult.installed.length} skill(s): ${skillResult.installed.join(", ")}`
934
- : `Skills detected (${neededSkills.join(", ")}) — all already installed or unavailable`,
935
- detail: skillResult
936
- }));
937
- }
493
+ // --- Persist inline domain if provided via --domain flag ---
494
+ if (flags?.domain) {
495
+ try {
496
+ await persistInlineDomain(flags.domain, config.projectDir || process.cwd());
497
+ } catch (err) {
498
+ logger.warn(`Failed to persist inline domain: ${err.message}`);
938
499
  }
939
- } catch (err) {
940
- logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
941
500
  }
942
501
 
943
502
  // --- Domain Curator (after triage + skill auto-install, before planning phases) ---
@@ -1069,13 +628,43 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
1069
628
  // --- Researcher → Planner ---
1070
629
  const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion });
1071
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
+
1072
661
  return { plannedTask, updatedConfig };
1073
662
  }
1074
663
 
1075
664
  async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i }) {
1076
665
  const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
1077
666
  if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
1078
- 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 });
1079
668
  if (coderStandby.handled) {
1080
669
  return coderStandby.action === "return"
1081
670
  ? { action: "return", result: coderStandby.result }
@@ -1085,7 +674,7 @@ async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refac
1085
674
  if (pipelineFlags.refactorerEnabled) {
1086
675
  const refResult = await runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
1087
676
  if (refResult?.action === "pause") return { action: "return", result: refResult.result };
1088
- 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 });
1089
678
  if (refStandby.handled) {
1090
679
  return refStandby.action === "return"
1091
680
  ? { action: "return", result: refStandby.result }
@@ -1178,7 +767,7 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
1178
767
  if (sonarResult.action === "continue") return { action: "continue" };
1179
768
  if (sonarResult.stageResult) {
1180
769
  stageResults.sonar = sonarResult.stageResult;
1181
- 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"}` });
1182
771
  }
1183
772
  }
1184
773
 
@@ -1219,11 +808,26 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
1219
808
  iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion
1220
809
  });
1221
810
  if (reviewerResult.action === "pause") return { action: "return", result: reviewerResult.result };
1222
- 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 });
1223
812
  if (revStandby.handled) {
1224
- return revStandby.action === "return"
1225
- ? { action: "return", result: revStandby.result }
1226
- : { 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" };
1227
831
  }
1228
832
  if (reviewerResult.stalled) return { action: "return", result: reviewerResult.stalledResult };
1229
833
  return { action: "ok", review: reviewerResult.review };
@@ -1233,7 +837,7 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
1233
837
  session.reviewer_retry_count = 0;
1234
838
  const postLoopResult = await handlePostLoopStages({
1235
839
  config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
1236
- 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
1237
841
  });
1238
842
  if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
1239
843
  if (postLoopResult.action === "continue") return { action: "continue" };
@@ -1243,7 +847,7 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
1243
847
  }
1244
848
 
1245
849
  async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker }) {
1246
- // Escalate to Solomon / human before giving up
850
+ const budget = budgetSummary();
1247
851
  const solomonResult = await invokeSolomon({
1248
852
  config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
1249
853
  iteration: config.max_iterations,
@@ -1252,17 +856,26 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
1252
856
  task,
1253
857
  iterationCount: config.max_iterations,
1254
858
  maxIterations: config.max_iterations,
859
+ budget_usd: budget?.total_cost_usd || 0,
1255
860
  history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached without reviewer approval" }]
1256
861
  }
1257
862
  });
1258
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
1259
871
  if (solomonResult.action === "continue") {
1260
872
  if (solomonResult.humanGuidance) {
1261
- session.last_reviewer_feedback = `User guidance: ${solomonResult.humanGuidance}`;
873
+ session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
1262
874
  }
1263
875
  session.reviewer_retry_count = 0;
1264
876
  await saveSession(session);
1265
- 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 };
1266
879
  }
1267
880
 
1268
881
  if (solomonResult.action === "pause") {
@@ -1273,7 +886,7 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
1273
886
  return { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "max_iterations_subtask" };
1274
887
  }
1275
888
 
1276
- // Solomon also couldn't resolve — fail
889
+ // Solomon couldn't resolve — fail
1277
890
  session.budget = budgetSummary();
1278
891
  const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
1279
892
  if (rtkSavings) session.rtk_savings = rtkSavings;
@@ -1310,6 +923,44 @@ async function tryAutoStartBoard(config, logger, emitter, eventBase) {
1310
923
  }
1311
924
 
1312
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
+
1313
964
  const ctx = new PipelineContext({ config, session: null, logger, emitter, task, flags });
1314
965
  ctx.askQuestion = askQuestion;
1315
966
  ctx.pgTaskId = pgTaskId;
@@ -1330,23 +981,19 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1330
981
  ctx.trackBudget = trackBudget;
1331
982
 
1332
983
  // --- RTK detection ---
1333
- if (config.proxy?.enabled) {
1334
- logger.info("Proxy compression active, RTK not needed");
1335
- } else {
1336
- const rtkResult = await detectRtk();
1337
- if (rtkResult.available) {
1338
- config = { ...config, rtk: { available: true, version: rtkResult.version } };
1339
- const rtkTracker = new RtkSavingsTracker();
1340
- const rtkRunner = createRtkRunner(true, rtkTracker);
1341
- setDiffRunner(rtkRunner);
1342
- setGitRunner(rtkRunner);
1343
- ctx.rtkTracker = rtkTracker;
1344
- logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
1345
- emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
1346
- message: "RTK detected — internal commands wrapped for token optimization",
1347
- detail: { version: rtkResult.version, executorType: "local" }
1348
- }));
1349
- }
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
+ }));
1350
997
  }
1351
998
 
1352
999
  // --- HU Board auto-start ---
@@ -1372,21 +1019,6 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1372
1019
  ctx.pgCard = pgAdapterResult.pgCard;
1373
1020
  ctx.session.pg_card = ctx.pgCard || null;
1374
1021
 
1375
- // --- Proxy startup ---
1376
- if (config.proxy?.enabled !== false) {
1377
- try {
1378
- const proxyResult = await startProxy({ config: config.proxy || {}, sessionId: ctx.session.id });
1379
- ctx.proxyPort = proxyResult.port;
1380
- logger.info(`Proxy started on 127.0.0.1:${proxyResult.port}`);
1381
- emitProgress(emitter, makeEvent("proxy:started", ctx.eventBase, {
1382
- message: `Proxy started on port ${proxyResult.port}`,
1383
- detail: { port: proxyResult.port, baseUrls: proxyResult.baseUrls }
1384
- }));
1385
- } catch (err) {
1386
- logger.warn(`Proxy startup failed (non-blocking): ${err.message}`);
1387
- }
1388
- }
1389
-
1390
1022
  emitProgress(
1391
1023
  emitter,
1392
1024
  makeEvent("session:start", ctx.eventBase, {
@@ -1402,6 +1034,38 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
1402
1034
  ctx.plannedTask = preLoopResult.plannedTask;
1403
1035
  ctx.config = preLoopResult.updatedConfig;
1404
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
+
1405
1069
  ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session });
1406
1070
  const projectDir = ctx.config.projectDir || process.cwd();
1407
1071
  ctx.reviewRules = (await resolveReviewProfile({ mode: ctx.config.review_mode, projectDir })).rules;
@@ -1414,7 +1078,7 @@ async function runSingleIteration(ctx) {
1414
1078
  const { config, logger, emitter, eventBase, session, task, iteration: i } = ctx;
1415
1079
 
1416
1080
  const iterStart = Date.now();
1417
- const becariaEnabled = Boolean(config.becaria?.enabled) && ctx.gitCtx?.enabled;
1081
+ const ciEnabled = Boolean(config.ci?.enabled) && ctx.gitCtx?.enabled;
1418
1082
  logger.setContext({ iteration: i, stage: "iteration" });
1419
1083
 
1420
1084
  const reviewerRetryCount = session.reviewer_retry_count || 0;
@@ -1445,8 +1109,8 @@ async function runSingleIteration(ctx) {
1445
1109
  });
1446
1110
  if (qgResult.action === "return" || qgResult.action === "continue") return qgResult;
1447
1111
 
1448
- await handleBecariaEarlyPrOrPush({
1449
- becariaEnabled, config, session, emitter, eventBase, gitCtx: ctx.gitCtx, task, logger,
1112
+ await handleCiEarlyPrOrPush({
1113
+ ciEnabled, config, session, emitter, eventBase, gitCtx: ctx.gitCtx, task, logger,
1450
1114
  stageResults: ctx.stageResults, i
1451
1115
  });
1452
1116
 
@@ -1464,13 +1128,26 @@ async function runSingleIteration(ctx) {
1464
1128
  }));
1465
1129
  session.standby_retry_count = 0;
1466
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
+
1467
1144
  const solomonResult = await handleSolomonCheck({
1468
1145
  config, session, emitter, eventBase, logger, task, i, askQuestion: ctx.askQuestion,
1469
- becariaEnabled, blockingIssues: review?.blocking_issues
1146
+ ciEnabled, blockingIssues: review?.blocking_issues
1470
1147
  });
1471
1148
  if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
1472
1149
 
1473
- await handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger });
1150
+ await handleCiReviewDispatch({ ciEnabled, config, session, review, i, logger });
1474
1151
 
1475
1152
  if (review.approved) {
1476
1153
  const approvedResult = await handleApprovedReview({
@@ -1490,8 +1167,13 @@ async function runSingleIteration(ctx) {
1490
1167
  } else {
1491
1168
  // Solomon is enabled — feed back the blocking issues for the next coder iteration
1492
1169
  session.last_reviewer_feedback = review.blocking_issues
1493
- .map((x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`)
1494
- .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");
1495
1177
  await saveSession(session);
1496
1178
  }
1497
1179
 
@@ -1549,14 +1231,92 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
1549
1231
  ctx.eventBase.iteration = i;
1550
1232
  ctx.iteration = i;
1551
1233
 
1552
- 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
+
1553
1271
  if (iterResult.action === "return") {
1554
1272
  return iterResult.result;
1555
1273
  }
1556
1274
  if (iterResult.action === "retry") { i -= 1; }
1557
1275
  }
1558
1276
 
1277
+ // Solomon decides whether to extend iterations or stop
1559
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
+
1560
1320
  return maxIterResult;
1561
1321
  }
1562
1322
 
@@ -1567,7 +1327,29 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1567
1327
  return handleDryRun({ task, config, flags, emitter, pipelineFlags });
1568
1328
  }
1569
1329
 
1570
- 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
+ }
1571
1353
 
1572
1354
  try {
1573
1355
  // --- Analysis-only flow: skip coder/reviewer when coderRequired === false ---
@@ -1657,14 +1439,6 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
1657
1439
  await writeHistoryRecord({ sessionId: ctx.session.id, task, result, logger });
1658
1440
  return result;
1659
1441
  } finally {
1660
- // --- Proxy shutdown ---
1661
- try {
1662
- await stopProxy();
1663
- if (ctx.proxyPort) {
1664
- logger.info("Proxy stopped");
1665
- }
1666
- } catch { /* non-blocking */ }
1667
-
1668
1442
  // --- Cleanup auto-installed skills ---
1669
1443
  const autoSkills = ctx.session?.autoInstalledSkills;
1670
1444
  if (autoSkills && autoSkills.length > 0) {