karajan-code 2.5.0 → 2.6.0

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