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
package/src/config.js CHANGED
@@ -1,636 +1,34 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import yaml from "js-yaml";
4
- import { ensureDir, exists } from "./utils/fs.js";
5
- import { getKarajanHome } from "./utils/paths.js";
6
- import { safeParseConfig, formatConfigIssues } from "./config/schema.js";
7
-
8
1
  /**
2
+ * Karajan config barrel — public re-exports for back-compat.
3
+ *
4
+ * Pre-v2.7.5 this file was 636 LOC with fan-in 28: it bundled DEFAULTS
5
+ * data, loadConfig/writeConfig I/O, applyRunOverrides merge logic,
6
+ * resolveRole lookup and validateConfig schema gate — touching any of
7
+ * those risked rippling to a quarter of the codebase. TSK-0332 (Oleada 2
8
+ * of the audit refactor) split along the natural seams:
9
+ *
10
+ * - `src/config/defaults.js` — pure data, DEFAULTS
11
+ * - `src/config/loader.js` — fs/yaml I/O (getConfigPath,
12
+ * loadConfig, writeConfig, etc.)
13
+ * - `src/config/overrides.js` — CLI-flag merge (applyRunOverrides)
14
+ * - `src/config/role-resolver.js` — provider/model/validate
15
+ * - `src/config/schema.js` — Valibot schema (pre-existing)
16
+ *
17
+ * This file is now a thin barrel so the 28+ consumers importing from
18
+ * `src/config.js` keep working unchanged. New code should import from
19
+ * the specific submodule.
20
+ *
9
21
  * @typedef {import("./config/schema.js").KarajanConfig} KarajanConfig
10
22
  */
11
23
 
