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
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Pre-loop driver — extracted from src/orchestrator/flow-runner.js in
3
+ * TSK-0335 (Oleada 3 of the v2.7.4 audit refactor).
4
+ *
5
+ * Everything that runs BEFORE the coder/reviewer iteration loop:
6
+ *
7
+ * - runPreLoopStages: the main pre-loop orchestration
8
+ * (HU-reviewer → intent → discover → triage →
9
+ * Brain decisor → domain → auto-simplify →
10
+ * PG decomposition → TDD detect → preflight →
11
+ * plan injection → planning phases →
12
+ * .gitignore update → auto-HU → skill
13
+ * auto-install).
14
+ * - runPlanningPhases: researcher → architect → planner.
15
+ * - emitConfigDeprecations: one-shot warnings for ignored config keys.
16
+ * - ensureAddyosmaniSkills: refresh + resolve addyosmani/agent-skills.
17
+ * - maybeGenerateAutoHuBatch: auto-generate HUs when triage recommends
18
+ * decomposition and no manual huFile was passed.
19
+ *
20
+ * Extracted verbatim; no behavior change. flow-runner.js now imports these
21
+ * instead of defining them. initFlowContext lives in ./init-context.js to
22
+ * keep each driver under the 600-LOC ceiling. Part of breaking up the
23
+ * 2254-LOC god-module — see docs/ARCHITECTURE.md and the TSK-0335
24
+ * acceptance criteria.
25
+ */
26
+
27
+ import { emitProgress, makeEvent } from "../../utils/events.js";
28
+ import { resolveRole } from "../../config.js";
29
+ import { classifyIntent } from "../../guards/intent-guard.js";
30
+ import { persistInlineDomain } from "../../domains/domain-loader.js";
31
+ import { detectTestFramework } from "../../utils/project-detect.js";
32
+ import { detectNeededSkills, autoInstallSkills } from "../../skills/skill-detector.js";
33
+ import { refineSkillsSemantically, resolveSkillsMode } from "../../skills/semantic-detector.js";
34
+ import { saveSession } from "../../session/store.js";
35
+ import {
36
+ setPreflight, setAutoInstalledSkills, setSkillsRecommended,
37
+ } from "../../session/mutators.js";
38
+ import {
39
+ runResearcherStage, runArchitectStage, runPlannerStage,
40
+ runDiscoverStage, runHuReviewerStage,
41
+ } from "../pre-loop-stages.js";
42
+ // TSK-0336: triage goes through the StageRegistry / StageExecutor contract
43
+ // so canRun / execute / onFailure are actually exercised in production.
44
+ // The registry is a singleton with TriageStage / CoderStage / ReviewerStage
45
+ // pre-registered; `runStage` returns null when `canRun` vetoes execution.
46
+ import { stageRegistry } from "../stages/stage-classes.js";
47
+ import { runStage } from "../stages/stage-executor.js";
48
+ import { runDomainCuratorStage } from "../stages/domain-curator-stage.js";
49
+ import { runPreflightChecks } from "../preflight-checks.js";
50
+ import { injectLoadedPlan } from "./pre-loop-phases/inject-loaded-plan.js";
51
+ import { emitConfigDeprecations } from "./pre-loop-phases/config-deprecations.js";
52
+ import { ensureAddyosmaniSkills } from "./pre-loop-phases/ensure-addyosmani-skills.js";
53
+ import { maybeGenerateAutoHuBatch } from "./pre-loop-phases/auto-hu-batch.js";
54
+
55
+ // Re-export for back-compat with any external caller; the canonical
56
+ // definitions now live in ./pre-loop-phases/.
57
+ export { ensureAddyosmaniSkills };
58
+ import {
59
+ applyTriageOverrides, applyAutoSimplify, applyFlagOverrides,
60
+ resolvePipelinePolicies, updateGitignoreForStack,
61
+ } from "../config-init.js";
62
+ import { tryCiComment } from "../ci-integration.js";
63
+ import { getIntegration } from "../integrations.js";
64
+
65
+ export async function runPreLoopStages({ config, logger, emitter, eventBase, session, flags, pipelineFlags, coderRole, trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults, brainCtx }) {
66
+ // --- HU Reviewer (first stage, before everything else, opt-in) ---
67
+ const huFile = flags.huFile || null;
68
+ if (flags.enableHuReviewer !== undefined) pipelineFlags.huReviewerEnabled = Boolean(flags.enableHuReviewer);
69
+ if (pipelineFlags.huReviewerEnabled && huFile) {
70
+ const huResult = await runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile, askQuestion, pgStories: null });
71
+ stageResults.huReviewer = huResult.stageResult;
72
+ }
73
+
74
+ // --- Intent classifier (deterministic pre-triage, opt-in) ---
75
+ if (config.guards?.intent?.enabled) {
76
+ const intentResult = classifyIntent(task, config);
77
+ stageResults.intent = intentResult;
78
+ if (intentResult.classified) {
79
+ emitProgress(emitter, makeEvent("intent:classified", { ...eventBase, stage: "intent" }, {
80
+ message: `Intent classified: ${intentResult.taskType} (${intentResult.level}) — ${intentResult.message}`,
81
+ detail: intentResult
82
+ }));
83
+ }
84
+ }
85
+
86
+ // --- Discover (pre-triage, opt-in) ---
87
+ if (flags.enableDiscover !== undefined) pipelineFlags.discoverEnabled = Boolean(flags.enableDiscover);
88
+ if (pipelineFlags.discoverEnabled) {
89
+ const discoverResult = await runDiscoverStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
90
+ stageResults.discover = discoverResult.stageResult;
91
+ }
92
+
93
+ // --- Triage (always on) — routed through StageRegistry (TSK-0336) ---
94
+ // canRun here is `pipelineFlags.triageEnabled !== false`; triageEnabled is
95
+ // never set to false in production, so behavior matches the previous
96
+ // unconditional call. If a caller ever flips it off, runStage returns null
97
+ // and we skip apply+persist with reasonable defaults (no overrides, no
98
+ // stageResult).
99
+ const triageCtx = { config, logger, emitter, eventBase, session, coderRole, trackBudget, pipelineFlags };
100
+ const triageResult = await runStage(stageRegistry.get("triage"), triageCtx) ?? { roleOverrides: null, stageResult: null };
101
+ applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
102
+ stageResults.triage = triageResult.stageResult;
103
+
104
+ // --- Brain decisor (opt-in, intent-driven routing) ---
105
+ //
106
+ // When enabled, translates triage output + task text + CLI overrides into a
107
+ // structured Decision and applies it as pipelineFlags overrides. The rest
108
+ // of the orchestrator continues to branch on pipelineFlags as before — the
109
+ // Brain is a router that adjusts WHICH roles run, not HOW they run.
110
+ //
111
+ // Flag precedence:
112
+ // 1. flags.brain === "off" → forced off
113
+ // 2. flags.brain === "on" → forced on
114
+ // 3. config.brain.decisor.enabled (if set)
115
+ // 4. config.testHarness.defaultBrainDecisor (test harness = false)
116
+ // 5. true in production
117
+ const brainDefault = config?.testHarness?.defaultBrainDecisor ?? true;
118
+ let brainDecisorEnabled = config?.brain?.decisor?.enabled ?? brainDefault;
119
+ if (flags?.brain === "off") brainDecisorEnabled = false;
120
+ if (flags?.brain === "on") brainDecisorEnabled = true;
121
+ if (brainDecisorEnabled) {
122
+ try {
123
+ const { buildDecision, applyDecisionToFlags } = await import("../../brain/decisor.js");
124
+ const { createTracker, recordDecision, checkLimits } = await import("../../brain/decision-tracker.js");
125
+ const tracker = createTracker(session, { max: config?.brain?.decisor?.maxDecisions });
126
+ const limitStatus = checkLimits(tracker);
127
+ if (limitStatus.status === "ok") {
128
+ let decision = buildDecision({
129
+ triage: triageResult.stageResult,
130
+ task,
131
+ config,
132
+ overrides: {
133
+ // Commander variadic --force-role a b → flags.forceRole = ["a","b"].
134
+ // Back-compat: accept both singular (commander) and plural (legacy / API).
135
+ forceRoles: flags?.forceRole || flags?.forceRoles || [],
136
+ skipRoles: flags?.skipRole || flags?.skipRoles || [],
137
+ },
138
+ });
139
+ // If the decision is low-confidence or ambiguous, consult Solomon
140
+ // for a refined routing suggestion. Solomon's ruling is advisory —
141
+ // if it fails or returns nothing useful, the baseline decision stands.
142
+ if (decision.consultSolomon) {
143
+ try {
144
+ const { consultSolomonForRouting } = await import("../../brain/solomon-consult.js");
145
+ decision = await consultSolomonForRouting({
146
+ decision, triage: triageResult.stageResult, task, config, logger,
147
+ emitter, eventBase, session,
148
+ });
149
+ } catch (err) {
150
+ logger.warn(`Brain → Solomon consult failed (non-blocking): ${err.message}`);
151
+ }
152
+ }
153
+ const newFlags = applyDecisionToFlags(decision, pipelineFlags);
154
+ Object.assign(pipelineFlags, newFlags);
155
+ recordDecision(tracker, decision, {
156
+ taskType: triageResult.stageResult?.taskType,
157
+ level: triageResult.stageResult?.level,
158
+ });
159
+ emitProgress(emitter, makeEvent("brain:decision", { ...eventBase, stage: "brain" }, {
160
+ status: "ok",
161
+ message: `Brain routing: ${decision.rolesOn.length} role(s) active — ${decision.rationale}`,
162
+ detail: {
163
+ rolesOn: decision.rolesOn,
164
+ rolesOff: decision.rolesOff,
165
+ confidence: decision.confidence,
166
+ consultSolomon: decision.consultSolomon,
167
+ appliedOverrides: decision.appliedOverrides,
168
+ },
169
+ }));
170
+ } else {
171
+ logger.warn(`Brain decisor skipped: ${limitStatus.detail}`);
172
+ emitProgress(emitter, makeEvent("brain:decision", { ...eventBase, stage: "brain" }, {
173
+ status: "warn",
174
+ message: `Brain decisor skipped: ${limitStatus.status}`,
175
+ detail: limitStatus,
176
+ }));
177
+ }
178
+ } catch (err) {
179
+ logger.warn(`Brain decisor failed (non-blocking): ${err.message}`);
180
+ }
181
+ }
182
+
183
+ // --- Persist inline domain if provided via --domain flag ---
184
+ if (flags?.domain) {
185
+ try {
186
+ await persistInlineDomain(flags.domain, config.projectDir || process.cwd());
187
+ } catch (err) {
188
+ logger.warn(`Failed to persist inline domain: ${err.message}`);
189
+ }
190
+ }
191
+
192
+ // --- Domain Curator (after triage + skill auto-install, before planning phases) ---
193
+ const domainHints = triageResult.stageResult?.domainHints || [];
194
+ if (domainHints.length > 0 || config.projectDir) {
195
+ try {
196
+ const { domainContext, stageResult: dcStageResult } = await runDomainCuratorStage({
197
+ config, logger, emitter, eventBase, session, trackBudget,
198
+ domainHints, askQuestion
199
+ });
200
+ stageResults.domainCurator = dcStageResult;
201
+ if (domainContext) {
202
+ config = { ...config, domainContext };
203
+ }
204
+ } catch (err) {
205
+ logger.warn(`Domain Curator failed (non-blocking): ${err.message}`);
206
+ }
207
+ }
208
+
209
+ // --- HU Reviewer auto-activation from triage (post-triage, no huFile needed) ---
210
+ const triageRoles = new Set(triageResult.stageResult?.roles || []);
211
+ if (triageRoles.has("hu-reviewer") && !stageResults.huReviewer) {
212
+ pipelineFlags.huReviewerEnabled = true;
213
+ // Feed tracker card structured data to hu-reviewer when available
214
+ let pgStories = null;
215
+ if (pgTaskId && pgProject && session.pg_card) {
216
+ pgStories = getIntegration("tracker")?.buildHuStoriesFromCard?.(session.pg_card) ?? null;
217
+ }
218
+ const huResult = await runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile: null, askQuestion, pgStories });
219
+ stageResults.huReviewer = huResult.stageResult;
220
+ }
221
+
222
+ // --- Auto-simplify pipeline for simple tasks (before explicit flag overrides) ---
223
+ const simplified = applyAutoSimplify({
224
+ pipelineFlags,
225
+ triageLevel: triageResult.stageResult?.level || null,
226
+ config, flags, logger, emitter, eventBase
227
+ });
228
+ if (simplified) stageResults.triage.autoSimplified = true;
229
+
230
+ await getIntegration("tracker")?.handleDecomposition?.({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger });
231
+
232
+ applyFlagOverrides(pipelineFlags, flags);
233
+
234
+ // --- Auto-detect TDD applicability when methodology not explicitly set ---
235
+ if (!flags.methodology) {
236
+ const projectDir = config.projectDir || process.cwd();
237
+ const detection = await detectTestFramework(projectDir);
238
+ if (!detection.hasTests) {
239
+ config = { ...config, development: { ...config.development, methodology: "standard", require_test_changes: false } };
240
+ logger.info("No test framework detected — using standard methodology");
241
+ } else {
242
+ config = { ...config, development: { ...config.development, methodology: "tdd", require_test_changes: true } };
243
+ logger.info(`Test framework detected (${detection.framework}) — using TDD methodology`);
244
+ }
245
+ emitProgress(emitter, makeEvent("tdd:auto-detect", { ...eventBase, stage: "pre-loop" }, {
246
+ message: detection.hasTests
247
+ ? `TDD auto-detected: ${detection.framework}`
248
+ : "TDD skipped: no test framework found",
249
+ detail: detection
250
+ }));
251
+ }
252
+
253
+ let updatedConfig = resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags });
254
+
255
+ // Deprecation warnings recorded at config load time. Done here, after
256
+ // policies are resolved, so the message lands in context: if the user's
257
+ // legacy `sonarqube.enabled: false` is being ignored, this is the right
258
+ // moment for them to see it.
259
+ emitConfigDeprecations(updatedConfig, logger, emitter, eventBase);
260
+
261
+ // --- Preflight environment checks ---
262
+ const preflightResult = await runPreflightChecks({
263
+ config: updatedConfig, logger, emitter, eventBase,
264
+ resolvedPolicies: session.resolved_policies,
265
+ securityEnabled: pipelineFlags.securityEnabled
266
+ });
267
+ setPreflight(session, preflightResult);
268
+ await saveSession(session);
269
+
270
+ // Hard fail if blocking checks failed (SonarQube enabled but not available)
271
+ if (!preflightResult.ok) {
272
+ const errorLines = (preflightResult.errors || [])
273
+ .map(e => ` - ${e.message}\n Fix: ${e.fix}`)
274
+ .join("\n");
275
+ throw new Error(
276
+ `Preflight FAILED — environment changed during session. Fix the issues and retry:\n${errorLines}`
277
+ );
278
+ }
279
+
280
+ if (preflightResult.configOverrides.securityDisabled) {
281
+ pipelineFlags.securityEnabled = false;
282
+ }
283
+
284
+ // --- Plan injection: skip researcher/architect/planner if a persisted plan is loaded ---
285
+ // Extracted to pre-loop-phases/inject-loaded-plan.js (PR-K).
286
+ // 189 LOC of plan-load + tests-gate + auto-certify + assertRunnable +
287
+ // huBatch + --hu filter + callback wiring + plannedTask build.
288
+ const planInjection = await injectLoadedPlan({
289
+ flags, updatedConfig, session, stageResults, eventBase, emitter, logger, task,
290
+ });
291
+ if (planInjection.handled) {
292
+ return { plannedTask: planInjection.plannedTask, updatedConfig };
293
+ }
294
+
295
+
296
+ // --- Researcher → Planner ---
297
+ const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion, brainCtx });
298
+
299
+ // --- Update .gitignore with stack-specific entries based on planner/architect output ---
300
+ const projectDir = updatedConfig.projectDir || process.cwd();
301
+ await updateGitignoreForStack(projectDir, { stageResults, task, logger });
302
+
303
+ // --- Auto-HU: when triage recommended decomposition and no manual huFile, generate HU batch ---
304
+ await maybeGenerateAutoHuBatch({
305
+ flags, stageResults, task, plannedTask, logger, emitter, eventBase, projectDir, session
306
+ });
307
+
308
+ // --- Auto-install skills based on task + planner output + project detection ---
309
+ // Runs AFTER triage and planner so that the planned task text (which includes
310
+ // planner output like implementation steps) is available for keyword detection.
311
+ // This ensures greenfield projects with no package.json still get correct skills.
312
+ //
313
+ // Detection runs UNCONDITIONALLY — even when the openskills CLI is missing —
314
+ // so the session report can recommend which skills would have been used. The
315
+ // actual install is skipped inside autoInstallSkills when unavailable.
316
+ const skillProjectDir = updatedConfig.projectDir || process.cwd();
317
+ try {
318
+ const skillsMode = resolveSkillsMode(updatedConfig, flags);
319
+ if (skillsMode === "none") {
320
+ // User explicitly opted out. Skip detection entirely.
321
+ return { plannedTask, updatedConfig };
322
+ }
323
+
324
+ // FIRST source: addyosmani/agent-skills (process skills per role).
325
+ // This runs BEFORE OpenSkills detection because process skills are the
326
+ // canonical workflows (TDD, code-review, security, performance...) that
327
+ // should shape every role's prompt; stack skills come second.
328
+ await ensureAddyosmaniSkills({
329
+ task: plannedTask,
330
+ config: updatedConfig,
331
+ logger,
332
+ session,
333
+ emitter,
334
+ eventBase,
335
+ });
336
+
337
+ let neededSkills = skillsMode === "semantic"
338
+ ? []
339
+ : await detectNeededSkills(plannedTask, skillProjectDir);
340
+ // Semantic mode augments regex detection with a classifier call.
341
+ // "auto" = regex always + semantic when budget allows (v1: always when mode=auto and classifier reachable).
342
+ if (skillsMode === "auto" || skillsMode === "semantic") {
343
+ const extra = await refineSkillsSemantically({
344
+ task: plannedTask,
345
+ alreadyDetected: neededSkills,
346
+ config: updatedConfig,
347
+ logger,
348
+ });
349
+ if (extra.length > 0) {
350
+ logger.info(`Semantic skill detection added: ${extra.join(", ")}`);
351
+ neededSkills = Array.from(new Set([...neededSkills, ...extra]));
352
+ }
353
+ }
354
+ if (neededSkills.length > 0) {
355
+ const skillResult = await autoInstallSkills(neededSkills, skillProjectDir);
356
+ if (skillResult.installed.length > 0) {
357
+ setAutoInstalledSkills(session, skillResult.installed);
358
+ }
359
+ if (skillResult.osAvailable === false && skillResult.wouldHaveUsed.length > 0) {
360
+ setSkillsRecommended(session, skillResult.wouldHaveUsed);
361
+ logger.warn(`OpenSkills CLI not available — would have used: ${skillResult.wouldHaveUsed.join(", ")}. Install openskills globally to auto-inject them next time.`);
362
+ emitProgress(emitter, makeEvent("skills:unavailable", { ...eventBase, stage: "skills" }, {
363
+ message: `OpenSkills CLI not available — would have used: ${skillResult.wouldHaveUsed.join(", ")}`,
364
+ detail: {
365
+ wouldHaveUsed: skillResult.wouldHaveUsed,
366
+ hint: "Install openskills globally: npm install -g openskills",
367
+ },
368
+ }));
369
+ }
370
+ emitProgress(emitter, makeEvent("skills:auto-install", { ...eventBase, stage: "skills" }, {
371
+ message: skillResult.installed.length > 0
372
+ ? `Auto-installed ${skillResult.installed.length} skill(s): ${skillResult.installed.join(", ")}`
373
+ : `Skills detected (${neededSkills.join(", ")}) — all already installed or unavailable`,
374
+ detail: skillResult,
375
+ }));
376
+ }
377
+ } catch (err) {
378
+ logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
379
+ }
380
+
381
+ return { plannedTask, updatedConfig };
382
+ }
383
+ export async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion, brainCtx }) {
384
+ let researchContext = null;
385
+ let plannedTask = task;
386
+
387
+ // Brain: track compression across pre-loop roles
388
+ const brainCompress = brainCtx?.enabled
389
+ ? (await import("../brain-coordinator.js")).processRoleOutput
390
+ : null;
391
+
392
+ if (pipelineFlags.researcherEnabled) {
393
+ const researcherResult = await runResearcherStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
394
+ researchContext = researcherResult.researchContext;
395
+ stageResults.researcher = researcherResult.stageResult;
396
+ if (brainCompress) brainCompress(brainCtx, { roleName: "researcher", output: researcherResult.stageResult, iteration: 0 });
397
+ }
398
+
399
+ // --- Architect (between researcher and planner) ---
400
+ let architectContext = null;
401
+ if (pipelineFlags.architectEnabled) {
402
+ const architectResult = await runArchitectStage({
403
+ config, logger, emitter, eventBase, session, coderRole, trackBudget,
404
+ researchContext,
405
+ discoverResult: stageResults.discover || null,
406
+ triageLevel: stageResults.triage?.level || null,
407
+ askQuestion
408
+ });
409
+ architectContext = architectResult.architectContext;
410
+ stageResults.architect = architectResult.stageResult;
411
+ if (brainCompress) brainCompress(brainCtx, { roleName: "architect", output: architectResult.stageResult, iteration: 0 });
412
+ }
413
+
414
+ const triageDecomposition = stageResults.triage?.shouldDecompose ? stageResults.triage.subtasks : null;
415
+ if (pipelineFlags.plannerEnabled) {
416
+ const plannerRole = resolveRole(config, "planner");
417
+ const plannerResult = await runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, architectContext, triageDecomposition, trackBudget });
418
+ plannedTask = plannerResult.plannedTask;
419
+ stageResults.planner = plannerResult.stageResult;
420
+ if (brainCompress) brainCompress(brainCtx, { roleName: "planner", output: plannerResult.stageResult, iteration: 0 });
421
+
422
+ await tryCiComment({
423
+ config, session, logger,
424
+ agent: "Planner",
425
+ body: `Plan: ${plannerResult.stageResult?.summary || plannedTask}`
426
+ });
427
+ }
428
+
429
+ return { plannedTask };
430
+ }
431
+ // `emitConfigDeprecations`, `ensureAddyosmaniSkills` and
432
+ // `maybeGenerateAutoHuBatch` were extracted to ./pre-loop-phases/ in
433
+ // TSK-0337 to keep this driver under the 600-LOC ceiling. Imports at
434
+ // the top of this file pull them back in; behaviour is unchanged.