karajan-code 2.7.4 → 2.9.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 (192) hide show
  1. package/README.md +1 -1
  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/audit/basal-cost.js +92 -9
  10. package/src/audit/deterministic-summary.js +200 -0
  11. package/src/audit/osv-findings.js +162 -0
  12. package/src/audit/semgrep-findings.js +117 -0
  13. package/src/audit/sonar-findings.js +81 -0
  14. package/src/audit/webperf-input.js +90 -0
  15. package/src/bootstrap.js +26 -1
  16. package/src/brain/decision-tracker.js +3 -1
  17. package/src/brain/decisor.js +1 -1
  18. package/src/canvas/parse-md.js +238 -0
  19. package/src/canvas/schema.js +101 -0
  20. package/src/canvas/to-flat.js +85 -0
  21. package/src/canvas/validate.js +163 -0
  22. package/src/checks/binaries.js +3 -3
  23. package/src/checks/ci.js +2 -2
  24. package/src/checks/config-files.js +6 -6
  25. package/src/checks/dir-setup.js +4 -0
  26. package/src/checks/mcp-health.js +6 -0
  27. package/src/checks/ports.js +91 -29
  28. package/src/checks/rtk.js +1 -1
  29. package/src/checks/skills.js +5 -1
  30. package/src/checks/sonar.js +2 -2
  31. package/src/checks/system.js +3 -3
  32. package/src/checks/tokens.js +1 -1
  33. package/src/checks/types.js +0 -12
  34. package/src/cli/_shared.js +30 -0
  35. package/src/cli/register-meta.js +218 -0
  36. package/src/cli/register-pipeline.js +212 -0
  37. package/src/cli/register-plan.js +152 -0
  38. package/src/cli/register-roles-skills.js +38 -0
  39. package/src/cli/register-sonar.js +28 -0
  40. package/src/cli.js +83 -468
  41. package/src/commands/agents.js +18 -6
  42. package/src/commands/architect.js +29 -2
  43. package/src/commands/audit.js +265 -30
  44. package/src/commands/board.js +160 -30
  45. package/src/commands/clean.js +83 -0
  46. package/src/commands/code.js +7 -2
  47. package/src/commands/discover.js +7 -2
  48. package/src/commands/doctor.js +1 -1
  49. package/src/commands/init.js +2 -2
  50. package/src/commands/plan/_shared.js +40 -0
  51. package/src/commands/plan/add-hu.js +21 -0
  52. package/src/commands/plan/delete.js +9 -0
  53. package/src/commands/plan/generate.js +284 -0
  54. package/src/commands/plan/index.js +14 -0
  55. package/src/commands/plan/list.js +21 -0
  56. package/src/commands/plan/ready.js +46 -0
  57. package/src/commands/plan/remove-hu.js +17 -0
  58. package/src/commands/plan/show.js +81 -0
  59. package/src/commands/plan/validate.js +20 -0
  60. package/src/commands/plan.js +14 -291
  61. package/src/commands/researcher.js +7 -2
  62. package/src/commands/resume.js +24 -1
  63. package/src/commands/review.js +7 -2
  64. package/src/commands/run.js +52 -1
  65. package/src/commands/status.js +1 -1
  66. package/src/commands/sync.js +75 -0
  67. package/src/commands/triage.js +7 -2
  68. package/src/commands/undo.js +1 -1
  69. package/src/config/defaults.js +213 -0
  70. package/src/config/loader.js +137 -0
  71. package/src/config/overrides.js +199 -0
  72. package/src/config/role-resolver.js +159 -0
  73. package/src/config/test-harness.js +78 -0
  74. package/src/config.js +30 -632
  75. package/src/git/automation.js +2 -2
  76. package/src/guards/output-guard.js +0 -2
  77. package/src/hu/acceptance-runner.js +52 -10
  78. package/src/hu/store.js +11 -0
  79. package/src/mcp/handlers/direct-handlers.js +2 -5
  80. package/src/mcp/handlers/management-handlers.js +48 -4
  81. package/src/mcp/handlers/run-handler.js +9 -8
  82. package/src/mcp/preflight.js +32 -13
  83. package/src/mcp/server-handlers.js +4 -3
  84. package/src/mcp/suggest-handler.js +3 -2
  85. package/src/mcp/tools.js +15 -0
  86. package/src/orchestrator/agent-fallback.js +1 -1
  87. package/src/orchestrator/ci-integration.js +11 -11
  88. package/src/orchestrator/config-init.js +25 -7
  89. package/src/orchestrator/direct-actions.js +7 -3
  90. package/src/orchestrator/drivers/error-recovery.js +287 -0
  91. package/src/orchestrator/drivers/init-context.js +222 -0
  92. package/src/orchestrator/drivers/iteration-loop.js +301 -0
  93. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
  94. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  95. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  96. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
  97. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  98. package/src/orchestrator/drivers/post-loop.js +370 -0
  99. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
  100. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  101. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  102. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
  103. package/src/orchestrator/drivers/pre-loop.js +431 -0
  104. package/src/orchestrator/drivers/run-hu-batch.js +367 -0
  105. package/src/orchestrator/feedback-queue.js +0 -1
  106. package/src/orchestrator/flow-control.js +4 -3
  107. package/src/orchestrator/flow-runner.js +121 -2077
  108. package/src/orchestrator/fs-leak-detector.js +156 -0
  109. package/src/orchestrator/hu-sub-pipeline.js +197 -9
  110. package/src/orchestrator/integrations.js +81 -0
  111. package/src/orchestrator/post-loop-stages.js +35 -8
  112. package/src/orchestrator/preflight-checks.js +20 -7
  113. package/src/orchestrator/repair/repair-detector.js +117 -0
  114. package/src/orchestrator/repair/repair-runner.js +142 -0
  115. package/src/orchestrator/reviewer-fallback.js +1 -1
  116. package/src/orchestrator/role-output-compressor.js +0 -1
  117. package/src/orchestrator/session-journal.js +4 -41
  118. package/src/orchestrator/solomon-escalation.js +3 -2
  119. package/src/orchestrator/solomon-rules.js +7 -3
  120. package/src/orchestrator/stages/architect-stage.js +12 -11
  121. package/src/orchestrator/stages/coder-stage.js +56 -7
  122. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  123. package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
  124. package/src/orchestrator/stages/planner-stage.js +1 -1
  125. package/src/orchestrator/stages/research-stage.js +1 -1
  126. package/src/orchestrator/stages/reviewer-stage.js +7 -7
  127. package/src/orchestrator/stages/sonar-stage.js +94 -10
  128. package/src/orchestrator/stages/stage-classes.js +84 -0
  129. package/src/orchestrator/stages/stage-executor.js +1 -1
  130. package/src/orchestrator/stages/triage-stage.js +1 -1
  131. package/src/orchestrator/standby.js +3 -2
  132. package/src/orchestrator/verification-gate.js +20 -12
  133. package/src/plan/adr-generator.js +170 -0
  134. package/src/plan/adr-loader.js +60 -0
  135. package/src/plan/plan-executor.js +19 -1
  136. package/src/plan/plan-hu-ops.js +170 -1
  137. package/src/plan/plan-reviewer.js +199 -0
  138. package/src/plan/plan-schema.js +69 -1
  139. package/src/plan/plan-store.js +40 -17
  140. package/src/plan/spec-detector.js +61 -0
  141. package/src/plan/tests-synthesizer.js +116 -0
  142. package/src/planning-game/client.js +3 -28
  143. package/src/planning-game/decomposition.js +1 -1
  144. package/src/planning-game/pipeline-adapter.js +37 -6
  145. package/src/prompts/audit.js +206 -6
  146. package/src/prompts/coder.js +172 -1
  147. package/src/prompts/hu-reviewer.js +0 -1
  148. package/src/prompts/planner.js +81 -4
  149. package/src/prompts/rtk-snippet.js +1 -1
  150. package/src/review/diff-generator.js +35 -22
  151. package/src/review/snapshot-diff.js +1 -1
  152. package/src/roles/audit-role.js +125 -13
  153. package/src/roles/coder-role.js +25 -3
  154. package/src/roles/planner-role.js +42 -1
  155. package/src/roles/repairer-role.js +181 -0
  156. package/src/roles/tester-role.js +70 -4
  157. package/src/session/mutators.js +304 -0
  158. package/src/session/runtime-overrides.js +59 -0
  159. package/src/{session-store.js → session/store.js} +2 -2
  160. package/src/skills/semantic-detector.js +10 -5
  161. package/src/skills/skill-detector.js +1 -1
  162. package/src/sonar/api.js +1 -1
  163. package/src/sonar/cloud-scanner.js +0 -1
  164. package/src/sonar/credentials.js +14 -6
  165. package/src/sonar/discovery.js +123 -0
  166. package/src/sonar/manager.js +18 -0
  167. package/src/sync/detect-drift.js +113 -0
  168. package/src/tracker-bootstrap.js +44 -0
  169. package/src/utils/agent-detect.js +15 -11
  170. package/src/utils/board-prompt-bridge.js +109 -0
  171. package/src/utils/cli-progress.js +161 -0
  172. package/src/utils/cwd-confirm.js +39 -0
  173. package/src/utils/derive-project-name-from-cwd.js +119 -0
  174. package/src/utils/display/event-handlers.js +1 -1
  175. package/src/utils/display/header.js +1 -1
  176. package/src/utils/display/session-summary.js +3 -3
  177. package/src/utils/display/solomon.js +1 -1
  178. package/src/utils/garbage-collector.js +396 -0
  179. package/src/utils/git.js +16 -4
  180. package/src/utils/locale.js +1 -1
  181. package/src/utils/paths.js +21 -1
  182. package/src/utils/process.js +1 -1
  183. package/src/utils/prompt-project-name.js +35 -0
  184. package/src/utils/role-assigner.js +1 -1
  185. package/src/utils/run-context.js +75 -0
  186. package/src/utils/run-log.js +12 -0
  187. package/src/utils/task-file.js +1 -1
  188. package/src/utils/telemetry.js +13 -1
  189. package/src/webperf/devtools-detect.js +1 -1
  190. package/templates/coder-rules.md +13 -0
  191. package/templates/roles/coder.md +34 -0
  192. package/templates/roles/repairer.md +94 -0
