karajan-code 2.5.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +39 -4
  2. package/package.json +5 -2
  3. package/src/agents/aider-agent.js +1 -2
  4. package/src/agents/base-agent.js +63 -1
  5. package/src/agents/claude-agent.js +3 -4
  6. package/src/agents/codex-agent.js +1 -2
  7. package/src/agents/gemini-agent.js +1 -2
  8. package/src/agents/index.js +11 -2
  9. package/src/agents/opencode-agent.js +1 -2
  10. package/src/brain/decision-tracker.js +123 -0
  11. package/src/brain/decisor.js +243 -0
  12. package/src/brain/solomon-consult.js +171 -0
  13. package/src/budget/comparison.js +96 -0
  14. package/src/checks/binaries.js +123 -0
  15. package/src/checks/ci.js +105 -0
  16. package/src/checks/config-files.js +217 -0
  17. package/src/checks/dir-setup.js +63 -0
  18. package/src/checks/mcp-health.js +157 -0
  19. package/src/checks/node.js +61 -0
  20. package/src/checks/ports.js +110 -0
  21. package/src/checks/rtk.js +36 -0
  22. package/src/checks/runner.js +267 -0
  23. package/src/checks/skills.js +49 -0
  24. package/src/checks/sonar.js +67 -0
  25. package/src/checks/system.js +53 -0
  26. package/src/checks/tokens.js +96 -0
  27. package/src/checks/types.js +105 -0
  28. package/src/cli.js +29 -3
  29. package/src/commands/board.js +23 -0
  30. package/src/commands/doctor.js +130 -362
  31. package/src/commands/resume.js +1 -1
  32. package/src/commands/run.js +2 -1
  33. package/src/commands/skills.js +49 -0
  34. package/src/config/schema.js +222 -0
  35. package/src/config.js +32 -3
  36. package/src/guards/policy-resolver.js +8 -0
  37. package/src/hu/store.js +9 -0
  38. package/src/infrastructure/command-runner.js +44 -0
  39. package/src/infrastructure/environment.js +38 -0
  40. package/src/infrastructure/file-system-service.js +63 -0
  41. package/src/infrastructure/mocks.js +182 -0
  42. package/src/mcp/handlers/management-handlers.js +7 -2
  43. package/src/mcp/tools.js +10 -3
  44. package/src/orchestrator/config-init.js +16 -1
  45. package/src/orchestrator/flow-runner.js +2085 -0
  46. package/src/orchestrator/pipeline-context.js +17 -0
  47. package/src/orchestrator/preflight-checks.js +78 -4
  48. package/src/orchestrator/solomon-escalation.js +55 -0
  49. package/src/orchestrator/stages/stage-executor.js +155 -0
  50. package/src/orchestrator.js +19 -1923
  51. package/src/prompts/architect.js +2 -2
  52. package/src/prompts/coder.js +2 -2
  53. package/src/prompts/prompt-resolver.js +127 -0
  54. package/src/prompts/reviewer.js +2 -2
  55. package/src/roles/agent-role.js +9 -0
  56. package/src/roles/architect-role.js +3 -1
  57. package/src/roles/base-role.js +40 -2
  58. package/src/roles/coder-role.js +3 -1
  59. package/src/roles/security-role.js +12 -0
  60. package/src/roles/tester-role.js +11 -0
  61. package/src/session/journal/decisions-writer.js +200 -0
  62. package/src/session/journal/iteration-logger.js +194 -0
  63. package/src/session/journal/summary-writer.js +176 -0
  64. package/src/session/journal/tree-writer.js +164 -0
  65. package/src/skills/semantic-detector.js +147 -0
  66. package/src/skills/skill-detector.js +186 -8
  67. package/src/skills/skill-loader.js +131 -1
  68. package/src/skills/skills-cache.js +128 -0
  69. package/src/types/agent.js +55 -0
  70. package/src/types/config.js +114 -0
  71. package/src/types/hu.js +47 -0
  72. package/src/types/index.js +38 -0
  73. package/src/types/policy.js +43 -0
  74. package/src/types/session.js +74 -0
  75. package/src/types/solomon.js +87 -0
  76. package/src/types/stage.js +68 -0
  77. package/src/utils/display/event-handlers.js +389 -0
  78. package/src/utils/display/formatters.js +132 -0
  79. package/src/utils/display/header.js +48 -0
  80. package/src/utils/display/session-summary.js +108 -0
  81. package/src/utils/display/solomon.js +42 -0
  82. package/src/utils/port-check.js +45 -0
  83. package/src/utils/port-occupant.js +95 -0
  84. package/src/utils/provider-env.js +116 -0
  85. package/src/utils/with-timeout.js +42 -0
  86. package/templates/roles/architect/anthropic.md +65 -0
  87. package/templates/roles/architect/default.md +63 -0
  88. package/templates/roles/architect/google.md +52 -0
  89. package/templates/roles/architect/openai.md +51 -0
  90. package/templates/roles/architect/opencode.md +50 -0
  91. package/templates/roles/coder/anthropic.md +72 -0
  92. package/templates/roles/coder/default.md +69 -0
  93. package/templates/roles/coder/google.md +78 -0
  94. package/templates/roles/coder/openai.md +59 -0
  95. package/templates/roles/coder/opencode.md +49 -0
  96. package/templates/roles/planner/anthropic.md +50 -0
  97. package/templates/roles/planner/default.md +48 -0
  98. package/templates/roles/planner/google.md +66 -0
  99. package/templates/roles/planner/openai.md +46 -0
  100. package/templates/roles/planner/opencode.md +53 -0
  101. package/templates/roles/reviewer/anthropic.md +61 -0
  102. package/templates/roles/reviewer/default.md +62 -0
  103. package/templates/roles/reviewer/google.md +63 -0
  104. package/templates/roles/reviewer/openai.md +57 -0
  105. package/templates/roles/reviewer/opencode.md +42 -0
  106. package/templates/roles/solomon/anthropic.md +71 -0
  107. package/templates/roles/solomon/default.md +83 -0
  108. package/templates/roles/solomon/google.md +61 -0
  109. package/templates/roles/solomon/openai.md +53 -0
  110. package/templates/roles/solomon/opencode.md +55 -0
  111. package/src/utils/display.js +0 -669
