karajan-code 2.7.4 → 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 (165) 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/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 +85 -31
  18. package/src/checks/sonar.js +1 -1
  19. package/src/checks/system.js +1 -1
  20. package/src/cli/_shared.js +30 -0
  21. package/src/cli/register-meta.js +204 -0
  22. package/src/cli/register-pipeline.js +212 -0
  23. package/src/cli/register-plan.js +152 -0
  24. package/src/cli/register-roles-skills.js +38 -0
  25. package/src/cli/register-sonar.js +28 -0
  26. package/src/cli.js +83 -468
  27. package/src/commands/agents.js +18 -6
  28. package/src/commands/architect.js +29 -2
  29. package/src/commands/audit.js +27 -3
  30. package/src/commands/board.js +160 -30
  31. package/src/commands/clean.js +83 -0
  32. package/src/commands/code.js +7 -2
  33. package/src/commands/discover.js +7 -2
  34. package/src/commands/init.js +2 -2
  35. package/src/commands/plan/_shared.js +40 -0
  36. package/src/commands/plan/add-hu.js +21 -0
  37. package/src/commands/plan/delete.js +9 -0
  38. package/src/commands/plan/generate.js +284 -0
  39. package/src/commands/plan/index.js +14 -0
  40. package/src/commands/plan/list.js +21 -0
  41. package/src/commands/plan/ready.js +46 -0
  42. package/src/commands/plan/remove-hu.js +17 -0
  43. package/src/commands/plan/show.js +81 -0
  44. package/src/commands/plan/validate.js +20 -0
  45. package/src/commands/plan.js +14 -291
  46. package/src/commands/researcher.js +7 -2
  47. package/src/commands/resume.js +24 -1
  48. package/src/commands/review.js +7 -2
  49. package/src/commands/run.js +52 -1
  50. package/src/commands/status.js +1 -1
  51. package/src/commands/sync.js +75 -0
  52. package/src/commands/triage.js +7 -2
  53. package/src/commands/undo.js +1 -1
  54. package/src/config/defaults.js +213 -0
  55. package/src/config/loader.js +137 -0
  56. package/src/config/overrides.js +199 -0
  57. package/src/config/role-resolver.js +159 -0
  58. package/src/config/test-harness.js +79 -0
  59. package/src/config.js +30 -632
  60. package/src/git/automation.js +2 -2
  61. package/src/guards/output-guard.js +0 -2
  62. package/src/hu/acceptance-runner.js +52 -10
  63. package/src/hu/store.js +11 -0
  64. package/src/mcp/handlers/direct-handlers.js +2 -5
  65. package/src/mcp/handlers/management-handlers.js +48 -4
  66. package/src/mcp/handlers/run-handler.js +9 -8
  67. package/src/mcp/preflight.js +32 -13
  68. package/src/mcp/server-handlers.js +4 -3
  69. package/src/mcp/suggest-handler.js +3 -2
  70. package/src/mcp/tools.js +15 -0
  71. package/src/orchestrator/agent-fallback.js +1 -1
  72. package/src/orchestrator/ci-integration.js +10 -10
  73. package/src/orchestrator/config-init.js +25 -7
  74. package/src/orchestrator/direct-actions.js +6 -2
  75. package/src/orchestrator/drivers/error-recovery.js +287 -0
  76. package/src/orchestrator/drivers/init-context.js +222 -0
  77. package/src/orchestrator/drivers/iteration-loop.js +311 -0
  78. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
  79. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  80. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  81. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
  82. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  83. package/src/orchestrator/drivers/post-loop.js +370 -0
  84. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
  85. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  86. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  87. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
  88. package/src/orchestrator/drivers/pre-loop.js +434 -0
  89. package/src/orchestrator/drivers/run-hu-batch.js +367 -0
  90. package/src/orchestrator/flow-control.js +4 -3
  91. package/src/orchestrator/flow-runner.js +121 -2077
  92. package/src/orchestrator/fs-leak-detector.js +156 -0
  93. package/src/orchestrator/hu-sub-pipeline.js +197 -9
  94. package/src/orchestrator/integrations.js +88 -0
  95. package/src/orchestrator/post-loop-stages.js +35 -8
  96. package/src/orchestrator/preflight-checks.js +20 -7
  97. package/src/orchestrator/repair/repair-detector.js +117 -0
  98. package/src/orchestrator/repair/repair-runner.js +142 -0
  99. package/src/orchestrator/reviewer-fallback.js +1 -1
  100. package/src/orchestrator/solomon-escalation.js +3 -2
  101. package/src/orchestrator/solomon-rules.js +7 -3
  102. package/src/orchestrator/stages/architect-stage.js +12 -11
  103. package/src/orchestrator/stages/coder-stage.js +56 -7
  104. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  105. package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
  106. package/src/orchestrator/stages/planner-stage.js +1 -1
  107. package/src/orchestrator/stages/research-stage.js +1 -1
  108. package/src/orchestrator/stages/reviewer-stage.js +7 -7
  109. package/src/orchestrator/stages/sonar-stage.js +94 -10
  110. package/src/orchestrator/stages/stage-classes.js +86 -0
  111. package/src/orchestrator/stages/stage-executor.js +1 -1
  112. package/src/orchestrator/stages/triage-stage.js +1 -1
  113. package/src/orchestrator/standby.js +3 -2
  114. package/src/orchestrator/verification-gate.js +20 -12
  115. package/src/plan/adr-generator.js +170 -0
  116. package/src/plan/adr-loader.js +60 -0
  117. package/src/plan/plan-executor.js +19 -1
  118. package/src/plan/plan-hu-ops.js +170 -1
  119. package/src/plan/plan-reviewer.js +199 -0
  120. package/src/plan/plan-schema.js +69 -1
  121. package/src/plan/plan-store.js +40 -3
  122. package/src/plan/spec-detector.js +61 -0
  123. package/src/plan/tests-synthesizer.js +116 -0
  124. package/src/planning-game/client.js +3 -14
  125. package/src/planning-game/decomposition.js +1 -1
  126. package/src/planning-game/pipeline-adapter.js +36 -5
  127. package/src/prompts/coder.js +172 -1
  128. package/src/prompts/hu-reviewer.js +0 -1
  129. package/src/prompts/planner.js +81 -4
  130. package/src/review/diff-generator.js +35 -22
  131. package/src/review/snapshot-diff.js +1 -1
  132. package/src/roles/coder-role.js +25 -3
  133. package/src/roles/planner-role.js +42 -1
  134. package/src/roles/repairer-role.js +181 -0
  135. package/src/roles/tester-role.js +70 -4
  136. package/src/session/mutators.js +304 -0
  137. package/src/session/runtime-overrides.js +59 -0
  138. package/src/{session-store.js → session/store.js} +2 -2
  139. package/src/skills/semantic-detector.js +10 -5
  140. package/src/skills/skill-detector.js +1 -1
  141. package/src/sonar/cloud-scanner.js +0 -1
  142. package/src/sonar/credentials.js +14 -6
  143. package/src/sonar/discovery.js +123 -0
  144. package/src/sonar/manager.js +18 -0
  145. package/src/sync/detect-drift.js +113 -0
  146. package/src/tracker-bootstrap.js +44 -0
  147. package/src/utils/agent-detect.js +15 -11
  148. package/src/utils/board-prompt-bridge.js +109 -0
  149. package/src/utils/cli-progress.js +161 -0
  150. package/src/utils/cwd-confirm.js +39 -0
  151. package/src/utils/derive-project-name-from-cwd.js +119 -0
  152. package/src/utils/garbage-collector.js +396 -0
  153. package/src/utils/git.js +16 -4
  154. package/src/utils/locale.js +1 -1
  155. package/src/utils/paths.js +21 -1
  156. package/src/utils/process.js +1 -1
  157. package/src/utils/prompt-project-name.js +35 -0
  158. package/src/utils/run-context.js +75 -0
  159. package/src/utils/run-log.js +12 -0
  160. package/src/utils/task-file.js +1 -1
  161. package/src/utils/telemetry.js +13 -1
  162. package/src/webperf/devtools-detect.js +1 -1
  163. package/templates/coder-rules.md +13 -0
  164. package/templates/roles/coder.md +34 -0
  165. package/templates/roles/repairer.md +94 -0
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Post-loop driver — extracted from src/orchestrator/flow-runner.js in
3
+ * TSK-0335 (Oleada 3 of the v2.7.4 audit refactor).
4
+ *
5
+ * Handlers for everything that happens AFTER the iteration loop decides
6
+ * an outcome (approved, max-iterations, or simple finalization):
7
+ *
8
+ * - handlePostLoopStages: run tester → security → final audit
9
+ * after the reviewer has approved; any of
10
+ * them can demand one more coder pass.
11
+ * - finalizeApprovedSession: commit/push/PR + PG card transition +
12
+ * full session journal + session:end event.
13
+ * - handleMaxIterationsReached: Brain-owned decision at the iteration
14
+ * cap (extend vs finalize vs escalate).
15
+ * Legacy path delegates to Solomon.
16
+ * - tryAutoStartBoard: auto-launch HU Board when config opts in.
17
+ * - writeHistoryRecord: non-blocking HU history write on session end.
18
+ *
19
+ * Extracted verbatim; no behavior change. flow-runner.js now imports these
20
+ * instead of defining them. Part of breaking up the 2254-LOC god-module —
21
+ * see docs/ARCHITECTURE.md and the TSK-0335 acceptance criteria.
22
+ */
23
+
24
+ import { generateDiff } from "../../review/diff-generator.js";
25
+ import { finalizeGitAutomation } from "../../git/automation.js";
26
+ import { saveSession, markSessionStatus } from "../../session/store.js";
27
+ import {
28
+ setReviewerFeedback, setBudget, setRtkSavings, resetRetryCount,
29
+ } from "../../session/mutators.js";
30
+ import { emitProgress, makeEvent } from "../../utils/events.js";
31
+ import { runTesterStage, runSecurityStage, runFinalAuditStage } from "../post-loop-stages.js";
32
+ import { invokeSolomon } from "../solomon-escalation.js";
33
+ import { tryCiComment } from "../ci-integration.js";
34
+ import { writeIterationsJournal } from "../session-journal.js";
35
+ import { getIntegration } from "../integrations.js";
36
+
37
+ export async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, ciEnabled: _ciEnabled, testerEnabled, securityEnabled, askQuestion, logger, brainCtx }) {
38
+ const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
39
+
40
+ if (testerEnabled) {
41
+ const testerResult = await runTesterStage({
42
+ config, logger, emitter, eventBase, session, coderRole, trackBudget,
43
+ iteration: i, task, diff: postLoopDiff, askQuestion
44
+ });
45
+ if (testerResult.action === "pause") return { action: "return", result: testerResult.result };
46
+ if (testerResult.action === "continue") {
47
+ const summary = testerResult.stageResult?.summary || "Tester found issues";
48
+ setReviewerFeedback(session, `Tester FAILED — fix these issues:\n${summary}`);
49
+ await saveSession(session);
50
+ if (testerResult.stageResult) stageResults.tester = testerResult.stageResult;
51
+ // Brain: push tester failure into feedback queue + compress for next coder iteration
52
+ if (brainCtx?.enabled) {
53
+ const { processRoleOutput } = await import("../brain-coordinator.js");
54
+ processRoleOutput(brainCtx, { roleName: "tester", output: testerResult.stageResult, iteration: i });
55
+ }
56
+ return { action: "continue" };
57
+ }
58
+ if (testerResult.stageResult) {
59
+ stageResults.tester = testerResult.stageResult;
60
+ await tryCiComment({ config, session, logger, agent: "Tester", body: `Tests: ${testerResult.stageResult.summary || "completed"}` });
61
+ }
62
+ }
63
+
64
+ if (securityEnabled) {
65
+ const securityResult = await runSecurityStage({
66
+ config, logger, emitter, eventBase, session, coderRole, trackBudget,
67
+ iteration: i, task, diff: postLoopDiff, askQuestion, brainCtx
68
+ });
69
+ if (securityResult.action === "pause") return { action: "return", result: securityResult.result };
70
+ if (securityResult.action === "continue") {
71
+ const summary = securityResult.stageResult?.summary || "Security found issues";
72
+ setReviewerFeedback(session, `Security FAILED — fix these issues:\n${summary}`);
73
+ await saveSession(session);
74
+ if (securityResult.stageResult) stageResults.security = securityResult.stageResult;
75
+ // Brain: push security findings into feedback queue + compress for next coder iteration
76
+ if (brainCtx?.enabled) {
77
+ const { processRoleOutput } = await import("../brain-coordinator.js");
78
+ processRoleOutput(brainCtx, { roleName: "security", output: securityResult.stageResult, iteration: i });
79
+ }
80
+ return { action: "continue" };
81
+ }
82
+ if (securityResult.stageResult) {
83
+ stageResults.security = securityResult.stageResult;
84
+ await tryCiComment({ config, session, logger, agent: "Security", body: `Security scan: ${securityResult.stageResult.summary || "completed"}` });
85
+ }
86
+ }
87
+
88
+ // Final audit — last quality gate before declaring success
89
+ const auditResult = await runFinalAuditStage({
90
+ config, logger, emitter, eventBase, session, coderRole, trackBudget,
91
+ iteration: i, task, diff: postLoopDiff
92
+ });
93
+ if (auditResult.stageResult) {
94
+ stageResults.audit = auditResult.stageResult;
95
+ await tryCiComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
96
+ }
97
+ if (auditResult.action === "retry") {
98
+ // Audit found actionable issues — loop back to coder
99
+ setReviewerFeedback(session, auditResult.feedback);
100
+ await saveSession(session);
101
+ return { action: "continue" };
102
+ }
103
+
104
+ return { action: "proceed" };
105
+ }
106
+
107
+ export async function finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i, rtkTracker }) {
108
+ const gitResult = await finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults });
109
+
110
+ // Accumulate final commits for PG card lifecycle tracking
111
+ if (gitResult?.commits?.length) {
112
+ const tracker = getIntegration("tracker");
113
+ for (const c of gitResult.commits) tracker?.onCommit?.(session, c);
114
+ }
115
+
116
+ if (stageResults.planner?.ok) {
117
+ stageResults.planner.completedSteps = [...(stageResults.planner.steps ?? [])];
118
+ }
119
+ setBudget(session, budgetSummary());
120
+ await markSessionStatus(session, "approved");
121
+
122
+ await getIntegration("tracker")?.onSessionApproved?.({ pgCard, pgProject, config, session, gitResult, logger });
123
+
124
+ const deferredIssues = session.deferred_issues || [];
125
+ const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
126
+ if (rtkSavings) setRtkSavings(session, rtkSavings);
127
+ await saveSession(session);
128
+
129
+ // --- Journal: write final files ---
130
+ const journalDir = session._journalDir;
131
+ if (journalDir) {
132
+ try {
133
+ await writeIterationsJournal(journalDir, session._journalIterations || []);
134
+ // decisions.md: writes only when Solomon was invoked (AC of KJC-TSK-0287).
135
+ // Pulls structured data from session.solomonRulings + session.brainDecisions.
136
+ const { writeDecisionsJournal: writeDecisionsJournalV2 } =
137
+ await import("../../session/journal/decisions-writer.js");
138
+ const decisionsResult = await writeDecisionsJournalV2(session, { journalDir, logger });
139
+ // KJC-TSK-0288: use the new tree-writer with directory grouping
140
+ const { writeTreeJournal: writeTreeJournalV2 } =
141
+ await import("../../session/journal/tree-writer.js");
142
+ const treeResult = await writeTreeJournalV2(journalDir, session.session_start_sha, { logger });
143
+
144
+ const journalFiles = [...(session._journalFiles || [])];
145
+ if (session._journalIterations?.length) journalFiles.push("iterations.md");
146
+ if (decisionsResult.written) journalFiles.push("decisions.md");
147
+ if (treeResult.written) journalFiles.push("tree.txt");
148
+
149
+ // KJC-TSK-0289: use the richer summary writer (stages table, budget
150
+ // breakdown, brain/solomon counts, typed links to other journal files).
151
+ const { writeSummaryJournal: writeSummaryJournalV2 } =
152
+ await import("../../session/journal/summary-writer.js");
153
+ const startedAt = session._startedAt ? new Date(session._startedAt).toISOString() : undefined;
154
+ await writeSummaryJournalV2(journalDir, {
155
+ task: session.task,
156
+ result: "APPROVED",
157
+ sessionId: session.id,
158
+ iterations: i,
159
+ durationMs: Date.now() - (session._startedAt || Date.now()),
160
+ budget: budgetSummary(),
161
+ stages: stageResults,
162
+ commits: gitResult?.commits || [],
163
+ files: journalFiles,
164
+ startedAt,
165
+ finishedAt: new Date().toISOString(),
166
+ brainDecisions: Array.isArray(session.brainDecisions) ? session.brainDecisions.length : 0,
167
+ solomonInvocations: Array.isArray(session.solomonRulings) ? session.solomonRulings.length : 0,
168
+ // KJC-TSK-0327: surface skill-source decisions in the summary so the
169
+ // post-mortem shows which process skills (addyosmani) and stack skills
170
+ // (OpenSkills) actually shaped the role prompts.
171
+ skills: {
172
+ addyosmani: session.addyosmaniSkills,
173
+ installed: session.autoInstalledSkills,
174
+ recommended: session.skillsRecommended,
175
+ },
176
+ }, { logger });
177
+ logger.info(`Session journal written to ${journalDir}`);
178
+ } catch (err) {
179
+ logger.warn(`Journal write failed (non-blocking): ${err.message}`);
180
+ }
181
+ }
182
+
183
+ const endDetail = { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues };
184
+ if (rtkSavings) endDetail.rtk_savings = rtkSavings;
185
+ emitProgress(
186
+ emitter,
187
+ makeEvent("session:end", { ...eventBase, stage: "done" }, {
188
+ message: deferredIssues.length > 0
189
+ ? `Session approved (${deferredIssues.length} deferred issue(s) tracked as tech debt)`
190
+ : "Session approved",
191
+ detail: endDetail
192
+ })
193
+ );
194
+ const result = { approved: true, sessionId: session.id, review, git: gitResult, deferredIssues };
195
+ if (rtkSavings) result.rtk_savings = rtkSavings;
196
+ return result;
197
+ }
198
+
199
+ export async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker, brainCtx }) {
200
+ const budget = budgetSummary();
201
+
202
+ // Brain-owned decision: max_iterations is guidance, not a hard rule.
203
+ // Brain evaluates the feedback queue state to decide extend / finalize / escalate.
204
+ // Solomon is only consulted if Brain cannot decide on its own.
205
+ if (brainCtx?.enabled) {
206
+ const entries = brainCtx.feedbackQueue?.entries || [];
207
+ const pending = entries.map(e => ({ source: e.source, category: e.category, severity: e.severity, description: e.description }));
208
+ const hasSecurity = entries.some(e => e.category === "security" || e.source === "security");
209
+ const hasCorrectness = entries.some(e => ["correctness", "tests"].includes(e.category));
210
+
211
+ if (hasSecurity) {
212
+ // Brain: security issues unresolved → cannot finalize, escalate
213
+ logger.warn(`Brain: max_iterations reached with ${entries.filter(e => e.category === "security" || e.source === "security").length} unresolved security issue(s) — cannot finalize`);
214
+ return { paused: true, sessionId: session.id, question: "Brain: unresolved security issues at max_iterations. Review manually or extend pipeline.", context: "brain_security_block", pending };
215
+ }
216
+
217
+ if (hasCorrectness) {
218
+ // Brain: correctness/test issues pending. Cap at MAX_EXTENSIONS to avoid infinite extensions.
219
+ const MAX_EXTENSIONS = 2;
220
+ if (brainCtx.extensionCount >= MAX_EXTENSIONS) {
221
+ logger.warn(`Brain: ${brainCtx.extensionCount} extensions exhausted with correctness issues still pending — escalating to human`);
222
+ 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 };
223
+ }
224
+ brainCtx.extensionCount += 1;
225
+ 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})`);
226
+ resetRetryCount(session, "reviewer");
227
+ await saveSession(session);
228
+ return { approved: false, sessionId: session.id, reason: "max_iterations_extended", extraIterations: Math.ceil(config.max_iterations / 2) };
229
+ }
230
+
231
+ if (entries.length === 0) {
232
+ // Brain: no pending feedback → last reviewer approved, finalize
233
+ logger.info("Brain: max_iterations reached with clean feedback queue — finalizing as approved");
234
+ return { approved: true, sessionId: session.id, reason: "brain_approved" };
235
+ }
236
+
237
+ // hasStyleOnly: genuine dilemma → Brain consults Solomon
238
+ logger.info(`Brain: max_iterations with ${entries.length} style-only issue(s) — consulting Solomon on dilemma`);
239
+ const solomonResult = await invokeSolomon({
240
+ config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
241
+ iteration: config.max_iterations,
242
+ conflict: {
243
+ stage: "brain-max-iterations",
244
+ task,
245
+ iterationCount: config.max_iterations,
246
+ maxIterations: config.max_iterations,
247
+ budget_usd: budget?.total_cost_usd || 0,
248
+ dilemma: `Max iterations reached with ${entries.length} style-only issue(s) pending. Accept as-is or request more work?`,
249
+ pendingIssues: pending,
250
+ history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached" }]
251
+ }
252
+ });
253
+ // Brain applies Solomon's decision
254
+ if (solomonResult.action === "approve") {
255
+ logger.info("Brain: Solomon advised approve for style-only pending — finalizing");
256
+ return { approved: true, sessionId: session.id, reason: "brain_solomon_approved" };
257
+ }
258
+ if (solomonResult.action === "continue") {
259
+ return { approved: false, sessionId: session.id, reason: "max_iterations_extended", extraIterations: solomonResult.extraIterations || Math.ceil(config.max_iterations / 2) };
260
+ }
261
+ if (solomonResult.action === "pause") {
262
+ return { paused: true, sessionId: session.id, question: solomonResult.question, context: "brain_solomon_dilemma" };
263
+ }
264
+ // Fallback: escalate to human
265
+ return { paused: true, sessionId: session.id, question: `Brain+Solomon cannot resolve: ${entries.length} pending issue(s) at max_iterations`, context: "max_iterations" };
266
+ }
267
+
268
+ // Legacy path (Brain disabled): original Solomon-driven flow
269
+ const solomonResult = await invokeSolomon({
270
+ config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
271
+ iteration: config.max_iterations,
272
+ conflict: {
273
+ stage: "max_iterations",
274
+ task,
275
+ iterationCount: config.max_iterations,
276
+ maxIterations: config.max_iterations,
277
+ budget_usd: budget?.total_cost_usd || 0,
278
+ history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached without reviewer approval" }]
279
+ }
280
+ });
281
+
282
+ if (solomonResult.action === "approve") {
283
+ logger.info("Solomon approved coder's work at max_iterations checkpoint");
284
+ return { approved: true, sessionId: session.id, reason: "solomon_approved" };
285
+ }
286
+
287
+ if (solomonResult.action === "continue") {
288
+ if (solomonResult.humanGuidance) {
289
+ setReviewerFeedback(session, `Solomon guidance: ${solomonResult.humanGuidance}`);
290
+ }
291
+ resetRetryCount(session, "reviewer");
292
+ await saveSession(session);
293
+ const extraIterations = solomonResult.extraIterations || config.max_iterations;
294
+ return { approved: false, sessionId: session.id, reason: "max_iterations_extended", humanGuidance: solomonResult.humanGuidance, extraIterations };
295
+ }
296
+
297
+ if (solomonResult.action === "pause") {
298
+ return { paused: true, sessionId: session.id, question: solomonResult.question, context: "max_iterations" };
299
+ }
300
+
301
+ if (solomonResult.action === "subtask") {
302
+ return { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "max_iterations_subtask" };
303
+ }
304
+
305
+ // Solomon couldn't resolve — fail
306
+ setBudget(session, budgetSummary());
307
+ const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
308
+ if (rtkSavings) setRtkSavings(session, rtkSavings);
309
+ await markSessionStatus(session, "failed");
310
+ const failDetail = { approved: false, reason: "max_iterations", iterations: config.max_iterations, stages: stageResults, budget: budgetSummary() };
311
+ if (rtkSavings) failDetail.rtk_savings = rtkSavings;
312
+ emitProgress(
313
+ emitter,
314
+ makeEvent("session:end", { ...eventBase, stage: "done" }, {
315
+ status: "fail",
316
+ message: "Max iterations reached (Solomon could not resolve)",
317
+ detail: failDetail
318
+ })
319
+ );
320
+ return { approved: false, sessionId: session.id, reason: "max_iterations" };
321
+ }
322
+
323
+ export async function tryAutoStartBoard(config, logger, emitter, eventBase) {
324
+ // TSK-0273: gate on hu_board.auto_start alone. The previous double-gate
325
+ // (enabled && auto_start) was confusing because `enabled` was never the
326
+ // user-facing switch — only auto_start was. This matches the AC: "kj run
327
+ // auto-inicia el board server si hu_board.auto_start es true".
328
+ if (!config.hu_board?.auto_start) return;
329
+ // Never auto-start during vitest: races the PID file and starts a detached
330
+ // process that outlives the test run.
331
+ if (process.env.VITEST || process.env.NODE_ENV === "test") return;
332
+
333
+ try {
334
+ const { startBoard, renderBoardBanner } = await import("../../commands/board.js");
335
+ const { projectSlug: slugFor } = await import("../../plan/plan-store.js");
336
+ const boardPort = config.hu_board.port || 4000;
337
+ // Scope the board URL to the current run's project (`/p/<slug>`) so
338
+ // the user lands on a filtered view, not the global dashboard.
339
+ const slug = config.projectDir ? slugFor(config.projectDir) : null;
340
+ const boardResult = await startBoard(boardPort, { projectSlug: slug });
341
+ const status = boardResult.alreadyRunning ? "already running" : "started";
342
+ // Highlighted URL box — visible regardless of log level (matches the
343
+ // post-planner auto-HU banner so the user gets the same signal either
344
+ // way the board got started).
345
+ console.log(renderBoardBanner({ url: boardResult.url, status }));
346
+ logger.info(`HU Board ${status} at ${boardResult.url}`);
347
+ emitProgress(emitter, makeEvent("board:started", eventBase, {
348
+ message: `HU Board running at ${boardResult.url}`,
349
+ detail: { pid: boardResult.pid, port: boardPort, url: boardResult.url }
350
+ }));
351
+ } catch (err) {
352
+ logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
353
+ }
354
+ }
355
+
356
+ export async function writeHistoryRecord({ sessionId, task, result, logger }) {
357
+ try {
358
+ const { createHistoryRecord } = await import("../../hu/store.js");
359
+ const approved = Boolean(result?.approved);
360
+ const summary = result?.review?.summary || result?.reason || null;
361
+ await createHistoryRecord(sessionId, {
362
+ task,
363
+ result: JSON.stringify(result),
364
+ approved,
365
+ summary
366
+ });
367
+ } catch (err) {
368
+ logger.warn(`HU history record failed (non-blocking): ${err.message}`);
369
+ }
370
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Extracted from `src/orchestrator/drivers/pre-loop.js` in TSK-0337
3
+ * (audit recommendation #6). Previously an internal helper inside
4
+ * pre-loop.js; moved here so the driver stays under 600 LOC.
5
+ * Behaviour unchanged.
6
+ *
7
+ * Auto-generates an HU batch from triage's decomposition recommendation
8
+ * when no manual `huFile` is present. Runs after researcher/architect/
9
+ * planner so context is available for better HU generation. Sets
10
+ * `stageResults.huReviewer` so `needsSubPipeline` picks it up later.
11
+ *
12
+ * Side effects:
13
+ * - Persists `batch.json` to `~/.karajan/hu-stories/<batchSessionId>/`
14
+ * so the sub-pipeline can update story status via `saveHuBatch`.
15
+ * - Auto-starts the HU board (skipped under VITEST / NODE_ENV=test).
16
+ */
17
+
18
+ import { emitProgress, makeEvent } from "#utils/events.js";
19
+
20
+ export async function maybeGenerateAutoHuBatch({
21
+ flags, stageResults, task, logger, emitter, eventBase, projectDir, session,
22
+ }) {
23
+ // Skip if user passed a manual hu-file
24
+ if (flags?.huFile) return;
25
+ // Skip if hu-reviewer already produced a batch (manual enable + PG stories)
26
+ if (stageResults.huReviewer) return;
27
+ // Need triage decomposition recommendation
28
+ const shouldDecompose = stageResults.triage?.shouldDecompose;
29
+ const subtasks = stageResults.triage?.subtasks;
30
+ if (!shouldDecompose || !Array.isArray(subtasks) || subtasks.length < 2) return;
31
+
32
+ const { generateHuBatch } = await import("#hu/auto-generator.js");
33
+
34
+ // Detect if project is new: empty dir or only .git/.karajan/.gitignore
35
+ let isNewProject = false;
36
+ try {
37
+ const fs = await import("node:fs/promises");
38
+ const entries = await fs.readdir(projectDir);
39
+ const relevant = entries.filter(e => !e.startsWith(".git") && e !== ".karajan" && e !== ".gitignore");
40
+ isNewProject = relevant.length === 0;
41
+ } catch { /* ignore */ }
42
+
43
+ // Extract stack hints from planner + architect output
44
+ const stackHints = [];
45
+ const combined = `${stageResults.planner?.plan || ""} ${stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : ""} ${task}`.toLowerCase();
46
+ const stackKeywords = ["express", "vite", "vitest", "jest", "next", "astro", "react", "vue", "svelte", "fastapi", "django", "spring", "gin", "nestjs", "monorepo", "workspaces"];
47
+ for (const kw of stackKeywords) {
48
+ if (combined.includes(kw)) stackHints.push(kw);
49
+ }
50
+
51
+ const batch = generateHuBatch({
52
+ originalTask: task,
53
+ subtasks,
54
+ stackHints,
55
+ isNewProject,
56
+ researcherContext: stageResults.researcher?.summary || null,
57
+ architectContext: stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : null
58
+ });
59
+
60
+ // Persist batch to HU store so hu-sub-pipeline can update story status via saveHuBatch.
61
+ // Use session.id as batchSessionId.
62
+ const batchSessionId = `auto-${session.id}`;
63
+ try {
64
+ const fs = await import("node:fs/promises");
65
+ const path = await import("node:path");
66
+ const { getKarajanHome } = await import("#utils/paths.js");
67
+ const huDir = path.join(getKarajanHome(), "hu-stories", batchSessionId);
68
+ await fs.mkdir(huDir, { recursive: true });
69
+ const persistBatch = {
70
+ session_id: batchSessionId,
71
+ project_id: batchSessionId,
72
+ project_name: batch.projectName,
73
+ created_at: new Date().toISOString(),
74
+ updated_at: new Date().toISOString(),
75
+ stories: batch.stories
76
+ };
77
+ await fs.writeFile(path.join(huDir, "batch.json"), JSON.stringify(persistBatch, null, 2));
78
+ } catch (err) {
79
+ logger.warn(`Auto-HU: failed to persist batch (${err.message}) — sub-pipeline will use in-memory fallback`);
80
+ }
81
+
82
+ // Wrap in format compatible with needsSubPipeline + runHuSubPipeline
83
+ stageResults.huReviewer = {
84
+ ok: true,
85
+ stories: batch.stories,
86
+ total: batch.total,
87
+ certified: batch.certified,
88
+ batchSessionId,
89
+ auto_generated: true,
90
+ source: batch.source
91
+ };
92
+
93
+ logger.info(`Auto-HU: generated ${batch.total} stories (${batch.source.triage_subtasks} subtasks${isNewProject ? ", new project" : ""}${stackHints.length ? `, stack: ${stackHints.join(",")}` : ""})`);
94
+ emitProgress(emitter, makeEvent("hu:auto-generated", { ...eventBase, stage: "hu-auto-gen" }, {
95
+ message: `Auto-generated ${batch.total} HU(s) from triage decomposition`,
96
+ detail: { total: batch.total, subtasks: batch.source.triage_subtasks, isNewProject, stackHints, projectName: batch.projectName }
97
+ }));
98
+
99
+ // Auto-start the board so the user can see the generated HUs.
100
+ // Always fires when auto-HU runs, independent of hu_board.auto_start flag.
101
+ // Never during vitest — would race the PID file and leave a detached
102
+ // node process around after the suite (TSK-0273).
103
+ if (process.env.VITEST || process.env.NODE_ENV === "test") return;
104
+ try {
105
+ const { startBoard, renderBoardBanner } = await import("../../../commands/board.js");
106
+ const desiredPort = session.config_snapshot?.hu_board?.port ?? 4000;
107
+ const boardResult = await startBoard(desiredPort);
108
+ const url = boardResult.url;
109
+ const status = boardResult.alreadyRunning ? "already running" : "started";
110
+ const projectName = batch.projectName || "Auto-generated HUs";
111
+ console.log(renderBoardBanner({ url, status, projectName }));
112
+ logger.info(`HU Board ${status} at ${url} (project: ${projectName})`);
113
+ } catch (err) {
114
+ logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
115
+ }
116
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Extracted from `src/orchestrator/drivers/pre-loop.js` in TSK-0337
3
+ * (audit recommendation #6: continue pre-loop extraction). pre-loop.js
4
+ * had grown to 626 LOC, over the 600-LOC ceiling that the file's own
5
+ * docstring set. Splitting the deprecation warnings, addyosmani skill
6
+ * bootstrap, and auto-HU batch generation into their own modules brings
7
+ * the driver back to ~440 LOC.
8
+ *
9
+ * Behaviour unchanged. The function is exported with the same name and
10
+ * signature so the move is mechanical.
11
+ *
12
+ * Emits one-shot warnings for config keys ignored since v2.7.4. Currently:
13
+ * - `sonarqube.enabled` (kj.config.yml) → ignored, sonar is intrinsic
14
+ * - `--no-sonar` CLI flag → ignored
15
+ */
16
+
17
+ import { emitProgress, makeEvent } from "#utils/events.js";
18
+
19
+ export function emitConfigDeprecations(config, logger, emitter, eventBase) {
20
+ const dep = config?._deprecated;
21
+ if (!dep) return;
22
+
23
+ if (dep.sonarqubeEnabledKey) {
24
+ const m =
25
+ "DEPRECATED: `sonarqube.enabled` in kj.config.yml is ignored since v2.7.4. " +
26
+ "Sonar is intrinsic to Karajan for code tasks (sw/refactor/add-tests) and " +
27
+ "skipped for non-code tasks (audit/doc/infra/analysis/no-code) by policy. " +
28
+ "Remove the key from your config to silence this warning.";
29
+ logger.warn(m);
30
+ emitProgress(emitter, makeEvent("config:deprecated", { ...eventBase, stage: "config" }, {
31
+ message: m,
32
+ detail: { key: "sonarqube.enabled", since: "v2.7.4" },
33
+ }));
34
+ }
35
+
36
+ if (dep.noSonarFlag) {
37
+ const m =
38
+ "DEPRECATED: `--no-sonar` flag is ignored since v2.7.4. Sonar runs for code " +
39
+ "tasks by policy. To skip Sonar on a one-off run, use a non-code task type " +
40
+ "(e.g. `--task-type doc`) or rely on Solomon's runtime override.";
41
+ logger.warn(m);
42
+ emitProgress(emitter, makeEvent("config:deprecated", { ...eventBase, stage: "config" }, {
43
+ message: m,
44
+ detail: { flag: "--no-sonar", since: "v2.7.4" },
45
+ }));
46
+ }
47
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Extracted from `src/orchestrator/drivers/pre-loop.js` in TSK-0337
3
+ * (audit recommendation #6). Previously a 60-line export inside
4
+ * pre-loop.js; moved here to keep that driver under the 600-LOC
5
+ * ceiling its own docstring set. Behaviour and signature unchanged.
6
+ *
7
+ * Refreshes the addyosmani/agent-skills catalog (clones or pulls,
8
+ * gated by `refreshDays`) and resolves which slugs are relevant to
9
+ * the current task. Sets the result on the session via the
10
+ * `setAddyosmaniSkills` mutator so downstream stages can read it
11
+ * without re-running git.
12
+ *
13
+ * Failures are non-blocking: a missing git, a network blip, or an
14
+ * unparseable catalog all just mark the catalog unavailable and
15
+ * emit a `skills:addyosmani-unavailable` event. The pipeline keeps
16
+ * going — the addyosmani sources are an optional supplement.
17
+ */
18
+
19
+ import {
20
+ refreshIfStale as refreshAddyosmaniCatalog,
21
+ listAvailableSlugs as listAddyosmaniSlugs,
22
+ } from "#skills/addyosmani-catalog.js";
23
+ import { resolveAddyosmaniSlugs } from "#skills/addyosmani-role-map.js";
24
+ import { setAddyosmaniSkills } from "#session/mutators.js";
25
+ import { emitProgress, makeEvent } from "#utils/events.js";
26
+
27
+ export async function ensureAddyosmaniSkills({ task, config, logger, session, emitter, eventBase }) {
28
+ const skillsConfig = config?.skills || {};
29
+ const sources = Array.isArray(skillsConfig.sources) ? skillsConfig.sources : ["addyosmani", "openskills", "local"];
30
+ const addyConfig = skillsConfig.addyosmani || {};
31
+ // Test harness override: config.testHarness.defaultAddyosmaniEnabled=false
32
+ // prevents orchestrator tests from spawning git. Tests that need the real
33
+ // catalog opt in by setting config.skills.addyosmani.enabled = true.
34
+ // Post-v2.7.5 no longer reads globalThis directly — config.testHarness is
35
+ // populated by the loader from the global or the production default.
36
+ const harnessDefault = config?.testHarness?.defaultAddyosmaniEnabled;
37
+ const enabledFromConfig = addyConfig.enabled === true
38
+ || (addyConfig.enabled !== false && harnessDefault !== false);
39
+ const enabled = enabledFromConfig && sources.includes("addyosmani");
40
+ if (!enabled) return;
41
+
42
+ const refreshDays = Number.isFinite(addyConfig.refreshDays) ? addyConfig.refreshDays : 7;
43
+ const refreshMs = Math.max(0, refreshDays) * 24 * 60 * 60 * 1000;
44
+
45
+ try {
46
+ const refreshResult = await refreshAddyosmaniCatalog({
47
+ refreshMs,
48
+ repoUrl: addyConfig.repoUrl,
49
+ logger,
50
+ });
51
+
52
+ if (!refreshResult.ok) {
53
+ setAddyosmaniSkills(session, { available: false, reason: refreshResult.error || "refresh failed" });
54
+ emitProgress(emitter, makeEvent("skills:addyosmani-unavailable", { ...eventBase, stage: "skills" }, {
55
+ message: `addyosmani/agent-skills catalog unavailable: ${refreshResult.error || refreshResult.action}`,
56
+ detail: { action: refreshResult.action, hint: "Install git to enable process skills from addyosmani/agent-skills" },
57
+ }));
58
+ return;
59
+ }
60
+
61
+ const available = new Set(await listAddyosmaniSlugs());
62
+ const resolved = resolveAddyosmaniSlugs({ role: null, task }); // role resolution happens per-stage later
63
+ const valid = resolved.filter((slug) => available.has(slug));
64
+
65
+ setAddyosmaniSkills(session, {
66
+ available: true,
67
+ action: refreshResult.action,
68
+ resolvedSlugs: valid,
69
+ allAvailable: Array.from(available),
70
+ });
71
+
72
+ emitProgress(emitter, makeEvent("skills:addyosmani-ready", { ...eventBase, stage: "skills" }, {
73
+ message: `addyosmani/agent-skills ${refreshResult.action} — ${available.size} slug(s) available, ${valid.length} relevant to task`,
74
+ detail: {
75
+ action: refreshResult.action,
76
+ relevantSlugs: valid,
77
+ availableCount: available.size,
78
+ },
79
+ }));
80
+ } catch (err) {
81
+ logger.warn(`addyosmani catalog step failed (non-blocking): ${err.message}`);
82
+ setAddyosmaniSkills(session, { available: false, reason: err.message });
83
+ }
84
+ }