@@ -0,0 +1,367 @@
1
+ /**
2
+ * HU sub-pipeline driver — extracted from flow-runner.js in PR-L
3
+ * (audit follow-up). Was 244 LOC inlined inside _runFlowInner, the
4
+ * second-worst long-function per `kj audit`.
5
+ *
6
+ * Behaviour identical to the inlined version — the closure passed to
7
+ * runHuSubPipeline is moved verbatim, only its surrounding context
8
+ * (logger, emitter, ctx, askQuestion) becomes function parameters.
9
+ *
10
+ * The extraction also fixes a latent bug: flow-runner.js called
11
+ * `runCoderStage`, `runSonarStage`, `generateDiff` and
12
+ * `runSecurityStage` without static OR dynamic imports. Those
13
+ * references would ReferenceError at runtime on the HU sub-pipeline
14
+ * path; the fact that no test caught it suggests the path was rarely
15
+ * exercised end-to-end (preflight failures often abort earlier).
16
+ * This module imports them explicitly.
17
+ *
18
+ * Returns:
19
+ * - { handled: true, result } — needsSubPipeline matched; caller
20
+ * must short-circuit and return `result`.
21
+ * - { handled: false } — caller falls through to the standard
22
+ * single-task pipeline (runIterationLoop).
23
+ */
24
+
25
+ import { emitProgress, makeEvent } from "../../utils/events.js";
26
+ import { needsSubPipeline, runHuSubPipeline } from "../hu-sub-pipeline.js";
27
+ import { runCoderStage } from "../stages/coder-stage.js";
28
+ import { runSonarStage } from "../stages/sonar-stage.js";
29
+ import { runIterationLoop } from "./iteration-loop.js";
30
+ import { classifyFailure, createFailureTracker } from "../repair/repair-detector.js";
31
+ import { runRepair } from "../repair/repair-runner.js";
32
+ import { writeHistoryRecord } from "./post-loop.js";
33
+ import { setReviewerFeedback } from "../../session/mutators.js";
34
+
35
+ /**
36
+ * @param {object} args
37
+ * @param {object} args.ctx — PipelineContext from initFlowContext
38
+ * @param {string} args.task — original task description (for writeHistoryRecord)
39
+ * @param {object} args.askQuestion
40
+ * @param {object} args.emitter
41
+ * @param {object} args.logger
42
+ * @returns {Promise<{handled: boolean, result?: object}>}
43
+ */
44
+ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
45
+ if (!needsSubPipeline(ctx.stageResults.huReviewer)) {
46
+ return { handled: false };
47
+ }
48
+
49
+ // --- HU Sub-Pipeline: run each certified HU as an independent iteration loop ---
50
+ logger.info(`HU sub-pipeline: ${ctx.stageResults.huReviewer.certified} certified stories — running each as a sub-pipeline`);
51
+ emitProgress(emitter, makeEvent("hu:sub-pipeline:start", { ...ctx.eventBase, stage: "hu-sub-pipeline" }, {
52
+ message: `Running ${ctx.stageResults.huReviewer.certified} HUs as sub-pipelines`,
53
+ detail: { total: ctx.stageResults.huReviewer.total, certified: ctx.stageResults.huReviewer.certified }
54
+ }));
55
+
56
+ // Per-HU pipeline: focused max_iterations, fresh Brain state, own git branch.
57
+ const originalMaxIterations = ctx.config.max_iterations;
58
+ const huMaxIterations = ctx.config.hu_max_iterations ?? 3;
59
+ const huBranches = new Map();
60
+ const { prepareHuBranch, finalizeHuCommit } = await import("../../git/hu-automation.js");
61
+
62
+ // PR F (v2.7.5): the per-HU loop needs three pieces of plan context:
63
+ // • ADRs (loaded once from disk, applies to every HU on this plan)
64
+ // • the plan-reviewer findings (rides on the huBatch)
65
+ // • each HU's spec_section (rides on the story)
66
+ // Loading ADRs once outside the loop avoids hitting the filesystem
67
+ // per-HU. When the plan never went through `kj plan` (legacy
68
+ // auto-generated batch), planId is null → loadActiveAdrs falls back
69
+ // to the "_loose" bucket and returns [] silently.
70
+ const planId = ctx.stageResults.huReviewer.planId || null;
71
+ const reviewerFindings = ctx.stageResults.huReviewer.review || null;
72
+ let planAdrs = [];
73
+ try {
74
+ const { loadActiveAdrs } = await import("../../plan/adr-loader.js");
75
+ planAdrs = await loadActiveAdrs(ctx.config.projectDir || process.cwd(), planId);
76
+ if (planAdrs.length > 0) {
77
+ logger.info(`Plan ${planId || "(loose)"}: loaded ${planAdrs.length} ADR(s) for coder context`);
78
+ }
79
+ } catch (err) {
80
+ logger.warn(`ADR loader failed (non-blocking): ${err.message}`);
81
+ }
82
+
83
+ const subPipelineResult = await runHuSubPipeline({
84
+ huReviewerResult: ctx.stageResults.huReviewer,
85
+ runIterationFn: async (huTask, story) => {
86
+ ctx.config.max_iterations = huMaxIterations;
87
+ if (ctx.brainCtx?.enabled) {
88
+ ctx.brainCtx.extensionCount = 0;
89
+ const { createBrainContext } = await import("../brain-coordinator.js");
90
+ const fresh = createBrainContext({ enabled: true });
91
+ ctx.brainCtx.feedbackQueue = fresh.feedbackQueue;
92
+ ctx.brainCtx.verificationTracker = fresh.verificationTracker;
93
+ }
94
+ // Apply per-HU policies based on task_type (infra skips reviewer/sonar/tdd)
95
+ const { applyPolicies } = await import("../../guards/policy-resolver.js");
96
+ const huPolicies = applyPolicies({ taskType: story.task_type, policies: ctx.config.policies });
97
+ const savedFlags = { ...ctx.pipelineFlags };
98
+ if (!huPolicies.reviewer) ctx.pipelineFlags.reviewerEnabled = false;
99
+ if (!huPolicies.tdd) ctx.config.development = { ...ctx.config.development, methodology: "standard", require_test_changes: false };
100
+ if (!huPolicies.sonar) ctx.config.sonarqube = { ...ctx.config.sonarqube, enabled: false };
101
+ if (!huPolicies.testsRequired) ctx.pipelineFlags.testerEnabled = false;
102
+ logger.info(`HU ${story.id} (${story.task_type}): policies → reviewer=${huPolicies.reviewer}, tdd=${huPolicies.tdd}, sonar=${huPolicies.sonar}, tests=${huPolicies.testsRequired}`);
103
+
104
+ const branchName = await prepareHuBranch({ story, huBranches, config: ctx.config, logger });
105
+ const projectDir = ctx.config.projectDir || process.cwd();
106
+
107
+ // If HU has acceptance_tests, Brain runs them as the gate instead of
108
+ // the standard reviewer/tester pipeline. This is the radical fix:
109
+ // concrete executable tests replace subjective reviewer opinions.
110
+ if (story.acceptance_tests?.length > 0) {
111
+ const { runAcceptanceTests, buildDiagnosticPrompt } = await import("../../hu/acceptance-runner.js");
112
+ // Issue #538 — Repairer runtime safety net. The tracker remembers
113
+ // each test's last failure signature; classifyFailure flags
114
+ // structural bugs (broken jq, shell parse error, missing tool)
115
+ // after 2 consecutive identical failures so we don't burn 5
116
+ // iterations against an irreparable test (the 2026-04-29 bug).
117
+ const failureTracker = createFailureTracker();
118
+
119
+ for (let attempt = 1; attempt <= ctx.config.max_iterations; attempt++) {
120
+ logger.info(`HU ${story.id}: coder iteration ${attempt}/${ctx.config.max_iterations}`);
121
+ emitProgress(emitter, makeEvent("iteration:start", { ...ctx.eventBase, stage: "iteration" }, {
122
+ message: `Iteration ${attempt}/${ctx.config.max_iterations}`,
123
+ detail: { iteration: attempt, maxIterations: ctx.config.max_iterations }
124
+ }));
125
+
126
+ // Coder runs with the HU task + any diagnostic feedback from previous attempt.
127
+ // Phase 2 of tests-first (v2.7.5): also hand over the declared
128
+ // acceptance_tests so the coder has the contract from turn 1,
129
+ // not only after the first failed run.
130
+ const coderResult = await runCoderStage({
131
+ coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole,
132
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
133
+ session: ctx.session, plannedTask: ctx.plannedTask,
134
+ trackBudget: ctx.trackBudget, iteration: attempt, brainCtx: ctx.brainCtx,
135
+ acceptanceTests: story.acceptance_tests,
136
+ // PR F (v2.7.5): plan-aware coder context. ADRs are
137
+ // shared across the plan, the rest are scoped to this
138
+ // HU. buildCoderPrompt skips any section whose data is
139
+ // null/empty so legacy paths render the same prompt.
140
+ adrs: planAdrs,
141
+ specSection: story.spec_section || null,
142
+ reviewerFindings,
143
+ huId: story.id
144
+ });
145
+ if (coderResult?.action === "standby" || coderResult?.action === "pause") {
146
+ return coderResult?.result || { approved: false, reason: "coder_failed" };
147
+ }
148
+
149
+ // Sonar quality gate (sw task_type only, when policies say sonar=true)
150
+ if (huPolicies.sonar && ctx.config.sonarqube?.enabled) {
151
+ try {
152
+ const sonarResult = await runSonarStage({
153
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
154
+ session: ctx.session, trackBudget: ctx.trackBudget, iteration: attempt,
155
+ // sonarState is required: runSonarStage reads/writes
156
+ // .issuesInitial / .issuesFinal on it. Forgetting this
157
+ // surfaces as `Cannot read properties of undefined
158
+ // (reading 'issuesInitial')` and the catch below swallows
159
+ // it as "sonar stage failed (non-blocking)" — the gate
160
+ // never actually runs for the HU, every iteration burns
161
+ // budget for nothing. Mirrors iteration-loop.js.
162
+ sonarState: ctx.sonarState,
163
+ repeatDetector: ctx.repeatDetector,
164
+ budgetSummary: ctx.budgetSummary,
165
+ askQuestion, brainCtx: ctx.brainCtx
166
+ });
167
+ if (sonarResult?.action === "continue") {
168
+ // Sonar failed — add to feedback for next coder attempt
169
+ ctx.plannedTask = `${huTask}\n\n--- SONAR FAILURE ---\n${ctx.session.last_reviewer_feedback}`;
170
+ continue;
171
+ }
172
+ } catch (err) {
173
+ logger.warn(`HU ${story.id}: sonar stage failed (non-blocking): ${err.message}`);
174
+ }
175
+ }
176
+
177
+ // Brain runs acceptance tests
178
+ logger.info(`HU ${story.id}: running ${story.acceptance_tests.length} acceptance tests`);
179
+ emitProgress(emitter, makeEvent("hu:acceptance-start", { ...ctx.eventBase, stage: "acceptance" }, {
180
+ message: `Running ${story.acceptance_tests.length} acceptance tests`,
181
+ detail: { huId: story.id, testCount: story.acceptance_tests.length }
182
+ }));
183
+
184
+ const testResult = await runAcceptanceTests(story.acceptance_tests, projectDir);
185
+ emitProgress(emitter, makeEvent("hu:acceptance-end", { ...ctx.eventBase, stage: "acceptance" }, {
186
+ status: testResult.allPassed ? "ok" : "fail",
187
+ message: testResult.summary,
188
+ detail: { allPassed: testResult.allPassed, results: testResult.results.map(r => ({ cmd: r.cmd, passed: r.passed })) }
189
+ }));
190
+
191
+ if (testResult.allPassed) {
192
+ // Phase 3 of tests-first (v2.7.5): the shell gate is green,
193
+ // but any Gherkin scenarios are still "pending" — they need
194
+ // the tester to translate them into real test files before
195
+ // we can call the HU done. If none are pending, approve
196
+ // immediately like before.
197
+ const pendingGherkinTests = (story.acceptance_tests || []).filter(
198
+ (t) => t && typeof t === "object" && t.type === "gherkin"
199
+ );
200
+ if (pendingGherkinTests.length === 0) {
201
+ logger.info(`HU ${story.id}: all shell acceptance tests PASSED, no Gherkin to translate — approved`);
202
+ await finalizeHuCommit({ story, branchName, config: ctx.config, logger });
203
+ return { approved: true, sessionId: ctx.session.id, reason: "acceptance_tests_passed" };
204
+ }
205
+
206
+ // Shell tests passed; Gherkin needs translation. Invoke the
207
+ // tester with both so it writes code tests for the Gherkin,
208
+ // runs the suite, and returns verdict.
209
+ logger.info(`HU ${story.id}: shell gate passed, asking tester to translate ${pendingGherkinTests.length} Gherkin scenario(s)`);
210
+ const { runTesterStage } = await import("../post-loop-stages.js");
211
+ const shellResults = testResult.results.filter((r) => r.type !== "gherkin");
212
+ const testerOutcome = await runTesterStage({
213
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
214
+ session: ctx.session, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
215
+ iteration: attempt, task: huTask, diff: null, askQuestion,
216
+ pendingGherkinTests, shellTestResults: shellResults,
217
+ });
218
+ const testerStage = testerOutcome?.stageResult;
219
+ if (testerOutcome?.action !== "continue" && testerStage?.verdict === "pass") {
220
+ logger.info(`HU ${story.id}: tester translated Gherkin and verdict=pass — approved`);
221
+ await finalizeHuCommit({ story, branchName, config: ctx.config, logger });
222
+ return { approved: true, sessionId: ctx.session.id, reason: "acceptance_tests_passed" };
223
+ }
224
+
225
+ // Tester rejected (translated tests failed or coverage
226
+ // inadequate). Feed its diagnostic back to the coder for
227
+ // another iteration.
228
+ const failing = testerStage?.failing_scenarios?.join("\n- ") || testerStage?.summary || "tester rejected";
229
+ const diagnostic = [
230
+ "Tester translated the Gherkin acceptance tests and some FAILED:",
231
+ failing ? `- ${failing}` : "",
232
+ "",
233
+ "Fix the implementation so every scenario passes. Do NOT soften the scenarios.",
234
+ ].filter(Boolean).join("\n");
235
+ logger.warn(`HU ${story.id}: tester verdict=fail after Gherkin translation — sending back to coder`);
236
+ setReviewerFeedback(ctx.session, diagnostic);
237
+ ctx.plannedTask = `${huTask}\n\n--- GHERKIN TRANSLATION FAILURES ---\n${diagnostic}`;
238
+ continue;
239
+ }
240
+
241
+ // Brain diagnoses failures and sends concrete fix to coder
242
+ const failed = testResult.results.filter(r => !r.passed);
243
+
244
+ // Repairer (#538): for each failing shell test, see if it's been
245
+ // failing the same way 2× in a row AND looks structurally broken
246
+ // (jq syntax, shell parse error, missing tool, etc.). If yes,
247
+ // invoke RepairerRole to rewrite the test in the plan + batch
248
+ // before iterating again. If unfixable, escalate (fail the HU
249
+ // immediately rather than burn the rest of max_iterations).
250
+ let repairedAny = false;
251
+ let repairEscalate = null;
252
+ for (const f of failed) {
253
+ if (f.type !== "shell") continue;
254
+ const consecutive = failureTracker.record(f.cmd, f.output);
255
+ const cls = classifyFailure({
256
+ cmd: f.cmd, errorOutput: f.output, consecutive,
257
+ });
258
+ if (!cls.infeasible) continue;
259
+ const testIndex = story.acceptance_tests.findIndex((t) => {
260
+ const c = typeof t === "object" ? t.content : t;
261
+ return c === f.cmd;
262
+ });
263
+ if (testIndex < 0) continue;
264
+ // Mutate `story.acceptance_tests` in place. The same story
265
+ // reference lives inside runHuSubPipeline's batch (passed
266
+ // by ref), so the next iteration's acceptance runner sees
267
+ // the fixed test without us needing batch+batchSessionId
268
+ // in this scope. The reconcile from PR #544 will sync the
269
+ // plan JSON on the next run.
270
+ const repair = await runRepair({
271
+ story, testIndex, errorOutput: f.output,
272
+ batchSessionId: ctx.stageResults.huReviewer.batchSessionId,
273
+ // batch unavailable in this closure scope — runRepair
274
+ // gates saveHuBatch on its presence and skips when
275
+ // missing; the in-memory mutation alone is sufficient
276
+ // for the next acceptance run within this same HU loop.
277
+ batch: null,
278
+ config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
279
+ failureKind: cls.kind,
280
+ savePlanPatch: null,
281
+ });
282
+ if (repair.escalate) { repairEscalate = repair.reason; break; }
283
+ if (repair.repaired) {
284
+ repairedAny = true;
285
+ failureTracker.reset(f.cmd);
286
+ }
287
+ }
288
+
289
+ if (repairEscalate) {
290
+ logger.warn(`HU ${story.id}: Repairer escalated — ${repairEscalate}`);
291
+ return { approved: false, sessionId: ctx.session.id, reason: "repair_escalated" };
292
+ }
293
+ if (repairedAny) {
294
+ // Tests were rewritten — restart iteration with the fixed
295
+ // tests in place; do NOT increment attempt against the user
296
+ // because the failure was the planner's bug, not the coder's.
297
+ logger.info(`HU ${story.id}: Repairer fixed test(s); restarting acceptance gate without consuming iteration`);
298
+ attempt -= 1;
299
+ continue;
300
+ }
301
+
302
+ // No repair happened — normal coder feedback path.
303
+ const diagnostic = buildDiagnosticPrompt(failed);
304
+ logger.warn(`HU ${story.id}: ${failed.length} acceptance test(s) FAILED — sending diagnostic to coder`);
305
+ setReviewerFeedback(ctx.session, diagnostic);
306
+ ctx.plannedTask = `${huTask}\n\n--- ACCEPTANCE TEST FAILURES ---\n${diagnostic}`;
307
+ }
308
+
309
+ // All iterations exhausted
310
+ logger.warn(`HU ${story.id}: max iterations reached with acceptance tests still failing`);
311
+ return { approved: false, sessionId: ctx.session.id, reason: "acceptance_tests_failed" };
312
+ }
313
+
314
+ // Fallback: no acceptance_tests → standard pipeline (reviewer/tester)
315
+ try {
316
+ const result = await runIterationLoop(ctx, { task: huTask, askQuestion, emitter, logger });
317
+ if (result?.approved) {
318
+ await finalizeHuCommit({ story, branchName, config: ctx.config, logger });
319
+ }
320
+ return result;
321
+ } finally {
322
+ ctx.config.max_iterations = originalMaxIterations;
323
+ Object.assign(ctx.pipelineFlags, savedFlags);
324
+ }
325
+ },
326
+ emitter,
327
+ eventBase: ctx.eventBase,
328
+ logger,
329
+ config: ctx.config,
330
+ // PR1 (live HU status): forward the per-HU status updater so
331
+ // hu-sub-pipeline can patch the plan JSON on every transition,
332
+ // not only at the end of the run. The board's chokidar then
333
+ // fires per-HU and the Kanban columns update in real time.
334
+ onStatusChange: ctx.session?._liveStatusUpdater || null,
335
+ // PR3 (per-HU outcome): same pattern, but for the rich
336
+ // outcome blob written ONCE per HU at the end of runSingleHu.
337
+ onOutcome: ctx.session?._liveOutcomeUpdater || null
338
+ });
339
+
340
+ emitProgress(emitter, makeEvent("hu:sub-pipeline:end", { ...ctx.eventBase, stage: "hu-sub-pipeline" }, {
341
+ status: subPipelineResult.approved ? "ok" : "fail",
342
+ message: subPipelineResult.approved
343
+ ? "All HUs completed successfully"
344
+ : `Sub-pipeline finished with failures (${subPipelineResult.blockedIds.length} blocked)`,
345
+ detail: { results: subPipelineResult.results, blockedIds: subPipelineResult.blockedIds }
346
+ }));
347
+
348
+ // Sync results back to plan file if this was a plan-based run
349
+ if (ctx.session._syncResultsToPlan) {
350
+ try {
351
+ await ctx.session._syncResultsToPlan(subPipelineResult);
352
+ logger.info("Plan updated with execution results");
353
+ } catch (err) {
354
+ logger.warn(`Failed to sync results to plan: ${err.message}`);
355
+ }
356
+ }
357
+
358
+ const finalResult = {
359
+ approved: subPipelineResult.approved,
360
+ sessionId: ctx.session.id,
361
+ huResults: subPipelineResult.results,
362
+ blockedIds: subPipelineResult.blockedIds,
363
+ planId: ctx.session._planRef?.planId || null
364
+ };
365
+ await writeHistoryRecord({ sessionId: ctx.session.id, task, result: finalResult, logger });
366
+ return { handled: true, result: finalResult };
367
+ }
@@ -149,4 +149,3 @@ export function deserialize(str) {
149
149
  }
