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