@@ -0,0 +1,2085 @@
1
+ import { createAgent } from "../agents/index.js";
2
+ import {
3
+ loadSession,
4
+ markSessionStatus,
5
+ pauseSession,
6
+ resumeSessionWithAnswer,
7
+ saveSession,
8
+ addCheckpoint
9
+ } from "../session-store.js";
10
+ import { generateDiff } from "../review/diff-generator.js";
11
+ import { resolveRole } from "../config.js";
12
+ import { resolveReviewProfile } from "../review/profiles.js";
13
+ import { msg, getLang } from "../utils/messages.js";
14
+ import { RepeatDetector, getRepeatThreshold } from "../repeat-detector.js";
15
+ import { emitProgress, makeEvent } from "../utils/events.js";
16
+ import {
17
+ prepareGitAutomation,
18
+ finalizeGitAutomation
19
+ } from "../git/automation.js";
20
+ import { scanDiff } from "../guards/output-guard.js";
21
+ import { scanPerfDiff } from "../guards/perf-guard.js";
22
+ import { classifyIntent } from "../guards/intent-guard.js";
23
+ import { CoderRole } from "../roles/coder-role.js";
24
+ import { invokeSolomon } from "./solomon-escalation.js";
25
+ import { PipelineContext } from "./pipeline-context.js";
26
+ import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./pre-loop-stages.js";
27
+ import { runDomainCuratorStage } from "./stages/domain-curator-stage.js";
28
+ import { persistInlineDomain } from "../domains/domain-loader.js";
29
+ import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./iteration-stages.js";
30
+ import { runTesterStage, runSecurityStage, runImpeccableStage, runFinalAuditStage } from "./post-loop-stages.js";
31
+ import { needsSubPipeline, runHuSubPipeline } from "./hu-sub-pipeline.js";
32
+ import { waitForCooldown, MAX_STANDBY_RETRIES } from "./standby.js";
33
+ import { detectTestFramework } from "../utils/project-detect.js";
34
+ import { runPreflightChecks } from "./preflight-checks.js";
35
+ import { detectRtk } from "../utils/rtk-detect.js";
36
+ import { createRtkRunner, RtkSavingsTracker } from "../utils/rtk-wrapper.js";
37
+ import { setRunner as setDiffRunner, setProjectDir as setDiffProjectDir } from "../review/diff-generator.js";
38
+ import { setRunner as setGitRunner } from "../utils/git.js";
39
+ import { detectNeededSkills, autoInstallSkills, cleanupAutoInstalledSkills } from "../skills/skill-detector.js";
40
+ import { isOpenSkillsAvailable } from "../skills/openskills-client.js";
41
+ import { refineSkillsSemantically, resolveSkillsMode } from "../skills/semantic-detector.js";
42
+
43
+ // Extracted modules
44
+ import {
45
+ loadProductContext,
46
+ resolvePipelineFlags, handleDryRun, createBudgetManager,
47
+ initializeSession, applyTriageOverrides, applyAutoSimplify,
48
+ applyFlagOverrides, resolvePipelinePolicies, autoInit,
49
+ updateGitignoreForStack
50
+ } from "./config-init.js";
51
+ import {
52
+ tryCiComment, handleCiEarlyPrOrPush, handleCiReviewDispatch,
53
+ formatBlockingIssues
54
+ } from "./ci-integration.js";
55
+ import {
56
+ handleCheckpoint, checkSessionTimeout, checkBudgetExceeded,
57
+ takeCheckpointSnapshot
58
+ } from "./flow-control.js";
59
+ import {
60
+ createJournalDir, writePreLoopJournal, writeIterationsJournal,
61
+ writeDecisionsJournal, writeTreeJournal, writeSummaryJournal,
62
+ formatIteration, formatDecision, buildPlanSummary
63
+ } from "./session-journal.js";
64
+
65
+ // Public re-exports (loadProductContext, shouldAutoContinueCheckpoint,
66
+ // parseCheckpointAnswer) live in src/orchestrator.js (the barrel).
67
+
68
+
69
+ // PG card "In Progress" logic moved to src/planning-game/pipeline-adapter.js → initPgAdapter()
70
+
71
+ async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion, brainCtx }) {
72
+ let researchContext = null;
73
+ let plannedTask = task;
74
+
75
+ // Brain: track compression across pre-loop roles
76
+ const brainCompress = brainCtx?.enabled
77
+ ? (await import("./brain-coordinator.js")).processRoleOutput
78
+ : null;
79
+
80
+ if (pipelineFlags.researcherEnabled) {
81
+ const researcherResult = await runResearcherStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
82
+ researchContext = researcherResult.researchContext;
83
+ stageResults.researcher = researcherResult.stageResult;
84
+ if (brainCompress) brainCompress(brainCtx, { roleName: "researcher", output: researcherResult.stageResult, iteration: 0 });
85
+ }
86
+
87
+ // --- Architect (between researcher and planner) ---
88
+ let architectContext = null;
89
+ if (pipelineFlags.architectEnabled) {
90
+ const architectResult = await runArchitectStage({
91
+ config, logger, emitter, eventBase, session, coderRole, trackBudget,
92
+ researchContext,
93
+ discoverResult: stageResults.discover || null,
94
+ triageLevel: stageResults.triage?.level || null,
95
+ askQuestion
96
+ });
97
+ architectContext = architectResult.architectContext;
98
+ stageResults.architect = architectResult.stageResult;
99
+ if (brainCompress) brainCompress(brainCtx, { roleName: "architect", output: architectResult.stageResult, iteration: 0 });
100
+ }
101
+
102
+ const triageDecomposition = stageResults.triage?.shouldDecompose ? stageResults.triage.subtasks : null;
103
+ if (pipelineFlags.plannerEnabled) {
104
+ const plannerRole = resolveRole(config, "planner");
105
+ const plannerResult = await runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, architectContext, triageDecomposition, trackBudget });
106
+ plannedTask = plannerResult.plannedTask;
107
+ stageResults.planner = plannerResult.stageResult;
108
+ if (brainCompress) brainCompress(brainCtx, { roleName: "planner", output: plannerResult.stageResult, iteration: 0 });
109
+
110
+ await tryCiComment({
111
+ config, session, logger,
112
+ agent: "Planner",
113
+ body: `Plan: ${plannerResult.stageResult?.summary || plannedTask}`
114
+ });
115
+ }
116
+
117
+ return { plannedTask };
118
+ }
119
+
120
+
121
+
122
+ async function handleStandbyResult({ stageResult, session, emitter, eventBase, i, stage, logger, config, askQuestion }) {
123
+ if (stageResult?.action !== "standby") {
124
+ return { handled: false };
125
+ }
126
+
127
+ const isOutage = stageResult.standbyInfo.isProviderOutage;
128
+ const agent = stageResult.standbyInfo.agent;
129
+ const cooldownUntil = stageResult.standbyInfo.cooldownUntil;
130
+ const cooldownMs = stageResult.standbyInfo.cooldownMs;
131
+
132
+ // Rate limit = out of normal flow → Solomon decides immediately
133
+ const solomonResult = await invokeSolomon({
134
+ config, logger, emitter, eventBase, stage: `${stage}_rate_limit`, askQuestion, session,
135
+ iteration: i,
136
+ conflict: {
137
+ stage: `${stage}_rate_limit`,
138
+ task: session.task,
139
+ iterationCount: i,
140
+ maxIterations: config?.max_iterations || 5,
141
+ cooldownUntil,
142
+ cooldownMs,
143
+ history: [{
144
+ agent: stage,
145
+ feedback: `Agent "${agent}" rate-limited. ${isOutage ? "Provider outage (5xx)." : "API rate limit (429)."} ${cooldownUntil ? `Cooldown until ${cooldownUntil}.` : ""}`
146
+ }]
147
+ }
148
+ });
149
+
150
+ if (solomonResult.action === "approve") {
151
+ // Solomon says skip is safe — only allowed after exhausting alternatives
152
+ logger.info(`Solomon: skip ${stage} after evaluating risk (agent "${agent}" rate-limited)`);
153
+ emitProgress(emitter, makeEvent(`${stage}:rate_limit`, { ...eventBase, stage }, {
154
+ status: "ok",
155
+ message: `Solomon: skip ${stage} (low risk, agent "${agent}" unavailable)`,
156
+ detail: { agent, solomonAction: "approve" }
157
+ }));
158
+ return { handled: true, action: "skip" };
159
+ }
160
+
161
+ if (solomonResult.action === "continue") {
162
+ // Solomon says: wait for cooldown, or retry with alternative agent
163
+ const altAgent = solomonResult.alternativeAgent;
164
+ const waitTarget = solomonResult.waitUntil;
165
+
166
+ if (waitTarget) {
167
+ const waitMs = Math.max(0, new Date(waitTarget).getTime() - Date.now());
168
+ if (waitMs > 0 && waitMs < 10 * 60 * 1000) {
169
+ logger.info(`Solomon: wait ${Math.round(waitMs / 1000)}s for cooldown, then retry ${stage}`);
170
+ await new Promise(r => setTimeout(r, waitMs));
171
+ }
172
+ }
173
+
174
+ if (altAgent) {
175
+ logger.info(`Solomon: retry ${stage} with alternative agent "${altAgent}"`);
176
+ session._alternative_agent = { stage, provider: altAgent };
177
+ }
178
+
179
+ if (solomonResult.humanGuidance) {
180
+ session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
181
+ }
182
+ await saveSession(session);
183
+ return { handled: true, action: "retry_reviewer_only" };
184
+ }
185
+
186
+ if (solomonResult.action === "pause") {
187
+ return {
188
+ handled: true,
189
+ action: "return",
190
+ result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "rate_limit" }
191
+ };
192
+ }
193
+
194
+ // Solomon couldn't resolve — pause
195
+ await pauseSession(session, {
196
+ question: `Agent "${agent}" rate-limited. Solomon could not resolve.`,
197
+ context: { iteration: i, stage, reason: "rate_limit" }
198
+ });
199
+ return {
200
+ handled: true,
201
+ action: "return",
202
+ result: { paused: true, sessionId: session.id, question: `Agent "${agent}" rate-limited`, context: "rate_limit" }
203
+ };
204
+ }
205
+
206
+
207
+ function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
208
+ for (const alert of alerts) {
209
+ emitProgress(emitter, makeEvent("brain:rules-alert", { ...eventBase, stage: "brain" }, {
210
+ status: alert.severity === "critical" ? "fail" : "warn",
211
+ message: alert.message,
212
+ detail: alert.detail
213
+ }));
214
+ logger.warn(`Rules alert [${alert.rule}]: ${alert.message}`);
215
+ }
216
+ }
217
+
218
+ async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, ciEnabled, blockingIssues, brainCtx }) {
219
+ if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
220
+
221
+ try {
222
+ const { evaluateRules, buildRulesContext } = await import("./solomon-rules.js");
223
+ const rulesContext = await buildRulesContext({ session, task, iteration: i, blockingIssues });
224
+ const rulesResult = evaluateRules(rulesContext, config.solomon?.rules);
225
+
226
+ if (rulesResult.alerts.length > 0) {
227
+ emitSolomonAlerts(rulesResult.alerts, emitter, eventBase, logger);
228
+ // Brain gateway: when Brain is the orchestrator, rule alerts are telemetry.
229
+ // On critical alerts Brain consults Solomon (AI judge). Only if Solomon can't
230
+ // resolve does Brain escalate to human. Solomon-rules never prompts directly.
231
+ if (!brainCtx?.enabled) {
232
+ const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
233
+ if (pauseResult) return pauseResult;
234
+ } else if (rulesResult.hasCritical) {
235
+ const criticalAlerts = rulesResult.alerts.filter(a => a.severity === "critical");
236
+ brainCtx.ruleAlerts = brainCtx.ruleAlerts || [];
237
+ brainCtx.ruleAlerts.push(...criticalAlerts);
238
+ logger.info(`Brain: ${criticalAlerts.length} critical rule alert(s) — consulting Solomon`);
239
+
240
+ const { invokeSolomon } = await import("./solomon-escalation.js");
241
+ const alertSummary = criticalAlerts.map(a => a.message).join("; ");
242
+ const solomonOpinion = await invokeSolomon({
243
+ config, logger, emitter, eventBase, stage: "brain-dilemma", askQuestion, session, iteration: i,
244
+ conflict: {
245
+ stage: "brain-dilemma",
246
+ task,
247
+ iterationCount: i,
248
+ maxIterations: config.max_iterations,
249
+ dilemma: `Brain detected critical rule alerts: ${alertSummary}. Should we continue iterating, pause for human, or stop?`,
250
+ ruleAlerts: criticalAlerts,
251
+ blockingIssues: blockingIssues || [],
252
+ history: (session.checkpoints || []).filter(cp => cp.stage === "reviewer").slice(-5).map(cp => ({ iteration: cp.iteration, feedback: cp.note || "" }))
253
+ }
254
+ });
255
+
256
+ if (solomonOpinion.action === "pause") {
257
+ logger.info("Brain: Solomon advised pause — escalating to human");
258
+ return { action: "return", result: { paused: true, sessionId: session.id, question: solomonOpinion.question || `Brain+Solomon paused: ${alertSummary}`, context: "brain_solomon_dilemma" } };
259
+ }
260
+ if (solomonOpinion.action === "approve") {
261
+ logger.info("Brain: Solomon advised proceeding — treating as approved");
262
+ return { action: "continue", approved: true };
263
+ }
264
+ // action === "continue" | "subtask" | fallback → Brain continues the loop
265
+ logger.info(`Brain: Solomon said '${solomonOpinion.action}' — continuing loop`);
266
+ }
267
+ }
268
+
269
+ if (ciEnabled && session.ci_pr_number) {
270
+ const alerts = rulesResult.alerts || [];
271
+ const alertMsg = alerts.length > 0
272
+ ? alerts.map(a => `- [${a.severity}] ${a.message}`).join("\n")
273
+ : "No anomalies detected";
274
+ await tryCiComment({
275
+ config, session, logger,
276
+ agent: "Solomon",
277
+ body: `Supervisor check iteration ${i}: ${alertMsg}`
278
+ });
279
+ }
280
+ } catch (err) {
281
+ logger.warn(`Solomon rules evaluation failed: ${err.message}`);
282
+ }
283
+
284
+ return { action: "continue" };
285
+ }
286
+
287
+ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i }) {
288
+ if (!rulesResult.hasCritical || !askQuestion) return null;
289
+
290
+ const alertSummary = rulesResult.alerts
291
+ .filter(a => a.severity === "critical")
292
+ .map(a => a.message)
293
+ .join("\n");
294
+ const question = [
295
+ "Solomon detected critical issues:",
296
+ alertSummary,
297
+ "",
298
+ "What should we do?",
299
+ "1 = Continue anyway",
300
+ "2 = Pause the session",
301
+ "3 = Stop the session",
302
+ "",
303
+ "Type 1, 2, or 3:"
304
+ ].join("\n");
305
+ const answer = await askQuestion(question, { iteration: i, stage: "solomon" });
306
+ const trimmed = (answer || "").trim().toLowerCase();
307
+ const shouldPause = !answer
308
+ || trimmed === "2" || trimmed === "3"
309
+ || trimmed.startsWith("pause") || trimmed.startsWith("stop");
310
+ if (shouldPause) {
311
+ await pauseSession(session, {
312
+ question: `Solomon supervisor paused: ${alertSummary}`,
313
+ context: { iteration: i, stage: "solomon", alerts: rulesResult.alerts }
314
+ });
315
+ return { action: "pause", result: { paused: true, sessionId: session.id, reason: "solomon_alert" } };
316
+ }
317
+ return null;
318
+ }
319
+
320
+
321
+ async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, ciEnabled, testerEnabled, securityEnabled, askQuestion, logger, brainCtx }) {
322
+ const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
323
+
324
+ if (testerEnabled) {
325
+ const testerResult = await runTesterStage({
326
+ config, logger, emitter, eventBase, session, coderRole, trackBudget,
327
+ iteration: i, task, diff: postLoopDiff, askQuestion
328
+ });
329
+ if (testerResult.action === "pause") return { action: "return", result: testerResult.result };
330
+ if (testerResult.action === "continue") {
331
+ const summary = testerResult.stageResult?.summary || "Tester found issues";
332
+ session.last_reviewer_feedback = `Tester FAILED — fix these issues:\n${summary}`;
333
+ await saveSession(session);
334
+ if (testerResult.stageResult) stageResults.tester = testerResult.stageResult;
335
+ // Brain: push tester failure into feedback queue + compress for next coder iteration
336
+ if (brainCtx?.enabled) {
337
+ const { processRoleOutput } = await import("./brain-coordinator.js");
338
+ processRoleOutput(brainCtx, { roleName: "tester", output: testerResult.stageResult, iteration: i });
339
+ }
340
+ return { action: "continue" };
341
+ }
342
+ if (testerResult.stageResult) {
343
+ stageResults.tester = testerResult.stageResult;
344
+ await tryCiComment({ config, session, logger, agent: "Tester", body: `Tests: ${testerResult.stageResult.summary || "completed"}` });
345
+ }
346
+ }
347
+
348
+ if (securityEnabled) {
349
+ const securityResult = await runSecurityStage({
350
+ config, logger, emitter, eventBase, session, coderRole, trackBudget,
351
+ iteration: i, task, diff: postLoopDiff, askQuestion, brainCtx
352
+ });
353
+ if (securityResult.action === "pause") return { action: "return", result: securityResult.result };
354
+ if (securityResult.action === "continue") {
355
+ const summary = securityResult.stageResult?.summary || "Security found issues";
356
+ session.last_reviewer_feedback = `Security FAILED — fix these issues:\n${summary}`;
357
+ await saveSession(session);
358
+ if (securityResult.stageResult) stageResults.security = securityResult.stageResult;
359
+ // Brain: push security findings into feedback queue + compress for next coder iteration
360
+ if (brainCtx?.enabled) {
361
+ const { processRoleOutput } = await import("./brain-coordinator.js");
362
+ processRoleOutput(brainCtx, { roleName: "security", output: securityResult.stageResult, iteration: i });
363
+ }
364
+ return { action: "continue" };
365
+ }
366
+ if (securityResult.stageResult) {
367
+ stageResults.security = securityResult.stageResult;
368
+ await tryCiComment({ config, session, logger, agent: "Security", body: `Security scan: ${securityResult.stageResult.summary || "completed"}` });
369
+ }
370
+ }
371
+
372
+ // Final audit — last quality gate before declaring success
373
+ const auditResult = await runFinalAuditStage({
374
+ config, logger, emitter, eventBase, session, coderRole, trackBudget,
375
+ iteration: i, task, diff: postLoopDiff
376
+ });
377
+ if (auditResult.stageResult) {
378
+ stageResults.audit = auditResult.stageResult;
379
+ await tryCiComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
380
+ }
381
+ if (auditResult.action === "retry") {
382
+ // Audit found actionable issues — loop back to coder
383
+ session.last_reviewer_feedback = auditResult.feedback;
384
+ await saveSession(session);
385
+ return { action: "continue" };
386
+ }
387
+
388
+ return { action: "proceed" };
389
+ }
390
+
391
+ async function finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i, rtkTracker }) {
392
+ const gitResult = await finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults });
393
+
394
+ // Accumulate final commits for PG card lifecycle tracking
395
+ if (gitResult?.commits?.length) {
396
+ const { accumulateCommit } = await import("../planning-game/pipeline-adapter.js");
397
+ for (const c of gitResult.commits) accumulateCommit(session, c);
398
+ }
399
+
400
+ if (stageResults.planner?.ok) {
401
+ stageResults.planner.completedSteps = [...(stageResults.planner.steps ?? [])];
402
+ }
403
+ session.budget = budgetSummary();
404
+ await markSessionStatus(session, "approved");
405
+
406
+ const { markPgCardToValidate } = await import("../planning-game/pipeline-adapter.js");
407
+ await markPgCardToValidate({ pgCard, pgProject, config, session, gitResult, logger });
408
+
409
+ const deferredIssues = session.deferred_issues || [];
410
+ const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
411
+ if (rtkSavings) session.rtk_savings = rtkSavings;
412
+ await saveSession(session);
413
+
414
+ // --- Journal: write final files ---
415
+ const journalDir = session._journalDir;
416
+ if (journalDir) {
417
+ try {
418
+ await writeIterationsJournal(journalDir, session._journalIterations || []);
419
+ // decisions.md: writes only when Solomon was invoked (AC of KJC-TSK-0287).
420
+ // Pulls structured data from session.solomonRulings + session.brainDecisions.
421
+ const { writeDecisionsJournal: writeDecisionsJournalV2 } =
422
+ await import("../session/journal/decisions-writer.js");
423
+ const decisionsResult = await writeDecisionsJournalV2(session, { journalDir, logger });
424
+ // KJC-TSK-0288: use the new tree-writer with directory grouping
425
+ const { writeTreeJournal: writeTreeJournalV2 } =
426
+ await import("../session/journal/tree-writer.js");
427
+ const treeResult = await writeTreeJournalV2(journalDir, session.session_start_sha, { logger });
428
+
429
+ const journalFiles = [...(session._journalFiles || [])];
430
+ if (session._journalIterations?.length) journalFiles.push("iterations.md");
431
+ if (decisionsResult.written) journalFiles.push("decisions.md");
432
+ if (treeResult.written) journalFiles.push("tree.txt");
433
+
434
+ // KJC-TSK-0289: use the richer summary writer (stages table, budget
435
+ // breakdown, brain/solomon counts, typed links to other journal files).
436
+ const { writeSummaryJournal: writeSummaryJournalV2 } =
437
+ await import("../session/journal/summary-writer.js");
438
+ const startedAt = session._startedAt ? new Date(session._startedAt).toISOString() : undefined;
439
+ await writeSummaryJournalV2(journalDir, {
440
+ task: session.task,
441
+ result: "APPROVED",
442
+ sessionId: session.id,
443
+ iterations: i,
444
+ durationMs: Date.now() - (session._startedAt || Date.now()),
445
+ budget: budgetSummary(),
446
+ stages: stageResults,
447
+ commits: gitResult?.commits || [],
448
+ files: journalFiles,
449
+ startedAt,
450
+ finishedAt: new Date().toISOString(),
451
+ brainDecisions: Array.isArray(session.brainDecisions) ? session.brainDecisions.length : 0,
452
+ solomonInvocations: Array.isArray(session.solomonRulings) ? session.solomonRulings.length : 0,
453
+ }, { logger });
454
+ logger.info(`Session journal written to ${journalDir}`);
455
+ } catch (err) {
456
+ logger.warn(`Journal write failed (non-blocking): ${err.message}`);
457
+ }
458
+ }
459
+
460
+ const endDetail = { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues };
461
+ if (rtkSavings) endDetail.rtk_savings = rtkSavings;
462
+ emitProgress(
463
+ emitter,
464
+ makeEvent("session:end", { ...eventBase, stage: "done" }, {
465
+ message: deferredIssues.length > 0
466
+ ? `Session approved (${deferredIssues.length} deferred issue(s) tracked as tech debt)`
467
+ : "Session approved",
468
+ detail: endDetail
469
+ })
470
+ );
471
+ const result = { approved: true, sessionId: session.id, review, git: gitResult, deferredIssues };
472
+ if (rtkSavings) result.rtk_savings = rtkSavings;
473
+ return result;
474
+ }
475
+
476
+ // PG card "To Validate" logic moved to src/planning-game/pipeline-adapter.js → markPgCardToValidate()
477
+
478
+ async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion }) {
479
+ session.last_reviewer_feedback = review.blocking_issues
480
+ .map((x) => {
481
+ const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
482
+ if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
483
+ if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
484
+ return parts.join("\n");
485
+ })
486
+ .join("\n\n");
487
+ session.reviewer_retry_count = (session.reviewer_retry_count || 0) + 1;
488
+ await saveSession(session);
489
+
490
+ const maxReviewerRetries = config.session.max_reviewer_retries ?? config.session.fail_fast_repeats;
491
+ if (session.reviewer_retry_count < maxReviewerRetries) {
492
+ return { action: "continue" };
493
+ }
494
+
495
+ emitProgress(
496
+ emitter,
497
+ makeEvent("solomon:escalate", { ...eventBase, stage: "reviewer" }, {
498
+ message: `Reviewer sub-loop limit reached (${session.reviewer_retry_count}/${maxReviewerRetries})`,
499
+ detail: { subloop: "reviewer", retryCount: session.reviewer_retry_count, limit: maxReviewerRetries }
500
+ })
501
+ );
502
+
503
+ const solomonResult = await invokeSolomon({
504
+ config, logger, emitter, eventBase, stage: "reviewer", askQuestion, session, iteration: i,
505
+ conflict: {
506
+ stage: "reviewer",
507
+ task,
508
+ iterationCount: session.reviewer_retry_count,
509
+ maxIterations: maxReviewerRetries,
510
+ history: [{ agent: "reviewer", feedback: session.last_reviewer_feedback }]
511
+ }
512
+ });
513
+
514
+ if (solomonResult.action === "pause") {
515
+ return { action: "return", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "reviewer_fail_fast" } };
516
+ }
517
+ if (solomonResult.action === "continue") {
518
+ if (solomonResult.humanGuidance) {
519
+ session.last_reviewer_feedback += `\nUser guidance: ${solomonResult.humanGuidance}`;
520
+ }
521
+ session.reviewer_retry_count = 0;
522
+ await saveSession(session);
523
+ return { action: "continue" };
524
+ }
525
+ if (solomonResult.action === "subtask") {
526
+ return { action: "return", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "reviewer_subtask" } };
527
+ }
528
+
529
+ return { action: "continue" };
530
+ }
531
+
532
+
533
+ async function runPreLoopStages({ config, logger, emitter, eventBase, session, flags, pipelineFlags, coderRole, trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults, brainCtx }) {
534
+ // --- HU Reviewer (first stage, before everything else, opt-in) ---
535
+ const huFile = flags.huFile || null;
536
+ if (flags.enableHuReviewer !== undefined) pipelineFlags.huReviewerEnabled = Boolean(flags.enableHuReviewer);
537
+ if (pipelineFlags.huReviewerEnabled && huFile) {
538
+ const huResult = await runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile, askQuestion, pgStories: null });
539
+ stageResults.huReviewer = huResult.stageResult;
540
+ }
541
+
542
+ // --- Intent classifier (deterministic pre-triage, opt-in) ---
543
+ if (config.guards?.intent?.enabled) {
544
+ const intentResult = classifyIntent(task, config);
545
+ stageResults.intent = intentResult;
546
+ if (intentResult.classified) {
547
+ emitProgress(emitter, makeEvent("intent:classified", { ...eventBase, stage: "intent" }, {
548
+ message: `Intent classified: ${intentResult.taskType} (${intentResult.level}) — ${intentResult.message}`,
549
+ detail: intentResult
550
+ }));
551
+ }
552
+ }
553
+
554
+ // --- Discover (pre-triage, opt-in) ---
555
+ if (flags.enableDiscover !== undefined) pipelineFlags.discoverEnabled = Boolean(flags.enableDiscover);
556
+ if (pipelineFlags.discoverEnabled) {
557
+ const discoverResult = await runDiscoverStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
558
+ stageResults.discover = discoverResult.stageResult;
559
+ }
560
+
561
+ // --- Triage (always on) ---
562
+ const triageResult = await runTriageStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
563
+ applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
564
+ stageResults.triage = triageResult.stageResult;
565
+
566
+ // --- Brain decisor (opt-in, intent-driven routing) ---
567
+ //
568
+ // When enabled, translates triage output + task text + CLI overrides into a
569
+ // structured Decision and applies it as pipelineFlags overrides. The rest
570
+ // of the orchestrator continues to branch on pipelineFlags as before — the
571
+ // Brain is a router that adjusts WHICH roles run, not HOW they run.
572
+ //
573
+ // Flag precedence:
574
+ // 1. flags.brain === "off" → forced off
575
+ // 2. flags.brain === "on" → forced on
576
+ // 3. config.brain.decisor.enabled (if set)
577
+ // 4. globalThis.__KJ_DEFAULT_BRAIN_DECISOR (test harness default = false)
578
+ // 5. true in production
579
+ const brainDefault = globalThis.__KJ_DEFAULT_BRAIN_DECISOR ?? true;
580
+ let brainDecisorEnabled = config?.brain?.decisor?.enabled ?? brainDefault;
581
+ if (flags?.brain === "off") brainDecisorEnabled = false;
582
+ if (flags?.brain === "on") brainDecisorEnabled = true;
583
+ if (brainDecisorEnabled) {
584
+ try {
585
+ const { buildDecision, applyDecisionToFlags } = await import("../brain/decisor.js");
586
+ const { createTracker, recordDecision, checkLimits } = await import("../brain/decision-tracker.js");
587
+ const tracker = createTracker(session, { max: config?.brain?.decisor?.maxDecisions });
588
+ const limitStatus = checkLimits(tracker);
589
+ if (limitStatus.status === "ok") {
590
+ let decision = buildDecision({
591
+ triage: triageResult.stageResult,
592
+ task,
593
+ config,
594
+ overrides: {
595
+ // Commander variadic --force-role a b → flags.forceRole = ["a","b"].
596
+ // Back-compat: accept both singular (commander) and plural (legacy / API).
597
+ forceRoles: flags?.forceRole || flags?.forceRoles || [],
598
+ skipRoles: flags?.skipRole || flags?.skipRoles || [],
599
+ },
600
+ });
601
+ // If the decision is low-confidence or ambiguous, consult Solomon
602
+ // for a refined routing suggestion. Solomon's ruling is advisory —
603
+ // if it fails or returns nothing useful, the baseline decision stands.
604
+ if (decision.consultSolomon) {
605
+ try {
606
+ const { consultSolomonForRouting } = await import("../brain/solomon-consult.js");
607
+ decision = await consultSolomonForRouting({
608
+ decision, triage: triageResult.stageResult, task, config, logger,
609
+ emitter, eventBase, session,
610
+ });
611
+ } catch (err) {
612
+ logger.warn(`Brain → Solomon consult failed (non-blocking): ${err.message}`);
613
+ }
614
+ }
615
+ const newFlags = applyDecisionToFlags(decision, pipelineFlags);
616
+ Object.assign(pipelineFlags, newFlags);
617
+ recordDecision(tracker, decision, {
618
+ taskType: triageResult.stageResult?.taskType,
619
+ level: triageResult.stageResult?.level,
620
+ });
621
+ emitProgress(emitter, makeEvent("brain:decision", { ...eventBase, stage: "brain" }, {
622
+ status: "ok",
623
+ message: `Brain routing: ${decision.rolesOn.length} role(s) active — ${decision.rationale}`,
624
+ detail: {
625
+ rolesOn: decision.rolesOn,
626
+ rolesOff: decision.rolesOff,
627
+ confidence: decision.confidence,
628
+ consultSolomon: decision.consultSolomon,
629
+ appliedOverrides: decision.appliedOverrides,
630
+ },
631
+ }));
632
+ } else {
633
+ logger.warn(`Brain decisor skipped: ${limitStatus.detail}`);
634
+ emitProgress(emitter, makeEvent("brain:decision", { ...eventBase, stage: "brain" }, {
635
+ status: "warn",
636
+ message: `Brain decisor skipped: ${limitStatus.status}`,
637
+ detail: limitStatus,
638
+ }));
639
+ }
640
+ } catch (err) {
641
+ logger.warn(`Brain decisor failed (non-blocking): ${err.message}`);
642
+ }
643
+ }
644
+
645
+ // --- Persist inline domain if provided via --domain flag ---
646
+ if (flags?.domain) {
647
+ try {
648
+ await persistInlineDomain(flags.domain, config.projectDir || process.cwd());
649
+ } catch (err) {
650
+ logger.warn(`Failed to persist inline domain: ${err.message}`);
651
+ }
652
+ }
653
+
654
+ // --- Domain Curator (after triage + skill auto-install, before planning phases) ---
655
+ const domainHints = triageResult.stageResult?.domainHints || [];
656
+ if (domainHints.length > 0 || config.projectDir) {
657
+ try {
658
+ const { domainContext, stageResult: dcStageResult } = await runDomainCuratorStage({
659
+ config, logger, emitter, eventBase, session, trackBudget,
660
+ domainHints, askQuestion
661
+ });
662
+ stageResults.domainCurator = dcStageResult;
663
+ if (domainContext) {
664
+ config = { ...config, domainContext };
665
+ }
666
+ } catch (err) {
667
+ logger.warn(`Domain Curator failed (non-blocking): ${err.message}`);
668
+ }
669
+ }
670
+
671
+ // --- HU Reviewer auto-activation from triage (post-triage, no huFile needed) ---
672
+ const triageRoles = new Set(triageResult.stageResult?.roles || []);
673
+ if (triageRoles.has("hu-reviewer") && !stageResults.huReviewer) {
674
+ pipelineFlags.huReviewerEnabled = true;
675
+ // Feed PG card structured data to hu-reviewer when available
676
+ let pgStories = null;
677
+ if (pgTaskId && pgProject && session.pg_card) {
678
+ const { buildHuStoriesFromPgCard } = await import("../planning-game/pipeline-adapter.js");
679
+ pgStories = buildHuStoriesFromPgCard(session.pg_card);
680
+ }
681
+ const huResult = await runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile: null, askQuestion, pgStories });
682
+ stageResults.huReviewer = huResult.stageResult;
683
+ }
684
+
685
+ // --- Auto-simplify pipeline for simple tasks (before explicit flag overrides) ---
686
+ const simplified = applyAutoSimplify({
687
+ pipelineFlags,
688
+ triageLevel: triageResult.stageResult?.level || null,
689
+ config, flags, logger, emitter, eventBase
690
+ });
691
+ if (simplified) stageResults.triage.autoSimplified = true;
692
+
693
+ const { handlePgDecomposition } = await import("../planning-game/pipeline-adapter.js");
694
+ await handlePgDecomposition({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger });
695
+
696
+ applyFlagOverrides(pipelineFlags, flags);
697
+
698
+ // --- Auto-detect TDD applicability when methodology not explicitly set ---
699
+ if (!flags.methodology) {
700
+ const projectDir = config.projectDir || process.cwd();
701
+ const detection = await detectTestFramework(projectDir);
702
+ if (!detection.hasTests) {
703
+ config = { ...config, development: { ...config.development, methodology: "standard", require_test_changes: false } };
704
+ logger.info("No test framework detected — using standard methodology");
705
+ } else {
706
+ config = { ...config, development: { ...config.development, methodology: "tdd", require_test_changes: true } };
707
+ logger.info(`Test framework detected (${detection.framework}) — using TDD methodology`);
708
+ }
709
+ emitProgress(emitter, makeEvent("tdd:auto-detect", { ...eventBase, stage: "pre-loop" }, {
710
+ message: detection.hasTests
711
+ ? `TDD auto-detected: ${detection.framework}`
712
+ : "TDD skipped: no test framework found",
713
+ detail: detection
714
+ }));
715
+ }
716
+
717
+ let updatedConfig = resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags });
718
+
719
+ // --- Preflight environment checks ---
720
+ const preflightResult = await runPreflightChecks({
721
+ config: updatedConfig, logger, emitter, eventBase,
722
+ resolvedPolicies: session.resolved_policies,
723
+ securityEnabled: pipelineFlags.securityEnabled
724
+ });
725
+ session.preflight = preflightResult;
726
+ await saveSession(session);
727
+
728
+ // Hard fail if blocking checks failed (SonarQube enabled but not available)
729
+ if (!preflightResult.ok) {
730
+ const errorLines = (preflightResult.errors || [])
731
+ .map(e => ` - ${e.message}\n Fix: ${e.fix}`)
732
+ .join("\n");
733
+ throw new Error(
734
+ `Preflight FAILED — environment changed during session. Fix the issues and retry:\n${errorLines}`
735
+ );
736
+ }
737
+
738
+ if (preflightResult.configOverrides.securityDisabled) {
739
+ pipelineFlags.securityEnabled = false;
740
+ }
741
+
742
+ // --- Plan injection: skip researcher/architect/planner if a persisted plan is loaded ---
743
+ if (flags.plan) {
744
+ try {
745
+ const { loadPlan, savePlan: savePlanToDisk } = await import("../plan/plan-store.js");
746
+ const { isPlanV2 } = await import("../plan/plan-schema.js");
747
+ const { planToHuBatch, syncResultsToPlan } = await import("../plan/plan-executor.js");
748
+ const projectDir = updatedConfig.projectDir || process.cwd();
749
+ const loadedPlan = await loadPlan(projectDir, flags.plan);
750
+ if (loadedPlan) {
751
+ logger.info(`Loaded persisted plan: ${flags.plan} (v${loadedPlan.version || 1})`);
752
+ emitProgress(emitter, makeEvent("plan:loaded", { ...eventBase, stage: "plan" }, {
753
+ message: `Plan loaded: ${flags.plan}`,
754
+ detail: { planId: flags.plan, task: loadedPlan.task, version: loadedPlan.version, huCount: loadedPlan.hus?.length || 0 }
755
+ }));
756
+ stageResults.researcher = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
757
+ stageResults.architect = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
758
+ stageResults.planner = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
759
+
760
+ // v2 plan with HUs → inject as huReviewer so sub-pipeline picks them up
761
+ if (isPlanV2(loadedPlan) && loadedPlan.hus?.length > 0) {
762
+ const huBatch = planToHuBatch(loadedPlan);
763
+ if (huBatch.ok) {
764
+ stageResults.huReviewer = huBatch;
765
+ // Store plan reference so we can sync results back after execution
766
+ session._planRef = { planId: loadedPlan.planId, projectDir };
767
+ session._syncResultsToPlan = async (subResult) => {
768
+ syncResultsToPlan(loadedPlan, subResult);
769
+ await savePlanToDisk(projectDir, loadedPlan);
770
+ };
771
+ logger.info(`Plan ${flags.plan}: ${huBatch.certified} HUs ready for execution`);
772
+ emitProgress(emitter, makeEvent("plan:hus-loaded", { ...eventBase, stage: "plan" }, {
773
+ message: `${huBatch.certified} HUs loaded from plan`,
774
+ detail: { planId: flags.plan, total: huBatch.total, certified: huBatch.certified }
775
+ }));
776
+ // Mark plan as running
777
+ loadedPlan.status = "running";
778
+ await savePlanToDisk(projectDir, loadedPlan);
779
+ }
780
+ }
781
+
782
+ // Inject contexts
783
+ const ctx = loadedPlan.context || {};
784
+ session.research_context = ctx.researchContext || loadedPlan.researchContext || null;
785
+ session.architect_context = ctx.architectContext || loadedPlan.architectContext || null;
786
+ session.loaded_plan = loadedPlan.approach || loadedPlan.plan || null;
787
+ await saveSession(session);
788
+
789
+ // Build planned task from plan (for non-HU or fallback path)
790
+ let plannedTask = task;
791
+ const plan = loadedPlan.plan || (loadedPlan.approach ? { approach: loadedPlan.approach } : null);
792
+ if (plan && typeof plan === "object" && plan.steps) {
793
+ const stepList = plan.steps.map((s, idx) => `${idx + 1}. ${s.description || s}`).join("\n");
794
+ plannedTask = `${task}\n\n## Implementation Plan\n${plan.approach || ""}\n\n## Steps\n${stepList}`;
795
+ } else if (typeof plan === "string") {
796
+ plannedTask = `${task}\n\n## Implementation Plan\n${plan}`;
797
+ } else if (loadedPlan.approach) {
798
+ plannedTask = `${task}\n\n## Approach\n${loadedPlan.approach}`;
799
+ }
800
+ return { plannedTask, updatedConfig };
801
+ }
802
+ logger.warn(`Plan ${flags.plan} not found — falling back to normal pipeline`);
803
+ } catch (err) {
804
+ logger.warn(`Plan loading failed: ${err.message} — falling back to normal pipeline`);
805
+ }
806
+ }
807
+
808
+ // --- Researcher → Planner ---
809
+ const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion, brainCtx });
810
+
811
+ // --- Update .gitignore with stack-specific entries based on planner/architect output ---
812
+ const projectDir = updatedConfig.projectDir || process.cwd();
813
+ await updateGitignoreForStack(projectDir, { stageResults, task, logger });
814
+
815
+ // --- Auto-HU: when triage recommended decomposition and no manual huFile, generate HU batch ---
816
+ await maybeGenerateAutoHuBatch({
817
+ flags, stageResults, task, plannedTask, logger, emitter, eventBase, projectDir, session
818
+ });
819
+
820
+ // --- Auto-install skills based on task + planner output + project detection ---
821
+ // Runs AFTER triage and planner so that the planned task text (which includes
822
+ // planner output like implementation steps) is available for keyword detection.
823
+ // This ensures greenfield projects with no package.json still get correct skills.
824
+ //
825
+ // Detection runs UNCONDITIONALLY — even when the openskills CLI is missing —
826
+ // so the session report can recommend which skills would have been used. The
827
+ // actual install is skipped inside autoInstallSkills when unavailable.
828
+ const skillProjectDir = updatedConfig.projectDir || process.cwd();
829
+ try {
830
+ const skillsMode = resolveSkillsMode(updatedConfig, flags);
831
+ if (skillsMode === "none") {
832
+ // User explicitly opted out. Skip detection entirely.
833
+ return { plannedTask, updatedConfig };
834
+ }
835
+ let neededSkills = skillsMode === "semantic"
836
+ ? []
837
+ : await detectNeededSkills(plannedTask, skillProjectDir);
838
+ // Semantic mode augments regex detection with a classifier call.
839
+ // "auto" = regex always + semantic when budget allows (v1: always when mode=auto and classifier reachable).
840
+ if (skillsMode === "auto" || skillsMode === "semantic") {
841
+ const extra = await refineSkillsSemantically({
842
+ task: plannedTask,
843
+ alreadyDetected: neededSkills,
844
+ config: updatedConfig,
845
+ logger,
846
+ });
847
+ if (extra.length > 0) {
848
+ logger.info(`Semantic skill detection added: ${extra.join(", ")}`);
849
+ neededSkills = Array.from(new Set([...neededSkills, ...extra]));
850
+ }
851
+ }
852
+ if (neededSkills.length > 0) {
853
+ const skillResult = await autoInstallSkills(neededSkills, skillProjectDir);
854
+ if (skillResult.installed.length > 0) {
855
+ session.autoInstalledSkills = skillResult.installed;
856
+ }
857
+ if (skillResult.osAvailable === false && skillResult.wouldHaveUsed.length > 0) {
858
+ session.skillsRecommended = skillResult.wouldHaveUsed;
859
+ logger.warn(`OpenSkills CLI not available — would have used: ${skillResult.wouldHaveUsed.join(", ")}. Install openskills globally to auto-inject them next time.`);
860
+ emitProgress(emitter, makeEvent("skills:unavailable", { ...eventBase, stage: "skills" }, {
861
+ message: `OpenSkills CLI not available — would have used: ${skillResult.wouldHaveUsed.join(", ")}`,
862
+ detail: {
863
+ wouldHaveUsed: skillResult.wouldHaveUsed,
864
+ hint: "Install openskills globally: npm install -g openskills",
865
+ },
866
+ }));
867
+ }
868
+ emitProgress(emitter, makeEvent("skills:auto-install", { ...eventBase, stage: "skills" }, {
869
+ message: skillResult.installed.length > 0
870
+ ? `Auto-installed ${skillResult.installed.length} skill(s): ${skillResult.installed.join(", ")}`
871
+ : `Skills detected (${neededSkills.join(", ")}) — all already installed or unavailable`,
872
+ detail: skillResult,
873
+ }));
874
+ }
875
+ } catch (err) {
876
+ logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
877
+ }
878
+
879
+ return { plannedTask, updatedConfig };
880
+ }
881
+
882
+ /**
883
+ * Auto-generate HU batch from triage decomposition when no manual huFile is present.
884
+ * Runs after researcher/architect/planner so that context is available for better HUs.
885
+ * Sets stageResults.huReviewer so needsSubPipeline picks it up later.
886
+ */
887
+ async function maybeGenerateAutoHuBatch({ flags, stageResults, task, plannedTask, logger, emitter, eventBase, projectDir, session }) {
888
+ // Skip if user passed a manual hu-file
889
+ if (flags?.huFile) return;
890
+ // Skip if hu-reviewer already produced a batch (manual enable + PG stories)
891
+ if (stageResults.huReviewer) return;
892
+ // Need triage decomposition recommendation
893
+ const shouldDecompose = stageResults.triage?.shouldDecompose;
894
+ const subtasks = stageResults.triage?.subtasks;
895
+ if (!shouldDecompose || !Array.isArray(subtasks) || subtasks.length < 2) return;
896
+
897
+ const { generateHuBatch } = await import("../hu/auto-generator.js");
898
+
899
+ // Detect if project is new: empty dir or only .git/.karajan/.gitignore
900
+ let isNewProject = false;
901
+ try {
902
+ const fs = await import("node:fs/promises");
903
+ const entries = await fs.readdir(projectDir);
904
+ const relevant = entries.filter(e => !e.startsWith(".git") && e !== ".karajan" && e !== ".gitignore");
905
+ isNewProject = relevant.length === 0;
906
+ } catch { /* ignore */ }
907
+
908
+ // Extract stack hints from planner + architect output
909
+ const stackHints = [];
910
+ const combined = `${stageResults.planner?.plan || ""} ${stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : ""} ${task}`.toLowerCase();
911
+ const stackKeywords = ["express", "vite", "vitest", "jest", "next", "astro", "react", "vue", "svelte", "fastapi", "django", "spring", "gin", "nestjs", "monorepo", "workspaces"];
912
+ for (const kw of stackKeywords) {
913
+ if (combined.includes(kw)) stackHints.push(kw);
914
+ }
915
+
916
+ const batch = generateHuBatch({
917
+ originalTask: task,
918
+ subtasks,
919
+ stackHints,
920
+ isNewProject,
921
+ researcherContext: stageResults.researcher?.summary || null,
922
+ architectContext: stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : null
923
+ });
924
+
925
+ // Persist batch to HU store so hu-sub-pipeline can update story status via saveHuBatch.
926
+ // Use session.id as batchSessionId.
927
+ const batchSessionId = `auto-${session.id}`;
928
+ try {
929
+ const fs = await import("node:fs/promises");
930
+ const path = await import("node:path");
931
+ const { getKarajanHome } = await import("../utils/paths.js");
932
+ const huDir = path.join(getKarajanHome(), "hu-stories", batchSessionId);
933
+ await fs.mkdir(huDir, { recursive: true });
934
+ const persistBatch = {
935
+ session_id: batchSessionId,
936
+ project_id: batchSessionId,
937
+ project_name: batch.projectName,
938
+ created_at: new Date().toISOString(),
939
+ updated_at: new Date().toISOString(),
940
+ stories: batch.stories
941
+ };
942
+ await fs.writeFile(path.join(huDir, "batch.json"), JSON.stringify(persistBatch, null, 2));
943
+ } catch (err) {
944
+ logger.warn(`Auto-HU: failed to persist batch (${err.message}) — sub-pipeline will use in-memory fallback`);
945
+ }
946
+
947
+ // Wrap in format compatible with needsSubPipeline + runHuSubPipeline
948
+ stageResults.huReviewer = {
949
+ ok: true,
950
+ stories: batch.stories,
951
+ total: batch.total,
952
+ certified: batch.certified,
953
+ batchSessionId,
954
+ auto_generated: true,
955
+ source: batch.source
956
+ };
957
+
958
+ logger.info(`Auto-HU: generated ${batch.total} stories (${batch.source.triage_subtasks} subtasks${isNewProject ? ", new project" : ""}${stackHints.length ? `, stack: ${stackHints.join(",")}` : ""})`);
959
+ emitProgress(emitter, makeEvent("hu:auto-generated", { ...eventBase, stage: "hu-auto-gen" }, {
960
+ message: `Auto-generated ${batch.total} HU(s) from triage decomposition`,
961
+ detail: { total: batch.total, subtasks: batch.source.triage_subtasks, isNewProject, stackHints, projectName: batch.projectName }
962
+ }));
963
+
964
+ // Auto-start the board so the user can see the generated HUs.
965
+ // Always fires when auto-HU runs, independent of hu_board.auto_start flag.
966
+ // Never during vitest — would race the PID file and leave a detached
967
+ // node process around after the suite (TSK-0273).
968
+ if (process.env.VITEST || process.env.NODE_ENV === "test") return;
969
+ try {
970
+ const { startBoard, renderBoardBanner } = await import("../commands/board.js");
971
+ const desiredPort = session.config_snapshot?.hu_board?.port ?? 4000;
972
+ const boardResult = await startBoard(desiredPort);
973
+ const url = boardResult.url;
974
+ const status = boardResult.alreadyRunning ? "already running" : "started";
975
+ const projectName = batch.projectName || "Auto-generated HUs";
976
+ console.log(renderBoardBanner({ url, status, projectName }));
977
+ logger.info(`HU Board ${status} at ${url} (project: ${projectName})`);
978
+ } catch (err) {
979
+ logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
980
+ }
981
+ }
982
+
983
+ async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i, brainCtx }) {
984
+ const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i, brainCtx });
985
+ if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
986
+ const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger, config });
987
+ if (coderStandby.handled) {
988
+ return coderStandby.action === "return"
989
+ ? { action: "return", result: coderStandby.result }
990
+ : { action: "retry" };
991
+ }
992
+
993
+ if (pipelineFlags.refactorerEnabled) {
994
+ const refResult = await runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
995
+ if (refResult?.action === "pause") return { action: "return", result: refResult.result };
996
+ const refStandby = await handleStandbyResult({ stageResult: refResult, session, emitter, eventBase, i, stage: "refactorer", logger, config });
997
+ if (refStandby.handled) {
998
+ return refStandby.action === "return"
999
+ ? { action: "return", result: refStandby.result }
1000
+ : { action: "retry" };
1001
+ }
1002
+ }
1003
+
1004
+ return { action: "ok" };
1005
+ }
1006
+
1007
+ async function runGuardStages({ config, logger, emitter, eventBase, session, iteration }) {
1008
+ const outputEnabled = config.guards?.output?.enabled !== false;
1009
+ const perfEnabled = config.guards?.perf?.enabled !== false;
1010
+
1011
+ if (!outputEnabled && !perfEnabled) return { action: "ok" };
1012
+
1013
+ const baseBranch = config.base_branch || "main";
1014
+ let diff;
1015
+ try {
1016
+ const { generateDiff: genDiff, computeBaseRef: compBase } = await import("../review/diff-generator.js");
1017
+ const baseRef = await compBase({ baseBranch });
1018
+ diff = await genDiff({ baseRef });
1019
+ } catch {
1020
+ logger.warn("Guards: could not generate diff, skipping");
1021
+ return { action: "ok" };
1022
+ }
1023
+
1024
+ if (!diff) return { action: "ok" };
1025
+
1026
+ if (outputEnabled) {
1027
+ const outputResult = scanDiff(diff, config);
1028
+ if (outputResult.violations.length > 0) {
1029
+ const critical = outputResult.violations.filter(v => v.severity === "critical");
1030
+ const warnings = outputResult.violations.filter(v => v.severity === "warning");
1031
+ emitProgress(emitter, makeEvent("guard:output", { ...eventBase, stage: "guard" }, {
1032
+ message: `Output guard: ${critical.length} critical, ${warnings.length} warnings`,
1033
+ detail: { violations: outputResult.violations, executorType: "local" }
1034
+ }));
1035
+ logger.info(`Output guard: ${outputResult.violations.length} violation(s) found`);
1036
+ for (const v of outputResult.violations) {
1037
+ logger.info(` [${v.severity}] ${v.file}:${v.line} — ${v.message}`);
1038
+ }
1039
+ await addCheckpoint(session, { stage: "guard-output", iteration, pass: outputResult.pass, violations: outputResult.violations.length });
1040
+
1041
+ if (!outputResult.pass && config.guards.output.on_violation === "block") {
1042
+ await markSessionStatus(session, "failed");
1043
+ emitProgress(emitter, makeEvent("guard:blocked", { ...eventBase, stage: "guard" }, {
1044
+ message: "Output guard blocked: critical violations detected",
1045
+ detail: { violations: critical }
1046
+ }));
1047
+ return {
1048
+ action: "return",
1049
+ result: { approved: false, sessionId: session.id, reason: "guard_blocked", violations: critical }
1050
+ };
1051
+ }
1052
+ }
1053
+ }
1054
+
1055
+ if (perfEnabled) {
1056
+ const perfResult = scanPerfDiff(diff, config);
1057
+ if (!perfResult.skipped && perfResult.violations.length > 0) {
1058
+ emitProgress(emitter, makeEvent("guard:perf", { ...eventBase, stage: "guard" }, {
1059
+ message: `Perf guard: ${perfResult.violations.length} issue(s)`,
1060
+ detail: { violations: perfResult.violations, executorType: "local" }
1061
+ }));
1062
+ logger.info(`Perf guard: ${perfResult.violations.length} issue(s) found`);
1063
+ for (const v of perfResult.violations) {
1064
+ logger.info(` [${v.severity}] ${v.file}:${v.line} — ${v.message}`);
1065
+ }
1066
+ await addCheckpoint(session, { stage: "guard-perf", iteration, pass: perfResult.pass, violations: perfResult.violations.length });
1067
+ }
1068
+ }
1069
+
1070
+ return { action: "ok" };
1071
+ }
1072
+
1073
+ async function runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults, coderRole, pipelineFlags, brainCtx }) {
1074
+ const tddResult = await runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration: i, askQuestion, task, brainCtx });
1075
+ if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
1076
+ if (tddResult.action === "continue") return { action: "continue" };
1077
+
1078
+ const skipSonarForTaskType = new Set(["infra", "doc", "no-code"]);
1079
+ const effectiveTaskType = session.resolved_policies?.taskType || null;
1080
+ if (config.sonarqube.enabled && !skipSonarForTaskType.has(effectiveTaskType)) {
1081
+ const sonarResult = await runSonarStage({
1082
+ config, logger, emitter, eventBase, session, trackBudget, iteration: i,
1083
+ repeatDetector, budgetSummary, sonarState, askQuestion, task, brainCtx
1084
+ });
1085
+ if (sonarResult.action === "stalled" || sonarResult.action === "pause") return { action: "return", result: sonarResult.result };
1086
+ if (sonarResult.action === "continue") return { action: "continue" };
1087
+ if (sonarResult.stageResult) {
1088
+ stageResults.sonar = sonarResult.stageResult;
1089
+ await tryCiComment({ config, session, logger, agent: "Sonar", body: `SonarQube scan: ${sonarResult.stageResult.summary || "completed"}` });
1090
+ }
1091
+ }
1092
+
1093
+ if (config.sonarcloud?.enabled) {
1094
+ const cloudResult = await runSonarCloudStage({
1095
+ config, logger, emitter, eventBase, session, trackBudget, iteration: i
1096
+ });
1097
+ if (cloudResult.stageResult) {
1098
+ stageResults.sonarcloud = cloudResult.stageResult;
1099
+ }
1100
+ }
1101
+
1102
+ if (pipelineFlags?.impeccableEnabled) {
1103
+ const diff = await generateDiff({ baseRef: session.session_start_sha });
1104
+ const impeccableMode = pipelineFlags?.impeccableMode || "audit";
1105
+ const impeccableResult = await runImpeccableStage({
1106
+ config, logger, emitter, eventBase, session, coderRole, trackBudget,
1107
+ iteration: i, task, diff, mode: impeccableMode
1108
+ });
1109
+ if (impeccableResult.stageResult) {
1110
+ stageResults.impeccable = impeccableResult.stageResult;
1111
+ }
1112
+ }
1113
+
1114
+ return { action: "ok" };
1115
+ }
1116
+
1117
+ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx }) {
1118
+ if (!pipelineFlags.reviewerEnabled) {
1119
+ return {
1120
+ action: "ok",
1121
+ review: { approved: true, blocking_issues: [], non_blocking_suggestions: [], summary: "Reviewer disabled by pipeline", confidence: 1 }
1122
+ };
1123
+ }
1124
+
1125
+ const reviewerResult = await runReviewerStage({
1126
+ reviewerRole, config, logger, emitter, eventBase, session, trackBudget,
1127
+ iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx
1128
+ });
1129
+ if (reviewerResult.action === "pause") return { action: "return", result: reviewerResult.result };
1130
+ const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger, config, askQuestion });
1131
+ if (revStandby.handled) {
1132
+ if (revStandby.action === "return") return { action: "return", result: revStandby.result };
1133
+ if (revStandby.action === "skip") {
1134
+ // Solomon said skip review — treat as approved
1135
+ return { action: "ok", review: { approved: true, blocking_issues: [], non_blocking_suggestions: [], summary: "Review skipped (agent rate-limited, Solomon approved)", confidence: 0.7 } };
1136
+ }
1137
+ if (revStandby.action === "retry_reviewer_only") {
1138
+ // Retry just the reviewer — use alternative agent if Solomon recommended one
1139
+ let retryReviewerRole = reviewerRole;
1140
+ const alt = session._alternative_agent;
1141
+ if (alt?.stage === "reviewer" && alt?.provider) {
1142
+ const { createAgent } = await import("../agents/index.js");
1143
+ retryReviewerRole = { provider: alt.provider, model: null };
1144
+ logger.info(`Retrying reviewer with alternative agent: ${alt.provider}`);
1145
+ delete session._alternative_agent;
1146
+ }
1147
+ return runReviewerGateStage({ pipelineFlags: { reviewerEnabled: true }, reviewerRole: retryReviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion });
1148
+ }
1149
+ return { action: "retry" };
1150
+ }
1151
+ if (reviewerResult.stalled) return { action: "return", result: reviewerResult.stalledResult };
1152
+ return { action: "ok", review: reviewerResult.review };
1153
+ }
1154
+
1155
+ async function handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review, rtkTracker, brainCtx }) {
1156
+ session.reviewer_retry_count = 0;
1157
+ const postLoopResult = await handlePostLoopStages({
1158
+ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
1159
+ ciEnabled: Boolean(config.ci?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger, brainCtx
1160
+ });
1161
+ if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
1162
+ if (postLoopResult.action === "continue") return { action: "continue" };
1163
+
1164
+ const result = await finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i, rtkTracker });
1165
+ return { action: "return", result };
1166
+ }
1167
+
1168
+ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker, brainCtx }) {
1169
+ const budget = budgetSummary();
1170
+
1171
+ // Brain-owned decision: max_iterations is guidance, not a hard rule.
1172
+ // Brain evaluates the feedback queue state to decide extend / finalize / escalate.
1173
+ // Solomon is only consulted if Brain cannot decide on its own.
1174
+ if (brainCtx?.enabled) {
1175
+ const entries = brainCtx.feedbackQueue?.entries || [];
1176
+ const pending = entries.map(e => ({ source: e.source, category: e.category, severity: e.severity, description: e.description }));
1177
+ const hasSecurity = entries.some(e => e.category === "security" || e.source === "security");
1178
+ const hasCorrectness = entries.some(e => ["correctness", "tests"].includes(e.category));
1179
+ const hasStyleOnly = entries.length > 0 && !hasSecurity && !hasCorrectness;
1180
+
1181
+ if (hasSecurity) {
1182
+ // Brain: security issues unresolved → cannot finalize, escalate
1183
+ logger.warn(`Brain: max_iterations reached with ${entries.filter(e => e.category === "security" || e.source === "security").length} unresolved security issue(s) — cannot finalize`);
1184
+ return { paused: true, sessionId: session.id, question: "Brain: unresolved security issues at max_iterations. Review manually or extend pipeline.", context: "brain_security_block", pending };
1185
+ }
1186
+
1187
+ if (hasCorrectness) {
1188
+ // Brain: correctness/test issues pending. Cap at MAX_EXTENSIONS to avoid infinite extensions.
1189
+ const MAX_EXTENSIONS = 2;
1190
+ if (brainCtx.extensionCount >= MAX_EXTENSIONS) {
1191
+ logger.warn(`Brain: ${brainCtx.extensionCount} extensions exhausted with correctness issues still pending — escalating to human`);
1192
+ return { paused: true, sessionId: session.id, question: `Brain exhausted ${MAX_EXTENSIONS} extensions with correctness/tests still pending. Manual intervention needed.`, context: "brain_extension_cap", pending };
1193
+ }
1194
+ brainCtx.extensionCount += 1;
1195
+ logger.info(`Brain: max_iterations reached with ${entries.filter(e => ["correctness", "tests"].includes(e.category)).length} correctness issue(s) pending — extending iterations (extension ${brainCtx.extensionCount}/${MAX_EXTENSIONS})`);
1196
+ session.reviewer_retry_count = 0;
1197
+ await saveSession(session);
1198
+ return { approved: false, sessionId: session.id, reason: "max_iterations_extended", extraIterations: Math.ceil(config.max_iterations / 2) };
1199
+ }
1200
+
1201
+ if (entries.length === 0) {
1202
+ // Brain: no pending feedback → last reviewer approved, finalize
1203
+ logger.info("Brain: max_iterations reached with clean feedback queue — finalizing as approved");
1204
+ return { approved: true, sessionId: session.id, reason: "brain_approved" };
1205
+ }
1206
+
1207
+ // hasStyleOnly: genuine dilemma → Brain consults Solomon
1208
+ logger.info(`Brain: max_iterations with ${entries.length} style-only issue(s) — consulting Solomon on dilemma`);
1209
+ const { invokeSolomon: invokeSolomonAI } = await import("./solomon-escalation.js");
1210
+ const solomonResult = await invokeSolomonAI({
1211
+ config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
1212
+ iteration: config.max_iterations,
1213
+ conflict: {
1214
+ stage: "brain-max-iterations",
1215
+ task,
1216
+ iterationCount: config.max_iterations,
1217
+ maxIterations: config.max_iterations,
1218
+ budget_usd: budget?.total_cost_usd || 0,
1219
+ dilemma: `Max iterations reached with ${entries.length} style-only issue(s) pending. Accept as-is or request more work?`,
1220
+ pendingIssues: pending,
1221
+ history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached" }]
1222
+ }
1223
+ });
1224
+ // Brain applies Solomon's decision
1225
+ if (solomonResult.action === "approve") {
1226
+ logger.info("Brain: Solomon advised approve for style-only pending — finalizing");
1227
+ return { approved: true, sessionId: session.id, reason: "brain_solomon_approved" };
1228
+ }
1229
+ if (solomonResult.action === "continue") {
1230
+ return { approved: false, sessionId: session.id, reason: "max_iterations_extended", extraIterations: solomonResult.extraIterations || Math.ceil(config.max_iterations / 2) };
1231
+ }
1232
+ if (solomonResult.action === "pause") {
1233
+ return { paused: true, sessionId: session.id, question: solomonResult.question, context: "brain_solomon_dilemma" };
1234
+ }
1235
+ // Fallback: escalate to human
1236
+ return { paused: true, sessionId: session.id, question: `Brain+Solomon cannot resolve: ${entries.length} pending issue(s) at max_iterations`, context: "max_iterations" };
1237
+ }
1238
+
1239
+ // Legacy path (Brain disabled): original Solomon-driven flow
1240
+ const solomonResult = await invokeSolomon({
1241
+ config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
1242
+ iteration: config.max_iterations,
1243
+ conflict: {
1244
+ stage: "max_iterations",
1245
+ task,
1246
+ iterationCount: config.max_iterations,
1247
+ maxIterations: config.max_iterations,
1248
+ budget_usd: budget?.total_cost_usd || 0,
1249
+ history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached without reviewer approval" }]
1250
+ }
1251
+ });
1252
+
1253
+ if (solomonResult.action === "approve") {
1254
+ logger.info("Solomon approved coder's work at max_iterations checkpoint");
1255
+ return { approved: true, sessionId: session.id, reason: "solomon_approved" };
1256
+ }
1257
+
1258
+ if (solomonResult.action === "continue") {
1259
+ if (solomonResult.humanGuidance) {
1260
+ session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
1261
+ }
1262
+ session.reviewer_retry_count = 0;
1263
+ await saveSession(session);
1264
+ const extraIterations = solomonResult.extraIterations || config.max_iterations;
1265
+ return { approved: false, sessionId: session.id, reason: "max_iterations_extended", humanGuidance: solomonResult.humanGuidance, extraIterations };
1266
+ }
1267
+
1268
+ if (solomonResult.action === "pause") {
1269
+ return { paused: true, sessionId: session.id, question: solomonResult.question, context: "max_iterations" };
1270
+ }
1271
+
1272
+ if (solomonResult.action === "subtask") {
1273
+ return { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "max_iterations_subtask" };
1274
+ }
1275
+
1276
+ // Solomon couldn't resolve — fail
1277
+ session.budget = budgetSummary();
1278
+ const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
1279
+ if (rtkSavings) session.rtk_savings = rtkSavings;
1280
+ await markSessionStatus(session, "failed");
1281
+ const failDetail = { approved: false, reason: "max_iterations", iterations: config.max_iterations, stages: stageResults, budget: budgetSummary() };
1282
+ if (rtkSavings) failDetail.rtk_savings = rtkSavings;
1283
+ emitProgress(
1284
+ emitter,
1285
+ makeEvent("session:end", { ...eventBase, stage: "done" }, {
1286
+ status: "fail",
1287
+ message: "Max iterations reached (Solomon could not resolve)",
1288
+ detail: failDetail
1289
+ })
1290
+ );
1291
+ return { approved: false, sessionId: session.id, reason: "max_iterations" };
1292
+ }
1293
+
1294
+ async function tryAutoStartBoard(config, logger, emitter, eventBase) {
1295
+ // TSK-0273: gate on hu_board.auto_start alone. The previous double-gate
1296
+ // (enabled && auto_start) was confusing because `enabled` was never the
1297
+ // user-facing switch — only auto_start was. This matches the AC: "kj run
1298
+ // auto-inicia el board server si hu_board.auto_start es true".
1299
+ if (!config.hu_board?.auto_start) return;
1300
+ // Never auto-start during vitest: races the PID file and starts a detached
1301
+ // process that outlives the test run.
1302
+ if (process.env.VITEST || process.env.NODE_ENV === "test") return;
1303
+
1304
+ try {
1305
+ const { startBoard, renderBoardBanner } = await import("../commands/board.js");
1306
+ const boardPort = config.hu_board.port || 4000;
1307
+ const boardResult = await startBoard(boardPort);
1308
+ const status = boardResult.alreadyRunning ? "already running" : "started";
1309
+ // Highlighted URL box — visible regardless of log level (matches the
1310
+ // post-planner auto-HU banner so the user gets the same signal either
1311
+ // way the board got started).
1312
+ console.log(renderBoardBanner({ url: boardResult.url, status }));
1313
+ logger.info(`HU Board ${status} at ${boardResult.url}`);
1314
+ emitProgress(emitter, makeEvent("board:started", eventBase, {
1315
+ message: `HU Board running at ${boardResult.url}`,
1316
+ detail: { pid: boardResult.pid, port: boardPort, url: boardResult.url }
1317
+ }));
1318
+ } catch (err) {
1319
+ logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
1320
+ }
1321
+ }
1322
+
1323
+ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
1324
+ // Auto-init .karajan/ if missing (copies coder-rules, review-rules, role templates)
1325
+ const initProjectDir = config.projectDir || process.cwd();
1326
+ await autoInit(initProjectDir, logger);
1327
+
1328
+ // Smart role assignment: detect installed AIs and assign to roles
1329
+ // Only runs if: (a) no roles configured AND (b) not in test environment
1330
+ const needsAssignment = !config.roles?.coder?.provider && !config.coder && process.env.NODE_ENV !== "test" && !process.env.VITEST;
1331
+ if (needsAssignment) {
1332
+ try {
1333
+ const { autoAssignRoles, applyRoleAssignments } = await import("../utils/role-assigner.js");
1334
+ const { assignments } = await autoAssignRoles(logger);
1335
+ if (assignments) config = applyRoleAssignments({ ...config }, assignments);
1336
+ } catch { /* non-blocking — defaults will be used */ }
1337
+ }
1338
+
1339
+ // Scope all git diffs to projectDir (prevents leaking unrelated branch changes)
1340
+ // When running from a subdirectory of a git repo, use relative path as scope
1341
+ let diffScope = config.projectDir || null;
1342
+ if (!diffScope) {
1343
+ try {
1344
+ const { execSync } = await import("node:child_process");
1345
+ const repoRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
1346
+ const cwd = process.cwd();
1347
+ if (cwd !== repoRoot && cwd.startsWith(repoRoot)) {
1348
+ diffScope = cwd.slice(repoRoot.length + 1);
1349
+ logger.info(`Running from subdirectory — diff scoped to ${diffScope}/`);
1350
+ }
1351
+ } catch { /* git not available */ }
1352
+ }
1353
+ setDiffProjectDir(diffScope);
1354
+
1355
+ // Auto-detect Chrome DevTools MCP
1356
+ const { detectDevToolsMcp } = await import("../webperf/devtools-detect.js");
1357
+ const devToolsAvailable = await detectDevToolsMcp(logger);
1358
+ if (devToolsAvailable) {
1359
+ config = { ...config, webperf: { ...config.webperf, devtools_mcp: true } };
1360
+ }
1361
+
1362
+ const ctx = new PipelineContext({ config, session: null, logger, emitter, task, flags });
1363
+ ctx.askQuestion = askQuestion;
1364
+ ctx.pgTaskId = pgTaskId;
1365
+ ctx.pgProject = pgProject;
1366
+
1367
+ ctx.coderRole = resolveRole(config, "coder");
1368
+ ctx.reviewerRole = resolveRole(config, "reviewer");
1369
+ ctx.refactorerRole = resolveRole(config, "refactorer");
1370
+ ctx.pipelineFlags = resolvePipelineFlags(config);
1371
+ ctx.repeatDetector = new RepeatDetector({ threshold: getRepeatThreshold(config) });
1372
+ ctx.coderRoleInstance = new CoderRole({ config, logger, emitter, createAgentFn: createAgent, askHost: askQuestion });
1373
+ ctx.startedAt = Date.now();
1374
+ ctx.eventBase = { sessionId: null, iteration: 0, stage: null, startedAt: ctx.startedAt };
1375
+ const { budgetTracker, budgetLimit, budgetSummary, trackBudget } = createBudgetManager({
1376
+ config,
1377
+ emitter,
1378
+ eventBase: ctx.eventBase,
1379
+ // Provides fresh compression data for "With KJ vs Without KJ" comparison
1380
+ // (KJC-TSK-0274). Invoked on every budgetSummary() call.
1381
+ getCompressionStats: () => ({
1382
+ rtkSavings: ctx.rtkTracker?.hasData() ? ctx.rtkTracker.summary() : (ctx.session?.rtk_savings || null),
1383
+ brainCtx: ctx.brainCtx || null,
1384
+ }),
1385
+ });
1386
+ ctx.budgetTracker = budgetTracker;
1387
+ ctx.budgetLimit = budgetLimit;
1388
+ ctx.budgetSummary = budgetSummary;
1389
+ ctx.trackBudget = trackBudget;
1390
+
1391
+ // --- RTK detection ---
1392
+ const rtkResult = await detectRtk();
1393
+ if (rtkResult.available) {
1394
+ config = { ...config, rtk: { available: true, version: rtkResult.version } };
1395
+ const rtkTracker = new RtkSavingsTracker();
1396
+ const rtkRunner = createRtkRunner(true, rtkTracker);
1397
+ setDiffRunner(rtkRunner);
1398
+ setGitRunner(rtkRunner);
1399
+ ctx.rtkTracker = rtkTracker;
1400
+ logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
1401
+ emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
1402
+ message: "RTK detected — internal commands wrapped for token optimization",
1403
+ detail: { version: rtkResult.version, executorType: "local" }
1404
+ }));
1405
+ }
1406
+
1407
+ // --- HU Board auto-start ---
1408
+ await tryAutoStartBoard(config, logger, emitter, ctx.eventBase);
1409
+
1410
+ // --- Product Context ---
1411
+ const ctxProjectDir = config.projectDir || process.cwd();
1412
+ const { content: productContext, source: productContextSource } = await loadProductContext(ctxProjectDir);
1413
+ if (productContext) {
1414
+ config = { ...config, productContext };
1415
+ logger.info(`Product context loaded from ${productContextSource}`);
1416
+ emitProgress(emitter, makeEvent("context:loaded", ctx.eventBase, {
1417
+ message: "Product context loaded",
1418
+ detail: { source: productContextSource }
1419
+ }));
1420
+ }
1421
+
1422
+ ctx.session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
1423
+ ctx.eventBase.sessionId = ctx.session.id;
1424
+
1425
+ // Karajan Brain: initialize runtime context (opt-in via config.brain.enabled)
1426
+ const { createBrainContext, isBrainEnabled } = await import("./brain-coordinator.js");
1427
+ ctx.brainCtx = createBrainContext({ enabled: isBrainEnabled(config) });
1428
+ if (ctx.brainCtx.enabled) {
1429
+ logger.info("Karajan Brain enabled — feedback queue, verification, compression active");
1430
+ }
1431
+
1432
+ const { initPgAdapter } = await import("../planning-game/pipeline-adapter.js");
1433
+ const pgAdapterResult = await initPgAdapter({ session: ctx.session, config, logger, pgTaskId, pgProject });
1434
+ ctx.pgCard = pgAdapterResult.pgCard;
1435
+ ctx.session.pg_card = ctx.pgCard || null;
1436
+
1437
+ emitProgress(
1438
+ emitter,
1439
+ makeEvent("session:start", ctx.eventBase, {
1440
+ message: "Session started",
1441
+ detail: { task, coder: ctx.coderRole.provider, reviewer: ctx.reviewerRole.provider, maxIterations: config.max_iterations }
1442
+ })
1443
+ );
1444
+
1445
+ ctx.stageResults = {};
1446
+ ctx.sonarState = { issuesInitial: null, issuesFinal: null };
1447
+
1448
+ const preLoopResult = await runPreLoopStages({ config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session, flags, pipelineFlags: ctx.pipelineFlags, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults: ctx.stageResults, brainCtx: ctx.brainCtx });
1449
+ ctx.plannedTask = preLoopResult.plannedTask;
1450
+ ctx.config = preLoopResult.updatedConfig;
1451
+
1452
+ // --- Session Journal: persist pre-loop outputs + display plan summary ---
1453
+ const reportDir = ctx.config.output?.report_dir || ".reviews";
1454
+ try {
1455
+ ctx.journalDir = await createJournalDir(reportDir, ctx.session.id);
1456
+ const journalFiles = await writePreLoopJournal(ctx.journalDir, ctx.stageResults);
1457
+ ctx.journalFiles = journalFiles;
1458
+ ctx.journalIterations = [];
1459
+ ctx.journalDecisions = [];
1460
+
1461
+ // Attach journal state to session so finalizeApprovedSession can access it
1462
+ ctx.session._journalDir = ctx.journalDir;
1463
+ ctx.session._journalFiles = journalFiles;
1464
+ ctx.session._journalIterations = ctx.journalIterations;
1465
+ ctx.session._journalDecisions = ctx.journalDecisions;
1466
+ ctx.session._startedAt = ctx.startedAt;
1467
+
1468
+ // Display plan summary in console before iteration loop
1469
+ const planSummary = buildPlanSummary({
1470
+ pipelineFlags: ctx.pipelineFlags,
1471
+ config: ctx.config,
1472
+ stageResults: ctx.stageResults,
1473
+ task
1474
+ });
1475
+ console.log(planSummary);
1476
+ } catch (err) {
1477
+ logger.warn(`Journal init failed (non-blocking): ${err.message}`);
1478
+ ctx.journalDir = null;
1479
+ ctx.journalFiles = [];
1480
+ ctx.journalIterations = [];
1481
+ ctx.journalDecisions = [];
1482
+ }
1483
+
1484
+ ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session });
1485
+ const projectDir = ctx.config.projectDir || process.cwd();
1486
+ ctx.reviewRules = (await resolveReviewProfile({ mode: ctx.config.review_mode, projectDir })).rules;
1487
+ await ctx.coderRoleInstance.init();
1488
+
1489
+ return ctx;
1490
+ }
1491
+
1492
+ async function runSingleIteration(ctx) {
1493
+ // Use plannedTask (HU-scoped or planner-enriched) over the raw original task.
1494
+ // When running per-HU sub-pipelines, plannedTask is the HU's text, not the full spec.
1495
+ const { config, logger, emitter, eventBase, session, iteration: i } = ctx;
1496
+ const task = ctx.plannedTask || ctx.task;
1497
+
1498
+ const iterStart = Date.now();
1499
+ const ciEnabled = Boolean(config.ci?.enabled) && ctx.gitCtx?.enabled;
1500
+ logger.setContext({ iteration: i, stage: "iteration" });
1501
+
1502
+ const reviewerRetryCount = session.reviewer_retry_count || 0;
1503
+ const maxReviewerRetries = config.session.max_reviewer_retries ?? config.session.fail_fast_repeats;
1504
+ const iterLang = getLang(config);
1505
+ const iterMsg = msg("pipeline_iteration", iterLang, { current: i, max: config.max_iterations });
1506
+ emitProgress(emitter, makeEvent("iteration:start", { ...eventBase, stage: "iteration" }, {
1507
+ message: iterMsg,
1508
+ detail: { iteration: i, maxIterations: config.max_iterations, reviewerRetryCount, maxReviewerRetries }
1509
+ }));
1510
+ logger.info(iterMsg);
1511
+
1512
+ const crResult = await runCoderAndRefactorerStages({
1513
+ coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole, refactorerRole: ctx.refactorerRole,
1514
+ pipelineFlags: ctx.pipelineFlags, config, logger, emitter, eventBase, session,
1515
+ plannedTask: ctx.plannedTask, trackBudget: ctx.trackBudget, i, brainCtx: ctx.brainCtx
1516
+ });
1517
+ if (crResult.action === "return" || crResult.action === "retry") return crResult;
1518
+
1519
+ const guardResult = await runGuardStages({ config, logger, emitter, eventBase, session, iteration: i });
1520
+ if (guardResult.action === "return") return guardResult;
1521
+
1522
+ const qgResult = await runQualityGateStages({
1523
+ config, logger, emitter, eventBase, session, trackBudget: ctx.trackBudget, i,
1524
+ askQuestion: ctx.askQuestion, repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary,
1525
+ sonarState: ctx.sonarState, task, stageResults: ctx.stageResults, coderRole: ctx.coderRole,
1526
+ pipelineFlags: ctx.pipelineFlags, brainCtx: ctx.brainCtx
1527
+ });
1528
+ if (qgResult.action === "return" || qgResult.action === "continue") return qgResult;
1529
+
1530
+ await handleCiEarlyPrOrPush({
1531
+ ciEnabled, config, session, emitter, eventBase, gitCtx: ctx.gitCtx, task, logger,
1532
+ stageResults: ctx.stageResults, i
1533
+ });
1534
+
1535
+ const revResult = await runReviewerGateStage({
1536
+ pipelineFlags: ctx.pipelineFlags, reviewerRole: ctx.reviewerRole, config, logger, emitter, eventBase,
1537
+ session, trackBudget: ctx.trackBudget, i, reviewRules: ctx.reviewRules, task,
1538
+ repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary, askQuestion: ctx.askQuestion,
1539
+ brainCtx: ctx.brainCtx
1540
+ });
1541
+ if (revResult.action === "return" || revResult.action === "retry") return revResult;
1542
+ const review = revResult.review;
1543
+
1544
+ const iterDuration = Date.now() - iterStart;
1545
+ emitProgress(emitter, makeEvent("iteration:end", { ...eventBase, stage: "iteration" }, {
1546
+ message: `Iteration ${i} completed`, detail: { duration: iterDuration }
1547
+ }));
1548
+ session.standby_retry_count = 0;
1549
+
1550
+ // --- Journal: record iteration ---
1551
+ // Uses the richer iteration-logger from TSK-0286: captures blocking-issue
1552
+ // details, sonar counts and Solomon rulings in addition to summaries.
1553
+ if (ctx.journalIterations) {
1554
+ try {
1555
+ const { recordIteration, extractIterationData } = await import("../session/journal/iteration-logger.js");
1556
+ const iterationData = extractIterationData({
1557
+ iteration: i,
1558
+ durationMs: iterDuration,
1559
+ stageResults: ctx.stageResults,
1560
+ session: ctx.session,
1561
+ });
1562
+ // Derive a richer reviewer summary if none was produced.
1563
+ if (!iterationData.reviewerSummary && review) {
1564
+ iterationData.reviewerSummary = review.approved
1565
+ ? `Approved: ${review.raw_summary || ""}`
1566
+ : `Rejected: ${(review?.blocking_issues || []).length} blocking issue(s)`;
1567
+ }
1568
+ if (!iterationData.blockingIssues || iterationData.blockingIssues.length === 0) {
1569
+ iterationData.blockingIssues = review?.blocking_issues || [];
1570
+ }
1571
+ recordIteration(ctx.session, iterationData);
1572
+ } catch (err) {
1573
+ logger.warn(`Iteration journal record failed (non-blocking): ${err.message}`);
1574
+ }
1575
+ }
1576
+
1577
+ const solomonResult = await handleSolomonCheck({
1578
+ config, session, emitter, eventBase, logger, task, i, askQuestion: ctx.askQuestion,
1579
+ ciEnabled, blockingIssues: review?.blocking_issues, brainCtx: ctx.brainCtx
1580
+ });
1581
+ if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
1582
+
1583
+ await handleCiReviewDispatch({ ciEnabled, config, session, review, i, logger });
1584
+
1585
+ if (review.approved) {
1586
+ const approvedResult = await handleApprovedReview({
1587
+ config, session, emitter, eventBase, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, i, task,
1588
+ stageResults: ctx.stageResults, pipelineFlags: ctx.pipelineFlags, askQuestion: ctx.askQuestion, logger,
1589
+ gitCtx: ctx.gitCtx, budgetSummary: ctx.budgetSummary, pgCard: ctx.pgCard, pgProject: ctx.pgProject, review,
1590
+ rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx
1591
+ });
1592
+ if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
1593
+ }
1594
+
1595
+ // Solomon already evaluated the rejection in runReviewerStage -> handleReviewerRejection
1596
+ // Only use retry counter as fallback if Solomon is disabled
1597
+ if (!config.pipeline?.solomon?.enabled) {
1598
+ const retryResult = await handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion: ctx.askQuestion });
1599
+ if (retryResult.action === "return") return retryResult;
1600
+ } else {
1601
+ // Solomon is enabled — feed back the blocking issues for the next coder iteration
1602
+ session.last_reviewer_feedback = review.blocking_issues
1603
+ .map((x) => {
1604
+ const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
1605
+ if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
1606
+ if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
1607
+ return parts.join("\n");
1608
+ })
1609
+ .join("\n\n");
1610
+ await saveSession(session);
1611
+ }
1612
+
1613
+ return { action: "next" };
1614
+ }
1615
+
1616
+ async function writeHistoryRecord({ sessionId, task, result, logger }) {
1617
+ try {
1618
+ const { createHistoryRecord } = await import("../hu/store.js");
1619
+ const approved = Boolean(result?.approved);
1620
+ const summary = result?.review?.summary || result?.reason || null;
1621
+ await createHistoryRecord(sessionId, {
1622
+ task,
1623
+ result: JSON.stringify(result),
1624
+ approved,
1625
+ summary
1626
+ });
1627
+ } catch (err) {
1628
+ logger.warn(`HU history record failed (non-blocking): ${err.message}`);
1629
+ }
1630
+ }
1631
+
1632
+ /**
1633
+ * Run the standard iteration loop for a given task using the pipeline context.
1634
+ * Returns a result object with at least { approved: boolean }.
1635
+ * Used both as the main loop and as the per-HU callback in sub-pipeline mode.
1636
+ */
1637
+ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, logger }) {
1638
+ ctx.plannedTask = loopTask;
1639
+
1640
+ const checkpointIntervalMs = (ctx.config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
1641
+ let lastCheckpointAt = Date.now();
1642
+ let checkpointDisabled = false;
1643
+ let lastCheckpointSnapshot = takeCheckpointSnapshot(ctx.session);
1644
+
1645
+ let i = 0;
1646
+ while (i < ctx.config.max_iterations) {
1647
+ i += 1;
1648
+ const elapsedMinutes = (Date.now() - ctx.startedAt) / 60000;
1649
+
1650
+ const cpResult = await handleCheckpoint({
1651
+ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
1652
+ i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary, lastCheckpointSnapshot
1653
+ });
1654
+ if (cpResult.action === "stop") {
1655
+ return cpResult.result;
1656
+ }
1657
+ checkpointDisabled = cpResult.checkpointDisabled;
1658
+ lastCheckpointAt = cpResult.lastCheckpointAt;
1659
+ if (cpResult.lastCheckpointSnapshot !== undefined) lastCheckpointSnapshot = cpResult.lastCheckpointSnapshot;
1660
+
1661
+ await checkSessionTimeout({ askQuestion, elapsedMinutes, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
1662
+ await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
1663
+
1664
+ ctx.eventBase.iteration = i;
1665
+ ctx.iteration = i;
1666
+
1667
+ let iterResult;
1668
+ try {
1669
+ iterResult = await runSingleIteration(ctx);
1670
+ } catch (stageError) {
1671
+ // ANY unhandled error in a stage = out of normal flow → Solomon decides
1672
+ logger.warn(`Stage error caught — escalating to Solomon: ${stageError.message}`);
1673
+ const solomonResult = await invokeSolomon({
1674
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
1675
+ stage: "stage_error", askQuestion, session: ctx.session, iteration: i,
1676
+ conflict: {
1677
+ stage: "stage_error",
1678
+ task: loopTask,
1679
+ iterationCount: i,
1680
+ maxIterations: ctx.config.max_iterations,
1681
+ budget_usd: ctx.budgetSummary()?.total_cost_usd || 0,
1682
+ history: [{ agent: "pipeline", feedback: `Stage threw: ${stageError.message}` }]
1683
+ }
1684
+ });
1685
+
1686
+ if (solomonResult.action === "approve") {
1687
+ logger.info("Solomon approved despite stage error");
1688
+ return { approved: true, sessionId: ctx.session.id, reason: "solomon_approved_after_error" };
1689
+ }
1690
+ if (solomonResult.action === "continue") {
1691
+ if (solomonResult.humanGuidance) {
1692
+ ctx.session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
1693
+ }
1694
+ continue; // next iteration
1695
+ }
1696
+ if (solomonResult.action === "pause") {
1697
+ return { paused: true, sessionId: ctx.session.id, question: solomonResult.question, context: "stage_error" };
1698
+ }
1699
+ // Solomon couldn't resolve — fail
1700
+ await markSessionStatus(ctx.session, "failed");
1701
+ return { approved: false, sessionId: ctx.session.id, reason: "stage_error", error: stageError.message };
1702
+ }
1703
+
1704
+ if (iterResult.action === "return") {
1705
+ return iterResult.result;
1706
+ }
1707
+ if (iterResult.action === "retry") { i -= 1; }
1708
+ }
1709
+
1710
+ // Solomon decides whether to extend iterations or stop
1711
+ const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx });
1712
+
1713
+ // Solomon said "continue" — extend iterations and keep going
1714
+ if (maxIterResult.reason === "max_iterations_extended") {
1715
+ const extra = maxIterResult.extraIterations || ctx.config.max_iterations;
1716
+ ctx.config.max_iterations += extra;
1717
+ logger.info(`Solomon extended pipeline by ${extra} iterations (new max: ${ctx.config.max_iterations})`);
1718
+
1719
+ if (maxIterResult.humanGuidance) {
1720
+ ctx.session.last_reviewer_feedback = `Solomon guidance: ${maxIterResult.humanGuidance}`;
1721
+ }
1722
+
1723
+ // Continue the loop
1724
+ while (i < ctx.config.max_iterations) {
1725
+ i += 1;
1726
+ const elapsedMinutes = (Date.now() - ctx.startedAt) / 60000;
1727
+
1728
+ const cpResult = await handleCheckpoint({
1729
+ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
1730
+ i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary, lastCheckpointSnapshot
1731
+ });
1732
+ if (cpResult.action === "stop") return cpResult.result;
1733
+ checkpointDisabled = cpResult.checkpointDisabled;
1734
+ lastCheckpointAt = cpResult.lastCheckpointAt;
1735
+ if (cpResult.lastCheckpointSnapshot !== undefined) lastCheckpointSnapshot = cpResult.lastCheckpointSnapshot;
1736
+
1737
+ await checkSessionTimeout({ askQuestion, elapsedMinutes, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
1738
+ await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
1739
+
1740
+ ctx.eventBase.iteration = i;
1741
+ ctx.iteration = i;
1742
+
1743
+ const iterResult = await runSingleIteration(ctx);
1744
+ if (iterResult.action === "return") return iterResult.result;
1745
+ if (iterResult.action === "retry") { i -= 1; }
1746
+ }
1747
+
1748
+ // Extended iterations also exhausted — final Solomon call
1749
+ const finalResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx });
1750
+ return finalResult;
1751
+ }
1752
+
1753
+ return maxIterResult;
1754
+ }
1755
+
1756
+ export async function runFlow({ task, config, logger, flags = {}, emitter = null, askQuestion = null, pgTaskId = null, pgProject = null }) {
1757
+ const pipelineFlags = resolvePipelineFlags(config);
1758
+
1759
+ if (flags.dryRun) {
1760
+ return handleDryRun({ task, config, flags, emitter, pipelineFlags });
1761
+ }
1762
+
1763
+ let ctx;
1764
+ try {
1765
+ ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
1766
+ } catch (initError) {
1767
+ // Pre-loop stage failure → Solomon decides
1768
+ logger.warn(`Init/pre-loop error — escalating to Solomon: ${initError.message}`);
1769
+ const tempSession = { id: "init-error", task, status: "failed" };
1770
+ const solomonResult = await invokeSolomon({
1771
+ config, logger, emitter, eventBase: { sessionId: "init-error", iteration: 0, stage: "init", startedAt: Date.now() },
1772
+ stage: "init_error", askQuestion, session: tempSession, iteration: 0,
1773
+ conflict: {
1774
+ stage: "init_error",
1775
+ task,
1776
+ iterationCount: 0,
1777
+ maxIterations: config.max_iterations || 5,
1778
+ history: [{ agent: "pipeline", feedback: `Initialization failed: ${initError.message}` }]
1779
+ }
1780
+ });
1781
+ if (solomonResult.action === "pause") {
1782
+ return { paused: true, sessionId: "init-error", question: solomonResult.question, context: "init_error" };
1783
+ }
1784
+ throw initError; // Solomon couldn't resolve — propagate
1785
+ }
1786
+
1787
+ try {
1788
+ // --- Analysis-only flow: skip coder/reviewer when coderRequired === false ---
1789
+ if (ctx.pipelineFlags.coderRequired === false) {
1790
+ logger.info("Analysis-only task — skipping coder/reviewer iteration loop");
1791
+ emitProgress(emitter, makeEvent("pipeline:analysis-only", { ...ctx.eventBase, stage: "analysis" }, {
1792
+ message: "Analysis-only task — running security and audit stages only",
1793
+ detail: { taskType: ctx.session.resolved_policies?.taskType, coderRequired: false }
1794
+ }));
1795
+
1796
+ const analysisStageResults = ctx.stageResults;
1797
+ const postLoopDiff = await generateDiff({ baseRef: ctx.session.session_start_sha });
1798
+
1799
+ if (ctx.pipelineFlags.securityEnabled) {
1800
+ const securityResult = await runSecurityStage({
1801
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session,
1802
+ coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
1803
+ iteration: 1, task: ctx.plannedTask, diff: postLoopDiff, askQuestion, brainCtx: ctx.brainCtx
1804
+ });
1805
+ if (securityResult.stageResult) analysisStageResults.security = securityResult.stageResult;
1806
+ }
1807
+
1808
+ const auditResult = await runFinalAuditStage({
1809
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session,
1810
+ coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
1811
+ iteration: 1, task: ctx.plannedTask, diff: postLoopDiff
1812
+ });
1813
+ if (auditResult.stageResult) analysisStageResults.audit = auditResult.stageResult;
1814
+
1815
+ ctx.session.budget = ctx.budgetSummary();
1816
+ await markSessionStatus(ctx.session, "approved");
1817
+
1818
+ const analysisResult = {
1819
+ approved: true,
1820
+ sessionId: ctx.session.id,
1821
+ analysisOnly: true,
1822
+ stages: analysisStageResults,
1823
+ budget: ctx.budgetSummary()
1824
+ };
1825
+ await writeHistoryRecord({ sessionId: ctx.session.id, task, result: analysisResult, logger });
1826
+
1827
+ emitProgress(emitter, makeEvent("session:end", { ...ctx.eventBase, stage: "done" }, {
1828
+ message: "Analysis-only session completed",
1829
+ detail: analysisResult
1830
+ }));
1831
+
1832
+ return analysisResult;
1833
+ }
1834
+
1835
+ // --- HU Sub-Pipeline: run each certified HU as an independent iteration loop ---
1836
+ if (needsSubPipeline(ctx.stageResults.huReviewer)) {
1837
+ logger.info(`HU sub-pipeline: ${ctx.stageResults.huReviewer.certified} certified stories — running each as a sub-pipeline`);
1838
+ emitProgress(emitter, makeEvent("hu:sub-pipeline:start", { ...ctx.eventBase, stage: "hu-sub-pipeline" }, {
1839
+ message: `Running ${ctx.stageResults.huReviewer.certified} HUs as sub-pipelines`,
1840
+ detail: { total: ctx.stageResults.huReviewer.total, certified: ctx.stageResults.huReviewer.certified }
1841
+ }));
1842
+
1843
+ // Per-HU pipeline: focused max_iterations, fresh Brain state, own git branch.
1844
+ const originalMaxIterations = ctx.config.max_iterations;
1845
+ const huMaxIterations = ctx.config.hu_max_iterations ?? 3;
1846
+ const huBranches = new Map();
1847
+ const { prepareHuBranch, finalizeHuCommit } = await import("../git/hu-automation.js");
1848
+ const subPipelineResult = await runHuSubPipeline({
1849
+ huReviewerResult: ctx.stageResults.huReviewer,
1850
+ runIterationFn: async (huTask, story) => {
1851
+ ctx.config.max_iterations = huMaxIterations;
1852
+ if (ctx.brainCtx?.enabled) {
1853
+ ctx.brainCtx.extensionCount = 0;
1854
+ const { createBrainContext } = await import("./brain-coordinator.js");
1855
+ const fresh = createBrainContext({ enabled: true });
1856
+ ctx.brainCtx.feedbackQueue = fresh.feedbackQueue;
1857
+ ctx.brainCtx.verificationTracker = fresh.verificationTracker;
1858
+ }
1859
+ // Apply per-HU policies based on task_type (infra skips reviewer/sonar/tdd)
1860
+ const { applyPolicies } = await import("../guards/policy-resolver.js");
1861
+ const huPolicies = applyPolicies({ taskType: story.task_type, policies: ctx.config.policies });
1862
+ const savedFlags = { ...ctx.pipelineFlags };
1863
+ if (!huPolicies.reviewer) ctx.pipelineFlags.reviewerEnabled = false;
1864
+ if (!huPolicies.tdd) ctx.config.development = { ...ctx.config.development, methodology: "standard", require_test_changes: false };
1865
+ if (!huPolicies.sonar) ctx.config.sonarqube = { ...ctx.config.sonarqube, enabled: false };
1866
+ if (!huPolicies.testsRequired) ctx.pipelineFlags.testerEnabled = false;
1867
+ logger.info(`HU ${story.id} (${story.task_type}): policies → reviewer=${huPolicies.reviewer}, tdd=${huPolicies.tdd}, sonar=${huPolicies.sonar}, tests=${huPolicies.testsRequired}`);
1868
+
1869
+ const branchName = await prepareHuBranch({ story, huBranches, config: ctx.config, logger });
1870
+ const projectDir = ctx.config.projectDir || process.cwd();
1871
+
1872
+ // If HU has acceptance_tests, Brain runs them as the gate instead of
1873
+ // the standard reviewer/tester pipeline. This is the radical fix:
1874
+ // concrete executable tests replace subjective reviewer opinions.
1875
+ if (story.acceptance_tests?.length > 0) {
1876
+ const { runAcceptanceTests, buildDiagnosticPrompt } = await import("../hu/acceptance-runner.js");
1877
+
1878
+ for (let attempt = 1; attempt <= ctx.config.max_iterations; attempt++) {
1879
+ logger.info(`HU ${story.id}: coder iteration ${attempt}/${ctx.config.max_iterations}`);
1880
+ emitProgress(emitter, makeEvent("iteration:start", { ...ctx.eventBase, stage: "iteration" }, {
1881
+ message: `Iteration ${attempt}/${ctx.config.max_iterations}`,
1882
+ detail: { iteration: attempt, maxIterations: ctx.config.max_iterations }
1883
+ }));
1884
+
1885
+ // Coder runs with the HU task + any diagnostic feedback from previous attempt
1886
+ const coderResult = await runCoderStage({
1887
+ coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole,
1888
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
1889
+ session: ctx.session, plannedTask: ctx.plannedTask,
1890
+ trackBudget: ctx.trackBudget, iteration: attempt, brainCtx: ctx.brainCtx
1891
+ });
1892
+ if (coderResult?.action === "standby" || coderResult?.action === "pause") {
1893
+ return coderResult?.result || { approved: false, reason: "coder_failed" };
1894
+ }
1895
+
1896
+ // Sonar quality gate (sw task_type only, when policies say sonar=true)
1897
+ if (huPolicies.sonar && ctx.config.sonarqube?.enabled) {
1898
+ try {
1899
+ const sonarResult = await runSonarStage({
1900
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
1901
+ session: ctx.session, trackBudget: ctx.trackBudget, iteration: attempt,
1902
+ askQuestion, brainCtx: ctx.brainCtx
1903
+ });
1904
+ if (sonarResult?.action === "continue") {
1905
+ // Sonar failed — add to feedback for next coder attempt
1906
+ ctx.plannedTask = `${huTask}\n\n--- SONAR FAILURE ---\n${ctx.session.last_reviewer_feedback}`;
1907
+ continue;
1908
+ }
1909
+ } catch (err) {
1910
+ logger.warn(`HU ${story.id}: sonar stage failed (non-blocking): ${err.message}`);
1911
+ }
1912
+ }
1913
+
1914
+ // Brain runs acceptance tests
1915
+ logger.info(`HU ${story.id}: running ${story.acceptance_tests.length} acceptance tests`);
1916
+ emitProgress(emitter, makeEvent("hu:acceptance-start", { ...ctx.eventBase, stage: "acceptance" }, {
1917
+ message: `Running ${story.acceptance_tests.length} acceptance tests`,
1918
+ detail: { huId: story.id, testCount: story.acceptance_tests.length }
1919
+ }));
1920
+
1921
+ const testResult = await runAcceptanceTests(story.acceptance_tests, projectDir);
1922
+ emitProgress(emitter, makeEvent("hu:acceptance-end", { ...ctx.eventBase, stage: "acceptance" }, {
1923
+ status: testResult.allPassed ? "ok" : "fail",
1924
+ message: testResult.summary,
1925
+ detail: { allPassed: testResult.allPassed, results: testResult.results.map(r => ({ cmd: r.cmd, passed: r.passed })) }
1926
+ }));
1927
+
1928
+ if (testResult.allPassed) {
1929
+ logger.info(`HU ${story.id}: all acceptance tests PASSED — approved`);
1930
+ await finalizeHuCommit({ story, branchName, config: ctx.config, logger });
1931
+ return { approved: true, sessionId: ctx.session.id, reason: "acceptance_tests_passed" };
1932
+ }
1933
+
1934
+ // Brain diagnoses failures and sends concrete fix to coder
1935
+ const failed = testResult.results.filter(r => !r.passed);
1936
+ const diagnostic = buildDiagnosticPrompt(failed);
1937
+ logger.warn(`HU ${story.id}: ${failed.length} acceptance test(s) FAILED — sending diagnostic to coder`);
1938
+ ctx.session.last_reviewer_feedback = diagnostic;
1939
+ ctx.plannedTask = `${huTask}\n\n--- ACCEPTANCE TEST FAILURES ---\n${diagnostic}`;
1940
+ }
1941
+
1942
+ // All iterations exhausted
1943
+ logger.warn(`HU ${story.id}: max iterations reached with acceptance tests still failing`);
1944
+ return { approved: false, sessionId: ctx.session.id, reason: "acceptance_tests_failed" };
1945
+ }
1946
+
1947
+ // Fallback: no acceptance_tests → standard pipeline (reviewer/tester)
1948
+ try {
1949
+ const result = await runIterationLoop(ctx, { task: huTask, askQuestion, emitter, logger });
1950
+ if (result?.approved) {
1951
+ await finalizeHuCommit({ story, branchName, config: ctx.config, logger });
1952
+ }
1953
+ return result;
1954
+ } finally {
1955
+ ctx.config.max_iterations = originalMaxIterations;
1956
+ Object.assign(ctx.pipelineFlags, savedFlags);
1957
+ }
1958
+ },
1959
+ emitter,
1960
+ eventBase: ctx.eventBase,
1961
+ logger,
1962
+ config: ctx.config
1963
+ });
1964
+
1965
+ emitProgress(emitter, makeEvent("hu:sub-pipeline:end", { ...ctx.eventBase, stage: "hu-sub-pipeline" }, {
1966
+ status: subPipelineResult.approved ? "ok" : "fail",
1967
+ message: subPipelineResult.approved
1968
+ ? "All HUs completed successfully"
1969
+ : `Sub-pipeline finished with failures (${subPipelineResult.blockedIds.length} blocked)`,
1970
+ detail: { results: subPipelineResult.results, blockedIds: subPipelineResult.blockedIds }
1971
+ }));
1972
+
1973
+ // Sync results back to plan file if this was a plan-based run
1974
+ if (ctx.session._syncResultsToPlan) {
1975
+ try {
1976
+ await ctx.session._syncResultsToPlan(subPipelineResult);
1977
+ logger.info("Plan updated with execution results");
1978
+ } catch (err) {
1979
+ logger.warn(`Failed to sync results to plan: ${err.message}`);
1980
+ }
1981
+ }
1982
+
1983
+ const finalResult = {
1984
+ approved: subPipelineResult.approved,
1985
+ sessionId: ctx.session.id,
1986
+ huResults: subPipelineResult.results,
1987
+ blockedIds: subPipelineResult.blockedIds,
1988
+ planId: ctx.session._planRef?.planId || null
1989
+ };
1990
+ await writeHistoryRecord({ sessionId: ctx.session.id, task, result: finalResult, logger });
1991
+ return finalResult;
1992
+ }
1993
+
1994
+ // --- Standard single-task pipeline (1 HU or no HU reviewer) ---
1995
+ const result = await runIterationLoop(ctx, { task: ctx.plannedTask, askQuestion, emitter, logger });
1996
+ await writeHistoryRecord({ sessionId: ctx.session.id, task, result, logger });
1997
+ return result;
1998
+ } finally {
1999
+ // --- Cleanup auto-installed skills ---
2000
+ const autoSkills = ctx.session?.autoInstalledSkills;
2001
+ if (autoSkills && autoSkills.length > 0) {
2002
+ const cleanProjectDir = ctx.config?.projectDir || process.cwd();
2003
+ try {
2004
+ const cleanupResult = await cleanupAutoInstalledSkills(autoSkills, cleanProjectDir);
2005
+ if (cleanupResult.removed.length > 0) {
2006
+ logger.info(`Cleaned up ${cleanupResult.removed.length} auto-installed skill(s): ${cleanupResult.removed.join(", ")}`);
2007
+ }
2008
+ } catch (err) {
2009
+ logger.warn(`Skill cleanup failed (non-blocking): ${err.message}`);
2010
+ }
2011
+ }
2012
+
2013
+ // --- Telemetry: anonymous pipeline_complete event (non-blocking) ---
2014
+ try {
2015
+ const { sendTelemetryEvent } = await import("../utils/telemetry.js");
2016
+ const durationS = Math.round((Date.now() - ctx.startedAt) / 1000);
2017
+ const sessionStatus = ctx.session?.status || "unknown";
2018
+ sendTelemetryEvent("pipeline_complete", {
2019
+ mode: config.review_mode,
2020
+ agent: ctx.coderRole?.provider || config.coder,
2021
+ duration_s: durationS,
2022
+ success: sessionStatus === "approved",
2023
+ taskType: ctx.session?.resolved_policies?.taskType || null
2024
+ }, config).catch(() => {});
2025
+ } catch { /* non-blocking */ }
2026
+ }
2027
+ }
2028
+
2029
+ export async function resumeFlow({ sessionId, answer, config, logger, flags = {}, emitter = null, askQuestion = null }) {
2030
+ const session = answer
2031
+ ? await resumeSessionWithAnswer(sessionId, answer)
2032
+ : await loadSession(sessionId);
2033
+
2034
+ if (session.status === "paused" && !answer) {
2035
+ logger.info(`Session ${sessionId} is paused. Provide --answer to resume.`);
2036
+ return session;
2037
+ }
2038
+
2039
+ // Allow resuming "stopped" sessions (checkpoint stop) and "failed" sessions
2040
+ const resumableStatuses = new Set(["running", "stopped", "failed"]);
2041
+ if (!resumableStatuses.has(session.status)) {
2042
+ logger.info(`Session ${sessionId} has status ${session.status} — not resumable`);
2043
+ return session;
2044
+ }
2045
+
2046
+ // Mark as running again for stopped/failed sessions
2047
+ if (session.status !== "running") {
2048
+ logger.info(`Resuming ${session.status} session ${sessionId}`);
2049
+ session.status = "running";
2050
+ await saveSession(session);
2051
+ }
2052
+
2053
+ // Session was paused and now resumed with answer - re-run the flow
2054
+ const task = session.task;
2055
+ const sessionConfig = config || session.config_snapshot;
2056
+ if (!sessionConfig) {
2057
+ throw new Error("No config available to resume session");
2058
+ }
2059
+
2060
+ logger.info(`Resuming session ${sessionId} with answer: ${answer}`);
2061
+
2062
+ // Inject the answer as additional feedback for the coder
2063
+ if (session.paused_state?.context?.lastFeedback) {
2064
+ session.last_reviewer_feedback = `Previous feedback: ${session.paused_state.context.lastFeedback}\nUser guidance: ${answer}`;
2065
+ }
2066
+ session.repeated_issue_count = 0;
2067
+ session.sonar_retry_count = 0;
2068
+ session.reviewer_retry_count = 0;
2069
+ session.standby_retry_count = 0;
2070
+ session.tester_retry_count = 0;
2071
+ session.security_retry_count = 0;
2072
+ session.last_sonar_issue_signature = null;
2073
+ session.sonar_repeat_count = 0;
2074
+ session.last_reviewer_issue_signature = null;
2075
+ session.reviewer_repeat_count = 0;
2076
+ await saveSession(session);
2077
+
2078
+ // Re-run the flow with the existing session context
2079
+ try {
2080
+ return await runFlow({ task, config: sessionConfig, logger, flags, emitter, askQuestion });
2081
+ } catch (err) {
2082
+ await markSessionStatus(session, "failed");
2083
+ throw err;
2084
+ }
2085
+ }