150
150
  }
151
151
 
152
- export { SEVERITY_ORDER, CATEGORY_ORDER };
@@ -2,7 +2,8 @@
2
2
  * Session flow control — checkpoints, timeouts, budget checks.
3
3
  * Extracted from orchestrator.js — session management, not orchestration.
4
4
  */
5
- import { markSessionStatus, addCheckpoint } from "../session-store.js";
5
+ import { markSessionStatus, addCheckpoint } from "../session/store.js";
6
+ import { setRetryCount } from "../session/mutators.js";
6
7
  import { emitProgress, makeEvent } from "../utils/events.js";
7
8
  import { msg, getLang } from "../utils/messages.js";
8
9
 
@@ -33,12 +34,12 @@ export function takeCheckpointSnapshot(session) {
33
34
  */
34
35
  export function shouldAutoContinueCheckpoint(session, hasProgress) {
35
36
  if (hasProgress) {
36
- session._checkpoint_stall_count = 0;
37
+ setRetryCount(session, "checkpoint_stall", 0);
37
38
  return { autoContinue: true, reason: "progress_detected" };
38
39
  }
39
40
  const wasRateLimited = (session.standby_retry_count || 0) > 0;
40
41
  const consecutiveStalls = (session._checkpoint_stall_count || 0) + 1;
41
- session._checkpoint_stall_count = consecutiveStalls;
42
+ setRetryCount(session, "checkpoint_stall", consecutiveStalls);
42
43
  if (wasRateLimited && consecutiveStalls < 3) {
43
44
  return { autoContinue: true, reason: "recoverable_stall" };
44
45
  }