12
- const DEFAULTS = {
13
- coder: "claude",
14
- reviewer: "codex",
15
- roles: {
16
- planner: { provider: null, model: null },
17
- coder: { provider: null, model: null },
18
- reviewer: { provider: null, model: null },
19
- refactorer: { provider: null, model: null },
20
- solomon: { provider: null, model: null },
21
- researcher: { provider: null, model: null },
22
- tester: { provider: null, model: null },
23
- security: { provider: null, model: null },
24
- impeccable: { provider: null, model: null },
25
- triage: { provider: null, model: null },
26
- discover: { provider: null, model: null },
27
- architect: { provider: null, model: null },
28
- hu_reviewer: { provider: null, model: null }
29
- },
30
- pipeline: {
31
- planner: { enabled: false },
32
- refactorer: { enabled: false },
33
- solomon: { enabled: true },
34
- researcher: { enabled: false },
35
- tester: { enabled: true },
36
- security: { enabled: true },
37
- impeccable: { enabled: false },
38
- triage: { enabled: true },
39
- discover: { enabled: false },
40
- architect: { enabled: false },
41
- hu_reviewer: { enabled: false },
42
- brain: { enabled: true },
43
- auto_simplify: true
44
- },
45
- brain: {
46
- enabled: true,
47
- provider: "claude",
48
- bypass_solomon_on_correctness: true,
49
- max_consecutive_verification_failures: 2
50
- },
51
- review_mode: "standard",
52
- max_iterations: 5,
53
- hu_max_iterations: 3,
54
- max_budget_usd: null,
55
- review_rules: "./.karajan/review-rules.md",
56
- coder_rules: "./.karajan/coder-rules.md",
57
- base_branch: "main",
58
- coder_options: { model: null, auto_approve: true, fallback_coder: null },
59
- reviewer_options: {
60
- output_format: "json",
61
- require_schema: true,
62
- model: null,
63
- deterministic: true,
64
- retries: 1,
65
- fallback_reviewer: null
66
- },
67
- development: {
68
- methodology: "tdd",
69
- require_test_changes: true,
70
- test_file_patterns: ["/tests/", "/__tests__/", ".test.", ".spec."],
71
- source_file_extensions: [".js", ".jsx", ".ts", ".tsx", ".py", ".go", ".java", ".rb", ".php", ".cs"]
72
- },
73
- sonarqube: {
74
- enabled: true,
75
- host: "http://localhost:9000",
76
- external: false,
77
- container_name: "karajan-sonarqube",
78
- network: "karajan_sonar_net",
79
- volumes: {
80
- data: "karajan_sonar_data",
81
- logs: "karajan_sonar_logs",
82
- extensions: "karajan_sonar_extensions"
83
- },
84
- timeouts: {
85
- healthcheck_seconds: 5,
86
- compose_up_ms: 300000,
87
- compose_control_ms: 120000,
88
- logs_ms: 30000,
89
- scanner_ms: 900000
90
- },
91
- token: null,
92
- project_key: null,
93
- admin_user: null,
94
- admin_password: null,
95
- coverage: {
96
- enabled: false,
97
- command: null,
98
- timeout_ms: 300000,
99
- block_on_failure: true,
100
- lcov_report_path: null
101
- },
102
- quality_gate: true,
103
- enforcement_profile: "pragmatic",
104
- gate_block_on: [
105
- "new_reliability_rating=E",
106
- "new_security_rating=E",
107
- "new_maintainability_rating=E",
108
- "new_coverage<80",
109
- "new_duplicated_lines_density>5"
110
- ],
111
- fail_on: ["BLOCKER", "CRITICAL"],
112
- ignore_on: ["INFO"],
113
- max_scan_retries: 3,
114
- scanner: {
115
- sources: "src,public,lib",
116
- exclusions: "**/node_modules/**,**/fake-apps/**,**/scripts/**,**/playground/**,**/dist/**,**/build/**,**/*.min.js",
117
- test_inclusions: "**/*.test.js,**/*.spec.js,**/tests/**,**/__tests__/**",
118
- coverage_exclusions: "**/tests/**,**/__tests__/**,**/*.test.js,**/*.spec.js",
119
- disabled_rules: ["javascript:S1116", "javascript:S3776"]
120
- }
121
- },
122
- sonarcloud: {
123
- enabled: false,
124
- organization: null,
125
- token: null,
126
- project_key: null,
127
- host: "https://sonarcloud.io",
128
- scanner: {
129
- sources: "src,public,lib",
130
- exclusions: "**/node_modules/**,**/dist/**,**/build/**,**/*.min.js",
131
- test_inclusions: "**/*.test.js,**/*.spec.js,**/tests/**,**/__tests__/**"
132
- }
133
- },
134
- hu_board: {
135
- enabled: false,
136
- port: 4000,
137
- auto_start: false
138
- },
139
- language: "en",
140
- hu_language: "en",
141
- policies: {},
142
- serena: { enabled: false },
143
- planning_game: { enabled: false, project_id: null, codeveloper: null },
144
- skills: {
145
- mode: "auto",
146
- sources: ["addyosmani", "openskills", "local"],
147
- addyosmani: {
148
- enabled: true,
149
- refreshDays: 7,
150
- repoUrl: "https://github.com/addyosmani/agent-skills.git"
151
- }
152
- },
153
- ci: { enabled: false, review_event: "kj-review", comment_event: "kj-comment", comment_prefix: true },
154
- git: { auto_commit: false, auto_push: false, auto_pr: false, auto_rebase: true, branch_prefix: "feat/" },
155
- output: { report_dir: "./.reviews", log_level: "info", quiet: true },
156
- budget: {
157
- warn_threshold_pct: 80,
158
- currency: "usd",
159
- exchange_rate_eur: 0.92
160
- },
161
- model_selection: {
162
- enabled: true,
163
- tiers: {},
164
- role_overrides: {}
165
- },
166
- session: {
167
- max_iteration_minutes: 30,
168
- max_total_minutes: 120,
169
- max_planner_minutes: 60,
170
- checkpoint_interval_minutes: 5,
171
- max_agent_silence_minutes: 20,
172
- fail_fast_repeats: 2,
173
- repeat_detection_threshold: 2,
174
- max_sonar_retries: 3,
175
- max_reviewer_retries: 3,
176
- max_tester_retries: 1,
177
- max_security_retries: 1,
178
- max_auto_resumes: 2,
179
- expiry_days: 30
180
- },
181
- failFast: {
182
- repeatThreshold: 2
183
- },
184
- webperf: {
185
- enabled: true,
186
- devtools_mcp: false,
187
- thresholds: { lcp: 2500, cls: 0.1, inp: 200 }
188
- },
189
- telemetry: true,
190
- guards: {
191
- output: {
192
- enabled: true,
193
- patterns: [],
194
- protected_files: [],
195
- on_violation: "block"
196
- },
197
- perf: {
198
- enabled: true,
199
- patterns: [],
200
- block_on_warning: false,
201
- frontend_extensions: []
202
- },
203
- intent: {
204
- enabled: false,
205
- patterns: [],
206
- confidence_threshold: 0.85
207
- }
208
- }
209
- };
210
-
211
- function mergeDeep(base, override) {
212
- const output = { ...base };
213
- for (const [key, value] of Object.entries(override || {})) {
214
- if (Array.isArray(value)) {
215
- output[key] = value;
216
- } else if (value && typeof value === "object") {
217
- output[key] = mergeDeep(base[key] || {}, value);
218
- } else {
219
- output[key] = value;
220
- }
221
- }
222
- return output;
223
- }
224
-
225
- export function getConfigPath() {
226
- return path.join(getKarajanHome(), "kj.config.yml");
227
- }
228
-
229
- export function getProjectConfigPath(projectDir = process.cwd()) {
230
- return path.join(projectDir, ".karajan", "kj.config.yml");
231
- }
232
-
233
- export async function loadProjectConfig(projectDir = process.cwd()) {
234
- const projectConfigPath = getProjectConfigPath(projectDir);
235
- if (!(await exists(projectConfigPath))) {
236
- return null;
237
- }
238
- const raw = await fs.readFile(projectConfigPath, "utf8");
239
- return yaml.load(raw, { json: true }) || {};
240
- }
241
-
242
- async function loadProjectPricingOverrides(projectDir = process.cwd()) {
243
- const projectConfigPath = path.join(projectDir, ".karajan.yml");
244
- if (!(await exists(projectConfigPath))) {
245
- return null;
246
- }
247
-
248
- const raw = await fs.readFile(projectConfigPath, "utf8");
249
- const parsed = yaml.load(raw, { json: true }) || {};
250
- const pricing = parsed?.budget?.pricing;
251
- if (!pricing || typeof pricing !== "object") {
252
- return null;
253
- }
254
-
255
- return pricing;
256
- }
257
-
258
- export async function loadConfig(projectDir) {
259
- const configPath = getConfigPath();
260
- const projectPricing = await loadProjectPricingOverrides(projectDir);
261
-
262
- // Load global config
263
- let globalConfig = {};
264
- const globalExists = await exists(configPath);
265
- if (globalExists) {
266
- const raw = await fs.readFile(configPath, "utf8");
267
- globalConfig = yaml.load(raw, { json: true }) || {};
268
- }
269
-
270
- // Load project config (.karajan/kj.config.yml)
271
- const projectConfig = await loadProjectConfig(projectDir);
272
-
273
- // Merge: DEFAULTS < global < project
274
- let merged = mergeDeep(DEFAULTS, globalConfig);
275
- if (projectConfig) {
276
- merged = mergeDeep(merged, projectConfig);
277
- }
278
-
279
- if (projectPricing) {
280
- merged.budget = mergeDeep(merged.budget || {}, { pricing: projectPricing });
281
- }
282
-
283
- // KJC-TSK-0318 — schema-validate the merged config so invalid values
284
- // (review_mode typos, max_iterations=0, out-of-range ports) surface at
285
- // load time with a clear error instead of crashing deep inside the
286
- // orchestrator. safeParse + explicit error prefix keeps the message
287
- // actionable.
288
- const validation = safeParseConfig(merged);
289
- if (!validation.success) {
290
- throw new Error(`Invalid Karajan config:\n${formatConfigIssues(validation.issues)}`);
291
- }
292
-
293
- // Deprecation tracking — `sonarqube.enabled` was a footgun: users would
294
- // set it to `false` in their global ~/.karajan/kj.config.yml and then
295
- // wonder why Sonar didn't run on a code task. Since v2.7.4 Sonar is
296
- // intrinsic to Karajan for code tasks; the field is ignored. We surface
297
- // a warning at run start (see emitConfigDeprecations in flow-runner.js)
298
- // so users notice and clean up their config. Don't throw — keep
299
- // back-compat so existing scripts keep working.
300
- const userSetSonarEnabled = (globalConfig?.sonarqube?.enabled !== undefined)
301
- || (projectConfig?.sonarqube?.enabled !== undefined);
302
- if (userSetSonarEnabled) {
303
- merged._deprecated = merged._deprecated || {};
304
- merged._deprecated.sonarqubeEnabledKey = true;
305
- }
306
-
307
- return { config: merged, path: configPath, exists: globalExists, hasProjectConfig: !!projectConfig };
308
- }
309
-
310
- export async function writeConfig(configPath, config) {
311
- await ensureDir(path.dirname(configPath));
312
- await fs.writeFile(configPath, yaml.dump(config, { lineWidth: 120 }), "utf8");
313
- }
314
-
315
- // Declarative mappings for applyRunOverrides to reduce cognitive complexity.
316
-
317
- // Role provider flags: [flagName, roleName] — truthy check
318
- const ROLE_PROVIDER_FLAGS = [
319
- ["planner", "planner"], ["coder", "coder"], ["reviewer", "reviewer"],
320
- ["refactorer", "refactorer"], ["solomon", "solomon"], ["researcher", "researcher"],
321
- ["tester", "tester"], ["security", "security"], ["triage", "triage"],
322
- ["discover", "discover"], ["architect", "architect"]
323
- ];
324
-
325
- // Role model flags: [flagName, roleName] — truthy check, String coercion
326
- const ROLE_MODEL_FLAGS = [
327
- ["plannerModel", "planner"], ["coderModel", "coder"], ["reviewerModel", "reviewer"],
328
- ["refactorerModel", "refactorer"], ["solomonModel", "solomon"], ["discoverModel", "discover"],
329
- ["architectModel", "architect"]
330
- ];
331
-
332
- // Pipeline enable flags: [flagName, pipelineKey] — !== undefined check, Boolean coercion
333
- const PIPELINE_ENABLE_FLAGS = [
334
- ["enablePlanner", "planner"], ["enableRefactorer", "refactorer"],
335
- ["enableSolomon", "solomon"], ["enableResearcher", "researcher"],
336
- ["enableTester", "tester"], ["enableSecurity", "security"], ["enableImpeccable", "impeccable"],
337
- ["enableTriage", "triage"], ["enableDiscover", "discover"],
338
- ["enableArchitect", "architect"],
339
- ["enableHuReviewer", "hu_reviewer"]
340
- ];
341
-
342
- const AUTO_SIMPLIFY_FLAG = "autoSimplify";
343
-
344
- // Scalar flags: [flagName, setter] — truthy check.
345
- // Note: max_iterations was previously here but `0` was silently ignored
346
- // (truthy check skipped it). It now lives in UNDEF_CHECK_FLAGS so
347
- // `--max-iterations 0` reaches the schema validator and errors cleanly
348
- // instead of falling through to the default (KJC-TSK-0318).
349
- const SCALAR_FLAGS = [
350
- ["mode", (out, v) => { out.review_mode = v; }],
351
- ["maxIterationMinutes", (out, v) => { out.session.max_iteration_minutes = Number(v); }],
352
- ["maxTotalMinutes", (out, v) => { out.session.max_total_minutes = Number(v); }],
353
- ["checkpointInterval", (out, v) => { out.session.checkpoint_interval_minutes = Number(v); }],
354
- ["baseBranch", (out, v) => { out.base_branch = v; }],
355
- ["coderFallback", (out, v) => { out.coder_options.fallback_coder = v; }],
356
- ["reviewerFallback", (out, v) => { out.reviewer_options.fallback_reviewer = v; }],
357
- ["taskType", (out, v) => { out.taskType = String(v); }],
358
- ["branchPrefix", (out, v) => { out.git.branch_prefix = String(v); }]
359
- ];
360
-
361
- // Boolean/undefined-check flags: [flagName, setter] — !== undefined check.
362
- // These preserve falsy values (false, 0) so --no-rebase correctly sets
363
- // auto_rebase=false and --reviewer-retries 0 correctly disables retries.
364
- const UNDEF_CHECK_FLAGS = [
365
- ["maxIterations", (out, v) => { out.max_iterations = Number(v); }],
366
- ["reviewerRetries", (out, v) => { out.reviewer_options.retries = Number(v); }],
367
- ["autoCommit", (out, v) => { out.git.auto_commit = Boolean(v); }],
368
- ["autoPush", (out, v) => { out.git.auto_push = Boolean(v); }],
369
- ["autoPr", (out, v) => { out.git.auto_pr = Boolean(v); }],
370
- ["autoRebase", (out, v) => { out.git.auto_rebase = Boolean(v); }],
371
- ["enableSerena", (out, v) => { out.serena.enabled = Boolean(v); }]
372
- ];
373
-
374
- function applyRoleOverrides(out, flags) {
375
- for (const [flag, role] of ROLE_PROVIDER_FLAGS) {
376
- if (flags[flag]) out.roles[role].provider = flags[flag];
377
- }
378
- // coder/reviewer also update top-level aliases
379
- if (flags.coder) out.coder = flags.coder;
380
- if (flags.reviewer) out.reviewer = flags.reviewer;
381
-
382
- for (const [flag, role] of ROLE_MODEL_FLAGS) {
383
- if (flags[flag]) out.roles[role].model = String(flags[flag]);
384
- }
385
- // reviewerModel also updates reviewer_options
386
- if (flags.reviewerModel) out.reviewer_options.model = String(flags.reviewerModel);
387
- }
388
-
389
- function applyPipelineOverrides(out, flags) {
390
- for (const [flag, key] of PIPELINE_ENABLE_FLAGS) {
391
- if (flags[flag] !== undefined) out.pipeline[key].enabled = Boolean(flags[flag]);
392
- }
393
- if (flags.enableReviewer !== undefined) {
394
- out.pipeline.reviewer = out.pipeline.reviewer || {};
395
- out.pipeline.reviewer.enabled = Boolean(flags.enableReviewer);
396
- }
397
- }
398
-
399
- function applyScalarAndBooleanOverrides(out, flags) {
400
- for (const [flag, setter] of SCALAR_FLAGS) {
401
- if (flags[flag]) setter(out, flags[flag]);
402
- }
403
- for (const [flag, setter] of UNDEF_CHECK_FLAGS) {
404
- if (flags[flag] !== undefined) setter(out, flags[flag]);
405
- }
406
- }
407
-
408
- function applyMethodologyOverride(out, flags) {
409
- if (!flags.methodology) return;
410
- const methodology = String(flags.methodology).toLowerCase();
411
- out.development.methodology = methodology;
412
- out.development.require_test_changes = methodology === "tdd";
413
- }
414
-
415
- function applyCiOverride(out, flags) {
416
- out.ci = out.ci || { enabled: false };
417
- if (flags.enableCi === undefined) return;
418
- out.ci.enabled = Boolean(flags.enableCi);
419
- // CI requires git automation (commit + push + PR)
420
- if (out.ci.enabled) {
421
- out.git.auto_commit = true;
422
- out.git.auto_push = true;
423
- out.git.auto_pr = true;
424
- }
425
- }
426
-
427
- function applyOutputModeOverrides(out, flags) {
428
- out.output = out.output || {};
429
- // --verbose explicitly overrides quiet
430
- if (flags.verbose === true) {
431
- out.output.quiet = false;
432
- } else if (flags.quiet === true) {
433
- out.output.quiet = true;
434
- }
435
- // quiet defaults to true (set in DEFAULTS)
436
- }
437
-
438
- function applyMiscOverrides(out, flags) {
439
- if (flags[AUTO_SIMPLIFY_FLAG] !== undefined) out.pipeline.auto_simplify = Boolean(flags[AUTO_SIMPLIFY_FLAG]);
440
- // `--no-sonar` and `sonarqube.enabled: false` are deprecated since v2.7.4.
441
- // Sonar is intrinsic to Karajan for code tasks (sw/refactor/add-tests) and
442
- // skipped for non-code tasks (audit/doc/infra/analysis/no-code) by policy.
443
- // The flag is still accepted to avoid breaking scripts; it now emits a
444
- // warning at run start and is otherwise ignored. See preflight-checks.js
445
- // and flow-runner.js for the actual gate (resolvedPolicies.sonar).
446
- if (flags.noSonar || flags.sonar === false) {
447
- out._deprecated = out._deprecated || {};
448
- out._deprecated.noSonarFlag = true;
449
- }
450
- out.sonarcloud = out.sonarcloud || {};
451
- if (flags.enableSonarcloud === true) out.sonarcloud.enabled = true;
452
- if (flags.noSonarcloud === true || flags.sonarcloud === false) out.sonarcloud.enabled = false;
453
-
454
- out.planning_game = out.planning_game || {};
455
- if (flags.pgTask) out.planning_game.enabled = true;
456
- if (flags.pgProject) out.planning_game.project_id = flags.pgProject;
457
-
458
- out.model_selection = out.model_selection || { enabled: true, tiers: {}, role_overrides: {} };
459
- if (flags.smartModels === true) out.model_selection.enabled = true;
460
- if (flags.smartModels === false || flags.noSmartModels === true) out.model_selection.enabled = false;
461
- }
462
-
463
- export function applyRunOverrides(config, flags) {
464
- const out = mergeDeep(config, {});
465
- out.coder_options = out.coder_options || {};
466
- out.reviewer_options = out.reviewer_options || {};
467
- out.session = out.session || {};
468
- out.git = out.git || {};
469
- out.development = out.development || {};
470
- out.sonarqube = out.sonarqube || {};
471
- if (out.max_budget_usd === undefined || out.max_budget_usd === null) {
472
- out.max_budget_usd = out.session.max_budget_usd ?? null;
473
- }
474
- out.budget = mergeDeep(DEFAULTS.budget, out.budget || {});
475
- out.roles = mergeDeep(DEFAULTS.roles, out.roles || {});
476
- out.pipeline = mergeDeep(DEFAULTS.pipeline, out.pipeline || {});
477
- out.serena = out.serena || { enabled: false };
478
-
479
- applyRoleOverrides(out, flags);
480
- applyPipelineOverrides(out, flags);
481
- applyScalarAndBooleanOverrides(out, flags);
482
- applyMethodologyOverride(out, flags);
483
- applyCiOverride(out, flags);
484
- applyMiscOverrides(out, flags);
485
- applyOutputModeOverrides(out, flags);
486
-
487
- // KJC-TSK-0318 — re-run schema validation after overrides so CLI flags
488
- // that would produce an invalid config (e.g. --max-iterations 0) fail
489
- // fast with the same clear message loadConfig would have produced.
490
- const validation = safeParseConfig(out);
491
- if (!validation.success) {
492
- throw new Error(`Invalid Karajan config after CLI overrides:\n${formatConfigIssues(validation.issues)}`);
493
- }
494
-
495
- return out;
496
- }
497
-
498
- /**
499
- * Check if a model string is compatible with an agent provider.
500
- * Only returns false when the model clearly belongs to a DIFFERENT provider.
501
- * Returns true if we can't determine or if the model is ambiguous.
502
- */
503
- const AGENT_MODEL_SIGNATURES = {
504
- claude: ["claude", "sonnet", "opus", "haiku"],
505
- codex: ["o4-", "o3-", "gpt-", "codex"],
506
- gemini: ["gemini", "flash-"]
507
- };
508
-
509
- function isModelCompatible(agent, model) {
510
- if (!model || !agent) return true;
511
- const lower = model.toLowerCase();
512
-
513
- // Check if model clearly belongs to a different provider
514
- for (const [provider, signatures] of Object.entries(AGENT_MODEL_SIGNATURES)) {
515
- if (provider === agent) continue;
516
- if (signatures.some(s => lower.includes(s))) {
517
- // Model belongs to a different provider — incompatible
518
- return false;
519
- }
520
- }
521
-
522
- // Model doesn't clearly belong to any other provider — allow it
523
- return true;
524
- }
525
-
526
- // Roles that inherit provider/model from the coder when not explicitly configured
527
- const CODER_INHERITED_ROLES = new Set([
528
- "planner", "refactorer", "solomon", "researcher", "tester", "security",
529
- "impeccable", "triage", "discover", "architect", "audit", "hu_reviewer", "hu-reviewer"
530
- ]);
531
-
532
- function resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer) {
533
- if (roleConfig.provider) return roleConfig.provider;
534
-
535
- // If model has "provider/model" format (e.g. "gemini/pro"), extract the provider
536
- if (roleConfig.model && roleConfig.model.includes("/")) {
537
- const inferredProvider = roleConfig.model.split("/")[0].toLowerCase();
538
- if (AGENT_MODEL_SIGNATURES[inferredProvider]) return inferredProvider;
539
- }
540
-
541
- if (role === "coder") return legacyCoder;
542
- if (role === "reviewer") return legacyReviewer;
543
- if (CODER_INHERITED_ROLES.has(role)) return roles.coder?.provider || legacyCoder;
544
- return null;
545
- }
546
-
547
- function resolveModel(roleConfig, role, config) {
548
- if (roleConfig.model) {
549
- // Strip "provider/" prefix from models like "gemini/pro" → "pro"
550
- const model = roleConfig.model.includes("/")
551
- ? roleConfig.model.split("/").slice(1).join("/")
552
- : roleConfig.model;
553
- return { model, inherited: false };
554
- }
555
- if (role === "coder") return { model: config?.coder_options?.model ?? null, inherited: false };
556
- if (role === "reviewer") return { model: config?.reviewer_options?.model ?? null, inherited: false };
557
- if (CODER_INHERITED_ROLES.has(role)) {
558
- const model = config?.coder_options?.model ?? null;
559
- return { model, inherited: !!model };
560
- }
561
- return { model: null, inherited: false };
562
- }
563
-
564
- export function resolveRole(config, role) {
565
- const roles = config?.roles || {};
566
- const roleConfig = roles[role] || {};
567
- const legacyCoder = config?.coder || null;
568
- const legacyReviewer = config?.reviewer || null;
569
-
570
- const provider = resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer);
571
- let { model, inherited: modelIsInherited } = resolveModel(roleConfig, role, config);
572
-
573
- // Drop model if incompatible with the resolved provider (inherited or explicit)
574
- if (provider && model && !isModelCompatible(provider, model)) {
575
- model = null;
576
- }
577
-
578
- return { provider, model };
579
- }
580
-
581
- // Pipeline roles checked when commandName is "run": [pipelineKey, roleName]
582
- const RUN_PIPELINE_ROLES = [
583
- ["reviewer", "reviewer"], ["triage", "triage"], ["planner", "planner"],
584
- ["refactorer", "refactorer"], ["researcher", "researcher"],
585
- ["tester", "tester"], ["security", "security"], ["impeccable", "impeccable"]
586
- ];
587
-
588
- // Direct command-to-role mapping for non-"run" commands
589
- const COMMAND_ROLE_MAP = {
590
- discover: ["discover"],
591
- plan: ["planner"],
592
- code: ["coder"],
593
- review: ["reviewer"]
594
- };
595
-
596
- function requiredRolesFor(commandName, config) {
597
- if (commandName !== "run") {
598
- return COMMAND_ROLE_MAP[commandName] || [];
599
- }
600
- const required = ["coder"];
601
- for (const [pipelineKey, roleName] of RUN_PIPELINE_ROLES) {
602
- const pipelineEntry = config?.pipeline?.[pipelineKey];
603
- // reviewer defaults to enabled (only excluded if explicitly false)
604
- const isEnabled = pipelineKey === "reviewer"
605
- ? pipelineEntry?.enabled !== false
606
- : Boolean(pipelineEntry?.enabled);
607
- if (isEnabled) required.push(roleName);
608
- }
609
- return required;
610
- }
611
-
612
- export function validateConfig(config, commandName = "run") {
613
- const errors = [];
614
- if (!new Set(["paranoid", "strict", "standard", "relaxed", "custom"]).has(config.review_mode)) {
615
- errors.push(`Invalid review_mode: ${config.review_mode}`);
616
- }
617
- if (!new Set(["tdd", "standard"]).has(config.development?.methodology)) {
618
- errors.push(`Invalid development.methodology: ${config.development?.methodology}`);
619
- }
620
-
621
- const requiredRoles = requiredRolesFor(commandName, config);
622
- for (const role of requiredRoles) {
623
- const { provider } = resolveRole(config, role);
624
- if (!provider) {
625
- errors.push(
626
- `Missing provider for required role '${role}'. Set 'roles.${role}.provider' or pass '--${role} <name>'`
627
- );
628
- }
629
- }
630
-
631
- if (errors.length > 0) {
632
- throw new Error(errors.join("\n"));
633
- }
634
-
635
- return config;
636
- }
24
+ export { DEFAULTS } from "./config/defaults.js";
25
+ export {
26
+ getConfigPath,
27
+ getProjectConfigPath,
28
+ loadProjectConfig,
29
+ loadConfig,
30
+ writeConfig,
31
+ } from "./config/loader.js";
32
+ export { applyRunOverrides } from "./config/overrides.js";
33
+ export { resolveRole, validateConfig } from "./config/role-resolver.js";
34
+ export { safeParseConfig, formatConfigIssues } from "./config/schema.js";
@@ -3,7 +3,7 @@
3
3
  * Extracted from orchestrator.js for testability and reuse.
4
4
  */
5
5
 
6
- import { addCheckpoint } from "../session-store.js";
6
+ import { addCheckpoint } from "../session/store.js";
7
7
  import {
8
8
  ensureGitRepo,
9
9
  currentBranch,
@@ -80,7 +80,7 @@ export async function prepareGitAutomation({ config, task, logger, session }) {
80
80
  return { enabled: true, branch, baseBranch, autoRebase };
81
81
  }
82
82
 
83
- export function buildPrBody({ task, stageResults }) {
83
+ export function buildPrBody({ task: _task, stageResults }) {
84
84
  const sections = ["Created by Karajan Code."];
85
85
 
86
86
  const approach = stageResults?.planner?.approach;
@@ -1,5 +1,3 @@
1
- import { runCommand } from "../utils/process.js";
2
-
3
1
  // Built-in destructive patterns
4
2
  const DESTRUCTIVE_PATTERNS = [
5
3
  { id: "rm-rf", pattern: /rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+|--force\s+)*-[a-zA-Z]*r/, severity: "critical", message: "Recursive file deletion detected" },