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
package/src/config.js CHANGED
@@ -1,613 +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
- return { config: merged, path: configPath, exists: globalExists, hasProjectConfig: !!projectConfig };
294
- }
295
-
296
- export async function writeConfig(configPath, config) {
297
- await ensureDir(path.dirname(configPath));
298
- await fs.writeFile(configPath, yaml.dump(config, { lineWidth: 120 }), "utf8");
299
- }
300
-
301
- // Declarative mappings for applyRunOverrides to reduce cognitive complexity.
302
-
303
- // Role provider flags: [flagName, roleName] — truthy check
304
- const ROLE_PROVIDER_FLAGS = [
305
- ["planner", "planner"], ["coder", "coder"], ["reviewer", "reviewer"],
306
- ["refactorer", "refactorer"], ["solomon", "solomon"], ["researcher", "researcher"],
307
- ["tester", "tester"], ["security", "security"], ["triage", "triage"],
308
- ["discover", "discover"], ["architect", "architect"]
309
- ];
310
-
311
- // Role model flags: [flagName, roleName] — truthy check, String coercion
312
- const ROLE_MODEL_FLAGS = [
313
- ["plannerModel", "planner"], ["coderModel", "coder"], ["reviewerModel", "reviewer"],
314
- ["refactorerModel", "refactorer"], ["solomonModel", "solomon"], ["discoverModel", "discover"],
315
- ["architectModel", "architect"]
316
- ];
317
-
318
- // Pipeline enable flags: [flagName, pipelineKey] — !== undefined check, Boolean coercion
319
- const PIPELINE_ENABLE_FLAGS = [
320
- ["enablePlanner", "planner"], ["enableRefactorer", "refactorer"],
321
- ["enableSolomon", "solomon"], ["enableResearcher", "researcher"],
322
- ["enableTester", "tester"], ["enableSecurity", "security"], ["enableImpeccable", "impeccable"],
323
- ["enableTriage", "triage"], ["enableDiscover", "discover"],
324
- ["enableArchitect", "architect"],
325
- ["enableHuReviewer", "hu_reviewer"]
326
- ];
327
-
328
- const AUTO_SIMPLIFY_FLAG = "autoSimplify";
329
-
330
- // Scalar flags: [flagName, setter] — truthy check.
331
- // Note: max_iterations was previously here but `0` was silently ignored
332
- // (truthy check skipped it). It now lives in UNDEF_CHECK_FLAGS so
333
- // `--max-iterations 0` reaches the schema validator and errors cleanly
334
- // instead of falling through to the default (KJC-TSK-0318).
335
- const SCALAR_FLAGS = [
336
- ["mode", (out, v) => { out.review_mode = v; }],
337
- ["maxIterationMinutes", (out, v) => { out.session.max_iteration_minutes = Number(v); }],
338
- ["maxTotalMinutes", (out, v) => { out.session.max_total_minutes = Number(v); }],
339
- ["checkpointInterval", (out, v) => { out.session.checkpoint_interval_minutes = Number(v); }],
340
- ["baseBranch", (out, v) => { out.base_branch = v; }],
341
- ["coderFallback", (out, v) => { out.coder_options.fallback_coder = v; }],
342
- ["reviewerFallback", (out, v) => { out.reviewer_options.fallback_reviewer = v; }],
343
- ["taskType", (out, v) => { out.taskType = String(v); }],
344
- ["branchPrefix", (out, v) => { out.git.branch_prefix = String(v); }]
345
- ];
346
-
347
- // Boolean/undefined-check flags: [flagName, setter] — !== undefined check.
348
- // These preserve falsy values (false, 0) so --no-rebase correctly sets
349
- // auto_rebase=false and --reviewer-retries 0 correctly disables retries.
350
- const UNDEF_CHECK_FLAGS = [
351
- ["maxIterations", (out, v) => { out.max_iterations = Number(v); }],
352
- ["reviewerRetries", (out, v) => { out.reviewer_options.retries = Number(v); }],
353
- ["autoCommit", (out, v) => { out.git.auto_commit = Boolean(v); }],
354
- ["autoPush", (out, v) => { out.git.auto_push = Boolean(v); }],
355
- ["autoPr", (out, v) => { out.git.auto_pr = Boolean(v); }],
356
- ["autoRebase", (out, v) => { out.git.auto_rebase = Boolean(v); }],
357
- ["enableSerena", (out, v) => { out.serena.enabled = Boolean(v); }]
358
- ];
359
-
360
- function applyRoleOverrides(out, flags) {
361
- for (const [flag, role] of ROLE_PROVIDER_FLAGS) {
362
- if (flags[flag]) out.roles[role].provider = flags[flag];
363
- }
364
- // coder/reviewer also update top-level aliases
365
- if (flags.coder) out.coder = flags.coder;
366
- if (flags.reviewer) out.reviewer = flags.reviewer;
367
-
368
- for (const [flag, role] of ROLE_MODEL_FLAGS) {
369
- if (flags[flag]) out.roles[role].model = String(flags[flag]);
370
- }
371
- // reviewerModel also updates reviewer_options
372
- if (flags.reviewerModel) out.reviewer_options.model = String(flags.reviewerModel);
373
- }
374
-
375
- function applyPipelineOverrides(out, flags) {
376
- for (const [flag, key] of PIPELINE_ENABLE_FLAGS) {
377
- if (flags[flag] !== undefined) out.pipeline[key].enabled = Boolean(flags[flag]);
378
- }
379
- if (flags.enableReviewer !== undefined) {
380
- out.pipeline.reviewer = out.pipeline.reviewer || {};
381
- out.pipeline.reviewer.enabled = Boolean(flags.enableReviewer);
382
- }
383
- }
384
-
385
- function applyScalarAndBooleanOverrides(out, flags) {
386
- for (const [flag, setter] of SCALAR_FLAGS) {
387
- if (flags[flag]) setter(out, flags[flag]);
388
- }
389
- for (const [flag, setter] of UNDEF_CHECK_FLAGS) {
390
- if (flags[flag] !== undefined) setter(out, flags[flag]);
391
- }
392
- }
393
-
394
- function applyMethodologyOverride(out, flags) {
395
- if (!flags.methodology) return;
396
- const methodology = String(flags.methodology).toLowerCase();
397
- out.development.methodology = methodology;
398
- out.development.require_test_changes = methodology === "tdd";
399
- }
400
-
401
- function applyCiOverride(out, flags) {
402
- out.ci = out.ci || { enabled: false };
403
- if (flags.enableCi === undefined) return;
404
- out.ci.enabled = Boolean(flags.enableCi);
405
- // CI requires git automation (commit + push + PR)
406
- if (out.ci.enabled) {
407
- out.git.auto_commit = true;
408
- out.git.auto_push = true;
409
- out.git.auto_pr = true;
410
- }
411
- }
412
-
413
- function applyOutputModeOverrides(out, flags) {
414
- out.output = out.output || {};
415
- // --verbose explicitly overrides quiet
416
- if (flags.verbose === true) {
417
- out.output.quiet = false;
418
- } else if (flags.quiet === true) {
419
- out.output.quiet = true;
420
- }
421
- // quiet defaults to true (set in DEFAULTS)
422
- }
423
-
424
- function applyMiscOverrides(out, flags) {
425
- if (flags[AUTO_SIMPLIFY_FLAG] !== undefined) out.pipeline.auto_simplify = Boolean(flags[AUTO_SIMPLIFY_FLAG]);
426
- if (flags.noSonar || flags.sonar === false) out.sonarqube.enabled = false;
427
- out.sonarcloud = out.sonarcloud || {};
428
- if (flags.enableSonarcloud === true) out.sonarcloud.enabled = true;
429
- if (flags.noSonarcloud === true || flags.sonarcloud === false) out.sonarcloud.enabled = false;
430
-
431
- out.planning_game = out.planning_game || {};
432
- if (flags.pgTask) out.planning_game.enabled = true;
433
- if (flags.pgProject) out.planning_game.project_id = flags.pgProject;
434
-
435
- out.model_selection = out.model_selection || { enabled: true, tiers: {}, role_overrides: {} };
436
- if (flags.smartModels === true) out.model_selection.enabled = true;
437
- if (flags.smartModels === false || flags.noSmartModels === true) out.model_selection.enabled = false;
438
- }
439
-
440
- export function applyRunOverrides(config, flags) {
441
- const out = mergeDeep(config, {});
442
- out.coder_options = out.coder_options || {};
443
- out.reviewer_options = out.reviewer_options || {};
444
- out.session = out.session || {};
445
- out.git = out.git || {};
446
- out.development = out.development || {};
447
- out.sonarqube = out.sonarqube || {};
448
- if (out.max_budget_usd === undefined || out.max_budget_usd === null) {
449
- out.max_budget_usd = out.session.max_budget_usd ?? null;
450
- }
451
- out.budget = mergeDeep(DEFAULTS.budget, out.budget || {});
452
- out.roles = mergeDeep(DEFAULTS.roles, out.roles || {});
453
- out.pipeline = mergeDeep(DEFAULTS.pipeline, out.pipeline || {});
454
- out.serena = out.serena || { enabled: false };
455
-
456
- applyRoleOverrides(out, flags);
457
- applyPipelineOverrides(out, flags);
458
- applyScalarAndBooleanOverrides(out, flags);
459
- applyMethodologyOverride(out, flags);
460
- applyCiOverride(out, flags);
461
- applyMiscOverrides(out, flags);
462
- applyOutputModeOverrides(out, flags);
463
-
464
- // KJC-TSK-0318 — re-run schema validation after overrides so CLI flags
465
- // that would produce an invalid config (e.g. --max-iterations 0) fail
466
- // fast with the same clear message loadConfig would have produced.
467
- const validation = safeParseConfig(out);
468
- if (!validation.success) {
469
- throw new Error(`Invalid Karajan config after CLI overrides:\n${formatConfigIssues(validation.issues)}`);
470
- }
471
-
472
- return out;
473
- }
474
-
475
- /**
476
- * Check if a model string is compatible with an agent provider.
477
- * Only returns false when the model clearly belongs to a DIFFERENT provider.
478
- * Returns true if we can't determine or if the model is ambiguous.
479
- */
480
- const AGENT_MODEL_SIGNATURES = {
481
- claude: ["claude", "sonnet", "opus", "haiku"],
482
- codex: ["o4-", "o3-", "gpt-", "codex"],
483
- gemini: ["gemini", "flash-"]
484
- };
485
-
486
- function isModelCompatible(agent, model) {
487
- if (!model || !agent) return true;
488
- const lower = model.toLowerCase();
489
-
490
- // Check if model clearly belongs to a different provider
491
- for (const [provider, signatures] of Object.entries(AGENT_MODEL_SIGNATURES)) {
492
- if (provider === agent) continue;
493
- if (signatures.some(s => lower.includes(s))) {
494
- // Model belongs to a different provider — incompatible
495
- return false;
496
- }
497
- }
498
-
499
- // Model doesn't clearly belong to any other provider — allow it
500
- return true;
501
- }
502
-
503
- // Roles that inherit provider/model from the coder when not explicitly configured
504
- const CODER_INHERITED_ROLES = new Set([
505
- "planner", "refactorer", "solomon", "researcher", "tester", "security",
506
- "impeccable", "triage", "discover", "architect", "audit", "hu_reviewer", "hu-reviewer"
507
- ]);
508
-
509
- function resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer) {
510
- if (roleConfig.provider) return roleConfig.provider;
511
-
512
- // If model has "provider/model" format (e.g. "gemini/pro"), extract the provider
513
- if (roleConfig.model && roleConfig.model.includes("/")) {
514
- const inferredProvider = roleConfig.model.split("/")[0].toLowerCase();
515
- if (AGENT_MODEL_SIGNATURES[inferredProvider]) return inferredProvider;
516
- }
517
-
518
- if (role === "coder") return legacyCoder;
519
- if (role === "reviewer") return legacyReviewer;
520
- if (CODER_INHERITED_ROLES.has(role)) return roles.coder?.provider || legacyCoder;
521
- return null;
522
- }
523
-
524
- function resolveModel(roleConfig, role, config) {
525
- if (roleConfig.model) {
526
- // Strip "provider/" prefix from models like "gemini/pro" → "pro"
527
- const model = roleConfig.model.includes("/")
528
- ? roleConfig.model.split("/").slice(1).join("/")
529
- : roleConfig.model;
530
- return { model, inherited: false };
531
- }
532
- if (role === "coder") return { model: config?.coder_options?.model ?? null, inherited: false };
533
- if (role === "reviewer") return { model: config?.reviewer_options?.model ?? null, inherited: false };
534
- if (CODER_INHERITED_ROLES.has(role)) {
535
- const model = config?.coder_options?.model ?? null;
536
- return { model, inherited: !!model };
537
- }
538
- return { model: null, inherited: false };
539
- }
540
-
541
- export function resolveRole(config, role) {
542
- const roles = config?.roles || {};
543
- const roleConfig = roles[role] || {};
544
- const legacyCoder = config?.coder || null;
545
- const legacyReviewer = config?.reviewer || null;
546
-
547
- const provider = resolveProvider(roleConfig, role, roles, legacyCoder, legacyReviewer);
548
- let { model, inherited: modelIsInherited } = resolveModel(roleConfig, role, config);
549
-
550
- // Drop model if incompatible with the resolved provider (inherited or explicit)
551
- if (provider && model && !isModelCompatible(provider, model)) {
552
- model = null;
553
- }
554
-
555
- return { provider, model };
556
- }
557
-
558
- // Pipeline roles checked when commandName is "run": [pipelineKey, roleName]
559
- const RUN_PIPELINE_ROLES = [
560
- ["reviewer", "reviewer"], ["triage", "triage"], ["planner", "planner"],
561
- ["refactorer", "refactorer"], ["researcher", "researcher"],
562
- ["tester", "tester"], ["security", "security"], ["impeccable", "impeccable"]
563
- ];
564
-
565
- // Direct command-to-role mapping for non-"run" commands
566
- const COMMAND_ROLE_MAP = {
567
- discover: ["discover"],
568
- plan: ["planner"],
569
- code: ["coder"],
570
- review: ["reviewer"]
571
- };
572
-
573
- function requiredRolesFor(commandName, config) {
574
- if (commandName !== "run") {
575
- return COMMAND_ROLE_MAP[commandName] || [];
576
- }
577
- const required = ["coder"];
578
- for (const [pipelineKey, roleName] of RUN_PIPELINE_ROLES) {
579
- const pipelineEntry = config?.pipeline?.[pipelineKey];
580
- // reviewer defaults to enabled (only excluded if explicitly false)
581
- const isEnabled = pipelineKey === "reviewer"
582
- ? pipelineEntry?.enabled !== false
583
- : Boolean(pipelineEntry?.enabled);
584
- if (isEnabled) required.push(roleName);
585
- }
586
- return required;
587
- }
588
-
589
- export function validateConfig(config, commandName = "run") {
590
- const errors = [];
591
- if (!new Set(["paranoid", "strict", "standard", "relaxed", "custom"]).has(config.review_mode)) {
592
- errors.push(`Invalid review_mode: ${config.review_mode}`);
593
- }
594
- if (!new Set(["tdd", "standard"]).has(config.development?.methodology)) {
595
- errors.push(`Invalid development.methodology: ${config.development?.methodology}`);
596
- }
597
-
598
- const requiredRoles = requiredRolesFor(commandName, config);
599
- for (const role of requiredRoles) {
600
- const { provider } = resolveRole(config, role);
601
- if (!provider) {
602
- errors.push(
603
- `Missing provider for required role '${role}'. Set 'roles.${role}.provider' or pass '--${role} <name>'`
604
- );
605
- }
606
- }
607
-
608
- if (errors.length > 0) {
609
- throw new Error(errors.join("\n"));
610
- }
611
-
612
- return config;
613
- }
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" },