karajan-code 1.2.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 (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +441 -0
  3. package/docs/karajan-code-logo-small.png +0 -0
  4. package/package.json +60 -0
  5. package/scripts/install.js +898 -0
  6. package/scripts/install.sh +7 -0
  7. package/scripts/postinstall.js +117 -0
  8. package/scripts/setup-multi-instance.sh +150 -0
  9. package/src/activity-log.js +59 -0
  10. package/src/agents/aider-agent.js +25 -0
  11. package/src/agents/availability.js +32 -0
  12. package/src/agents/base-agent.js +27 -0
  13. package/src/agents/claude-agent.js +24 -0
  14. package/src/agents/codex-agent.js +27 -0
  15. package/src/agents/gemini-agent.js +25 -0
  16. package/src/agents/index.js +19 -0
  17. package/src/agents/resolve-bin.js +60 -0
  18. package/src/cli.js +200 -0
  19. package/src/commands/code.js +32 -0
  20. package/src/commands/config.js +74 -0
  21. package/src/commands/doctor.js +155 -0
  22. package/src/commands/init.js +181 -0
  23. package/src/commands/plan.js +67 -0
  24. package/src/commands/report.js +340 -0
  25. package/src/commands/resume.js +39 -0
  26. package/src/commands/review.js +26 -0
  27. package/src/commands/roles.js +117 -0
  28. package/src/commands/run.js +91 -0
  29. package/src/commands/scan.js +18 -0
  30. package/src/commands/sonar.js +53 -0
  31. package/src/config.js +322 -0
  32. package/src/git/automation.js +100 -0
  33. package/src/mcp/progress.js +69 -0
  34. package/src/mcp/run-kj.js +87 -0
  35. package/src/mcp/server-handlers.js +259 -0
  36. package/src/mcp/server.js +37 -0
  37. package/src/mcp/tool-arg-normalizers.js +16 -0
  38. package/src/mcp/tools.js +184 -0
  39. package/src/orchestrator.js +1277 -0
  40. package/src/planning-game/adapter.js +105 -0
  41. package/src/planning-game/client.js +81 -0
  42. package/src/prompts/coder.js +60 -0
  43. package/src/prompts/planner.js +26 -0
  44. package/src/prompts/reviewer.js +45 -0
  45. package/src/repeat-detector.js +77 -0
  46. package/src/review/diff-generator.js +22 -0
  47. package/src/review/parser.js +93 -0
  48. package/src/review/profiles.js +66 -0
  49. package/src/review/schema.js +31 -0
  50. package/src/review/tdd-policy.js +57 -0
  51. package/src/roles/base-role.js +127 -0
  52. package/src/roles/coder-role.js +60 -0
  53. package/src/roles/commiter-role.js +94 -0
  54. package/src/roles/index.js +12 -0
  55. package/src/roles/planner-role.js +81 -0
  56. package/src/roles/refactorer-role.js +66 -0
  57. package/src/roles/researcher-role.js +134 -0
  58. package/src/roles/reviewer-role.js +132 -0
  59. package/src/roles/security-role.js +128 -0
  60. package/src/roles/solomon-role.js +199 -0
  61. package/src/roles/sonar-role.js +65 -0
  62. package/src/roles/tester-role.js +114 -0
  63. package/src/roles/triage-role.js +128 -0
  64. package/src/session-store.js +80 -0
  65. package/src/sonar/api.js +78 -0
  66. package/src/sonar/enforcer.js +19 -0
  67. package/src/sonar/manager.js +163 -0
  68. package/src/sonar/project-key.js +83 -0
  69. package/src/sonar/scanner.js +267 -0
  70. package/src/utils/agent-detect.js +32 -0
  71. package/src/utils/budget.js +123 -0
  72. package/src/utils/display.js +346 -0
  73. package/src/utils/events.js +23 -0
  74. package/src/utils/fs.js +19 -0
  75. package/src/utils/git.js +101 -0
  76. package/src/utils/logger.js +86 -0
  77. package/src/utils/paths.js +18 -0
  78. package/src/utils/pricing.js +28 -0
  79. package/src/utils/process.js +67 -0
  80. package/src/utils/wizard.js +41 -0
  81. package/templates/coder-rules.md +24 -0
  82. package/templates/docker-compose.sonar.yml +60 -0
  83. package/templates/kj.config.yml +82 -0
  84. package/templates/review-rules.md +11 -0
  85. package/templates/roles/coder.md +42 -0
  86. package/templates/roles/commiter.md +44 -0
  87. package/templates/roles/planner.md +45 -0
  88. package/templates/roles/refactorer.md +39 -0
  89. package/templates/roles/researcher.md +37 -0
  90. package/templates/roles/reviewer-paranoid.md +38 -0
  91. package/templates/roles/reviewer-relaxed.md +34 -0
  92. package/templates/roles/reviewer-strict.md +37 -0
  93. package/templates/roles/reviewer.md +55 -0
  94. package/templates/roles/security.md +54 -0
  95. package/templates/roles/solomon.md +106 -0
  96. package/templates/roles/sonar.md +49 -0
  97. package/templates/roles/tester.md +41 -0
  98. package/templates/roles/triage.md +25 -0
@@ -0,0 +1,53 @@
1
+ import os from "node:os";
2
+ import { sonarDown, sonarLogs, sonarStatus, sonarUp, isSonarReachable } from "../sonar/manager.js";
3
+ import { resolveSonarProjectKey } from "../sonar/project-key.js";
4
+ import { runCommand } from "../utils/process.js";
5
+
6
+ function openBrowserCmd() {
7
+ const platform = os.platform();
8
+ if (platform === "darwin") return "open";
9
+ if (platform === "win32") return "start";
10
+ return "xdg-open";
11
+ }
12
+
13
+ export async function sonarOpenCommand({ config }) {
14
+ const host = config?.sonarqube?.host || "http://localhost:9000";
15
+
16
+ if (!(await isSonarReachable(host))) {
17
+ return { ok: false, error: `SonarQube is not reachable at ${host}. Run 'kj sonar start' first.` };
18
+ }
19
+
20
+ let projectKey;
21
+ try {
22
+ projectKey = await resolveSonarProjectKey(config);
23
+ } catch (err) {
24
+ return { ok: false, error: err.message };
25
+ }
26
+
27
+ const url = `${host}/dashboard?id=${projectKey}`;
28
+ await runCommand(openBrowserCmd(), [url]);
29
+ return { ok: true, url };
30
+ }
31
+
32
+ export async function sonarCommand({ action }) {
33
+ if (action === "start") {
34
+ const res = await sonarUp();
35
+ console.log(res.stdout || res.stderr);
36
+ return;
37
+ }
38
+
39
+ if (action === "stop") {
40
+ const res = await sonarDown();
41
+ console.log(res.stdout || res.stderr);
42
+ return;
43
+ }
44
+
45
+ if (action === "logs") {
46
+ const res = await sonarLogs();
47
+ console.log(res.stdout || res.stderr);
48
+ return;
49
+ }
50
+
51
+ const res = await sonarStatus();
52
+ console.log(res.stdout || "stopped");
53
+ }
package/src/config.js ADDED
@@ -0,0 +1,322 @@
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
+
7
+ const DEFAULTS = {
8
+ coder: "claude",
9
+ reviewer: "codex",
10
+ roles: {
11
+ planner: { provider: null, model: null },
12
+ coder: { provider: null, model: null },
13
+ reviewer: { provider: null, model: null },
14
+ refactorer: { provider: null, model: null },
15
+ solomon: { provider: null, model: null },
16
+ researcher: { provider: null, model: null },
17
+ tester: { provider: null, model: null },
18
+ security: { provider: null, model: null },
19
+ triage: { provider: null, model: null }
20
+ },
21
+ pipeline: {
22
+ planner: { enabled: false },
23
+ refactorer: { enabled: false },
24
+ solomon: { enabled: false },
25
+ researcher: { enabled: false },
26
+ tester: { enabled: false },
27
+ security: { enabled: false },
28
+ triage: { enabled: false }
29
+ },
30
+ review_mode: "standard",
31
+ max_iterations: 5,
32
+ max_budget_usd: null,
33
+ review_rules: "./review-rules.md",
34
+ coder_rules: "./coder-rules.md",
35
+ base_branch: "main",
36
+ coder_options: { model: null, auto_approve: true },
37
+ reviewer_options: {
38
+ output_format: "json",
39
+ require_schema: true,
40
+ model: null,
41
+ deterministic: true,
42
+ retries: 1,
43
+ fallback_reviewer: "codex"
44
+ },
45
+ development: {
46
+ methodology: "tdd",
47
+ require_test_changes: true,
48
+ test_file_patterns: ["/tests/", "/__tests__/", ".test.", ".spec."],
49
+ source_file_extensions: [".js", ".jsx", ".ts", ".tsx", ".py", ".go", ".java", ".rb", ".php", ".cs"]
50
+ },
51
+ sonarqube: {
52
+ enabled: true,
53
+ host: "http://localhost:9000",
54
+ external: false,
55
+ container_name: "karajan-sonarqube",
56
+ network: "karajan_sonar_net",
57
+ volumes: {
58
+ data: "karajan_sonar_data",
59
+ logs: "karajan_sonar_logs",
60
+ extensions: "karajan_sonar_extensions"
61
+ },
62
+ timeouts: {
63
+ healthcheck_seconds: 5,
64
+ compose_up_ms: 300000,
65
+ compose_control_ms: 120000,
66
+ logs_ms: 30000,
67
+ scanner_ms: 900000
68
+ },
69
+ token: null,
70
+ project_key: null,
71
+ admin_user: "admin",
72
+ admin_password: null,
73
+ coverage: {
74
+ enabled: false,
75
+ command: null,
76
+ timeout_ms: 300000,
77
+ block_on_failure: true,
78
+ lcov_report_path: null
79
+ },
80
+ quality_gate: true,
81
+ enforcement_profile: "pragmatic",
82
+ gate_block_on: [
83
+ "new_reliability_rating=E",
84
+ "new_security_rating=E",
85
+ "new_maintainability_rating=E",
86
+ "new_coverage<80",
87
+ "new_duplicated_lines_density>5"
88
+ ],
89
+ fail_on: ["BLOCKER", "CRITICAL"],
90
+ ignore_on: ["INFO"],
91
+ max_scan_retries: 3,
92
+ scanner: {
93
+ sources: "src,public,lib",
94
+ exclusions: "**/node_modules/**,**/fake-apps/**,**/scripts/**,**/playground/**,**/dist/**,**/build/**,**/*.min.js",
95
+ test_inclusions: "**/*.test.js,**/*.spec.js,**/tests/**,**/__tests__/**",
96
+ coverage_exclusions: "**/tests/**,**/__tests__/**,**/*.test.js,**/*.spec.js",
97
+ disabled_rules: ["javascript:S1116", "javascript:S3776"]
98
+ }
99
+ },
100
+ serena: { enabled: false },
101
+ planning_game: { enabled: false, project_id: null, codeveloper: null },
102
+ git: { auto_commit: false, auto_push: false, auto_pr: false, auto_rebase: true, branch_prefix: "feat/" },
103
+ output: { report_dir: "./.reviews", log_level: "info" },
104
+ budget: {
105
+ warn_threshold_pct: 80,
106
+ currency: "usd",
107
+ exchange_rate_eur: 0.92
108
+ },
109
+ session: {
110
+ max_iteration_minutes: 15,
111
+ max_total_minutes: 120,
112
+ fail_fast_repeats: 2,
113
+ repeat_detection_threshold: 2,
114
+ max_sonar_retries: 3,
115
+ max_reviewer_retries: 3,
116
+ max_tester_retries: 1,
117
+ max_security_retries: 1
118
+ },
119
+ failFast: {
120
+ repeatThreshold: 2
121
+ }
122
+ };
123
+
124
+ function mergeDeep(base, override) {
125
+ const output = { ...base };
126
+ for (const [key, value] of Object.entries(override || {})) {
127
+ if (Array.isArray(value)) {
128
+ output[key] = value;
129
+ } else if (value && typeof value === "object") {
130
+ output[key] = mergeDeep(base[key] || {}, value);
131
+ } else {
132
+ output[key] = value;
133
+ }
134
+ }
135
+ return output;
136
+ }
137
+
138
+ export function getConfigPath() {
139
+ return path.join(getKarajanHome(), "kj.config.yml");
140
+ }
141
+
142
+ async function loadProjectPricingOverrides(projectDir = process.cwd()) {
143
+ const projectConfigPath = path.join(projectDir, ".karajan.yml");
144
+ if (!(await exists(projectConfigPath))) {
145
+ return null;
146
+ }
147
+
148
+ const raw = await fs.readFile(projectConfigPath, "utf8");
149
+ const parsed = yaml.load(raw) || {};
150
+ const pricing = parsed?.budget?.pricing;
151
+ if (!pricing || typeof pricing !== "object") {
152
+ return null;
153
+ }
154
+
155
+ return pricing;
156
+ }
157
+
158
+ export async function loadConfig() {
159
+ const configPath = getConfigPath();
160
+ const projectPricing = await loadProjectPricingOverrides();
161
+ if (!(await exists(configPath))) {
162
+ const baseDefaults = mergeDeep(DEFAULTS, {});
163
+ if (projectPricing) {
164
+ baseDefaults.budget = mergeDeep(baseDefaults.budget || {}, { pricing: projectPricing });
165
+ }
166
+ return { config: baseDefaults, path: configPath, exists: false };
167
+ }
168
+
169
+ const raw = await fs.readFile(configPath, "utf8");
170
+ const parsed = yaml.load(raw) || {};
171
+ const merged = mergeDeep(DEFAULTS, parsed);
172
+ if (projectPricing) {
173
+ merged.budget = mergeDeep(merged.budget || {}, { pricing: projectPricing });
174
+ }
175
+ return { config: merged, path: configPath, exists: true };
176
+ }
177
+
178
+ export async function writeConfig(configPath, config) {
179
+ await ensureDir(path.dirname(configPath));
180
+ await fs.writeFile(configPath, yaml.dump(config, { lineWidth: 120 }), "utf8");
181
+ }
182
+
183
+ export function applyRunOverrides(config, flags) {
184
+ const out = mergeDeep(config, {});
185
+ out.coder_options = out.coder_options || {};
186
+ out.reviewer_options = out.reviewer_options || {};
187
+ out.session = out.session || {};
188
+ out.git = out.git || {};
189
+ out.development = out.development || {};
190
+ out.sonarqube = out.sonarqube || {};
191
+ if (out.max_budget_usd === undefined || out.max_budget_usd === null) {
192
+ out.max_budget_usd = out.session.max_budget_usd ?? null;
193
+ }
194
+ out.budget = mergeDeep(DEFAULTS.budget, out.budget || {});
195
+ out.roles = mergeDeep(DEFAULTS.roles, out.roles || {});
196
+ out.pipeline = mergeDeep(DEFAULTS.pipeline, out.pipeline || {});
197
+
198
+ if (flags.planner) out.roles.planner.provider = flags.planner;
199
+ if (flags.coder) out.coder = flags.coder;
200
+ if (flags.coder) out.roles.coder.provider = flags.coder;
201
+ if (flags.reviewer) out.reviewer = flags.reviewer;
202
+ if (flags.reviewer) out.roles.reviewer.provider = flags.reviewer;
203
+ if (flags.refactorer) out.roles.refactorer.provider = flags.refactorer;
204
+ if (flags.solomon) out.roles.solomon.provider = flags.solomon;
205
+ if (flags.researcher) out.roles.researcher.provider = flags.researcher;
206
+ if (flags.tester) out.roles.tester.provider = flags.tester;
207
+ if (flags.security) out.roles.security.provider = flags.security;
208
+ if (flags.triage) out.roles.triage.provider = flags.triage;
209
+ if (flags.plannerModel) out.roles.planner.model = String(flags.plannerModel);
210
+ if (flags.coderModel) {
211
+ out.roles.coder.model = String(flags.coderModel);
212
+ }
213
+ if (flags.reviewerModel) {
214
+ out.roles.reviewer.model = String(flags.reviewerModel);
215
+ out.reviewer_options.model = String(flags.reviewerModel);
216
+ }
217
+ if (flags.refactorerModel) out.roles.refactorer.model = String(flags.refactorerModel);
218
+ if (flags.solomonModel) out.roles.solomon.model = String(flags.solomonModel);
219
+ if (flags.enablePlanner !== undefined) out.pipeline.planner.enabled = Boolean(flags.enablePlanner);
220
+ if (flags.enableRefactorer !== undefined) out.pipeline.refactorer.enabled = Boolean(flags.enableRefactorer);
221
+ if (flags.enableSolomon !== undefined) out.pipeline.solomon.enabled = Boolean(flags.enableSolomon);
222
+ if (flags.enableResearcher !== undefined) out.pipeline.researcher.enabled = Boolean(flags.enableResearcher);
223
+ if (flags.enableTester !== undefined) out.pipeline.tester.enabled = Boolean(flags.enableTester);
224
+ if (flags.enableSecurity !== undefined) out.pipeline.security.enabled = Boolean(flags.enableSecurity);
225
+ if (flags.enableReviewer !== undefined) {
226
+ out.pipeline.reviewer = out.pipeline.reviewer || {};
227
+ out.pipeline.reviewer.enabled = Boolean(flags.enableReviewer);
228
+ }
229
+ if (flags.enableTriage !== undefined) out.pipeline.triage.enabled = Boolean(flags.enableTriage);
230
+ if (flags.mode) out.review_mode = flags.mode;
231
+ if (flags.maxIterations) out.max_iterations = Number(flags.maxIterations);
232
+ if (flags.maxIterationMinutes) out.session.max_iteration_minutes = Number(flags.maxIterationMinutes);
233
+ if (flags.maxTotalMinutes) out.session.max_total_minutes = Number(flags.maxTotalMinutes);
234
+ if (flags.baseBranch) out.base_branch = flags.baseBranch;
235
+ if (flags.reviewerFallback) out.reviewer_options.fallback_reviewer = flags.reviewerFallback;
236
+ if (flags.reviewerRetries !== undefined) out.reviewer_options.retries = Number(flags.reviewerRetries);
237
+ if (flags.autoCommit !== undefined) out.git.auto_commit = Boolean(flags.autoCommit);
238
+ if (flags.autoPush !== undefined) out.git.auto_push = Boolean(flags.autoPush);
239
+ if (flags.autoPr !== undefined) out.git.auto_pr = Boolean(flags.autoPr);
240
+ if (flags.autoRebase !== undefined) out.git.auto_rebase = Boolean(flags.autoRebase);
241
+ if (flags.branchPrefix) out.git.branch_prefix = String(flags.branchPrefix);
242
+ if (flags.methodology) {
243
+ const methodology = String(flags.methodology).toLowerCase();
244
+ out.development = out.development || {};
245
+ out.development.methodology = methodology;
246
+ out.development.require_test_changes = methodology === "tdd";
247
+ }
248
+ if (flags.noSonar || flags.sonar === false) out.sonarqube.enabled = false;
249
+ out.serena = out.serena || { enabled: false };
250
+ if (flags.enableSerena !== undefined) out.serena.enabled = Boolean(flags.enableSerena);
251
+ out.planning_game = out.planning_game || {};
252
+ if (flags.pgTask) out.planning_game.enabled = true;
253
+ if (flags.pgProject) out.planning_game.project_id = flags.pgProject;
254
+ return out;
255
+ }
256
+
257
+ export function resolveRole(config, role) {
258
+ const roles = config?.roles || {};
259
+ const roleConfig = roles[role] || {};
260
+ const legacyCoder = config?.coder || null;
261
+ const legacyReviewer = config?.reviewer || null;
262
+
263
+ let provider = roleConfig.provider ?? null;
264
+ if (!provider && role === "coder") provider = legacyCoder;
265
+ if (!provider && role === "reviewer") provider = legacyReviewer;
266
+ if (!provider && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "triage")) {
267
+ provider = roles.coder?.provider || legacyCoder;
268
+ }
269
+
270
+ let model = roleConfig.model ?? null;
271
+ if (!model && role === "coder") model = config?.coder_options?.model ?? null;
272
+ if (!model && role === "reviewer") model = config?.reviewer_options?.model ?? null;
273
+ if (!model && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "triage")) {
274
+ model = config?.coder_options?.model ?? null;
275
+ }
276
+
277
+ return { provider, model };
278
+ }
279
+
280
+ function requiredRolesFor(commandName, config) {
281
+ if (commandName === "run") {
282
+ const required = ["coder"];
283
+ if (config?.pipeline?.reviewer?.enabled !== false) required.push("reviewer");
284
+ if (config?.pipeline?.triage?.enabled) required.push("triage");
285
+ if (config?.pipeline?.planner?.enabled) required.push("planner");
286
+ if (config?.pipeline?.refactorer?.enabled) required.push("refactorer");
287
+ if (config?.pipeline?.researcher?.enabled) required.push("researcher");
288
+ if (config?.pipeline?.tester?.enabled) required.push("tester");
289
+ if (config?.pipeline?.security?.enabled) required.push("security");
290
+ return required;
291
+ }
292
+ if (commandName === "plan") return ["planner"];
293
+ if (commandName === "code") return ["coder"];
294
+ if (commandName === "review") return ["reviewer"];
295
+ return [];
296
+ }
297
+
298
+ export function validateConfig(config, commandName = "run") {
299
+ const errors = [];
300
+ if (!["paranoid", "strict", "standard", "relaxed", "custom"].includes(config.review_mode)) {
301
+ errors.push(`Invalid review_mode: ${config.review_mode}`);
302
+ }
303
+ if (!["tdd", "standard"].includes(config.development?.methodology)) {
304
+ errors.push(`Invalid development.methodology: ${config.development?.methodology}`);
305
+ }
306
+
307
+ const requiredRoles = requiredRolesFor(commandName, config);
308
+ for (const role of requiredRoles) {
309
+ const { provider } = resolveRole(config, role);
310
+ if (!provider) {
311
+ errors.push(
312
+ `Missing provider for required role '${role}'. Set 'roles.${role}.provider' or pass '--${role} <name>'`
313
+ );
314
+ }
315
+ }
316
+
317
+ if (errors.length > 0) {
318
+ throw new Error(errors.join("\n"));
319
+ }
320
+
321
+ return config;
322
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Git automation helpers for the pipeline.
3
+ * Extracted from orchestrator.js for testability and reuse.
4
+ */
5
+
6
+ import { addCheckpoint } from "../session-store.js";
7
+ import {
8
+ ensureGitRepo,
9
+ currentBranch,
10
+ fetchBase,
11
+ syncBaseBranch,
12
+ ensureBranchUpToDateWithBase,
13
+ createBranch,
14
+ buildBranchName,
15
+ commitAll,
16
+ pushBranch,
17
+ createPullRequest
18
+ } from "../utils/git.js";
19
+
20
+ export function commitMessageFromTask(task) {
21
+ const clean = String(task || "")
22
+ .replace(/\s+/g, " ")
23
+ .trim();
24
+ return `feat: ${clean.slice(0, 72) || "karajan update"}`;
25
+ }
26
+
27
+ export async function prepareGitAutomation({ config, task, logger, session }) {
28
+ const enabled = config.git.auto_commit || config.git.auto_push || config.git.auto_pr;
29
+ if (!enabled) return { enabled: false };
30
+
31
+ if (!(await ensureGitRepo())) {
32
+ throw new Error("Git automation requested but current directory is not a git repository");
33
+ }
34
+
35
+ const baseBranch = config.base_branch;
36
+ const autoRebase = config.git.auto_rebase !== false;
37
+ await fetchBase(baseBranch);
38
+
39
+ let branch = await currentBranch();
40
+ if (branch === baseBranch) {
41
+ await syncBaseBranch({ baseBranch, autoRebase });
42
+ const created = buildBranchName(config.git.branch_prefix || "feat/", task);
43
+ await createBranch(created);
44
+ branch = created;
45
+ logger.info(`Created working branch: ${branch}`);
46
+ await addCheckpoint(session, { stage: "git-prep", branch, created: true });
47
+ } else {
48
+ await ensureBranchUpToDateWithBase({ branch, baseBranch, autoRebase });
49
+ await addCheckpoint(session, { stage: "git-prep", branch, created: false });
50
+ }
51
+
52
+ return { enabled: true, branch, baseBranch, autoRebase };
53
+ }
54
+
55
+ export async function finalizeGitAutomation({ config, gitCtx, task, logger, session }) {
56
+ if (!gitCtx?.enabled) return { git: "disabled", commits: [] };
57
+
58
+ const commitMsg = config.git.commit_message || commitMessageFromTask(task);
59
+ let committed = false;
60
+ const commits = [];
61
+ if (config.git.auto_commit) {
62
+ const commitResult = await commitAll(commitMsg);
63
+ committed = commitResult.committed;
64
+ if (commitResult.commit) {
65
+ commits.push(commitResult.commit);
66
+ }
67
+ await addCheckpoint(session, { stage: "git-commit", committed });
68
+ logger.info(committed ? "Committed changes" : "No changes to commit");
69
+ }
70
+
71
+ if (config.git.auto_push || config.git.auto_pr) {
72
+ await fetchBase(gitCtx.baseBranch);
73
+ await ensureBranchUpToDateWithBase({
74
+ branch: gitCtx.branch,
75
+ baseBranch: gitCtx.baseBranch,
76
+ autoRebase: gitCtx.autoRebase
77
+ });
78
+ await addCheckpoint(session, { stage: "git-rebase-check", branch: gitCtx.branch });
79
+ }
80
+
81
+ if (config.git.auto_push || config.git.auto_pr) {
82
+ await pushBranch(gitCtx.branch);
83
+ await addCheckpoint(session, { stage: "git-push", branch: gitCtx.branch });
84
+ logger.info(`Pushed branch: ${gitCtx.branch}`);
85
+ }
86
+
87
+ let prUrl = null;
88
+ if (config.git.auto_pr) {
89
+ prUrl = await createPullRequest({
90
+ baseBranch: gitCtx.baseBranch,
91
+ branch: gitCtx.branch,
92
+ title: commitMessageFromTask(task),
93
+ body: "Created by Karajan Code."
94
+ });
95
+ await addCheckpoint(session, { stage: "git-pr", branch: gitCtx.branch, pr: prUrl });
96
+ logger.info("Pull request created");
97
+ }
98
+
99
+ return { committed, branch: gitCtx.branch, prUrl, pr: prUrl, commits };
100
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * MCP progress notification helpers.
3
+ * Extracted from server.js for testability and reuse.
4
+ */
5
+
6
+ export const PROGRESS_STAGES = [
7
+ "session:start",
8
+ "iteration:start",
9
+ "planner:start",
10
+ "planner:end",
11
+ "coder:start",
12
+ "coder:end",
13
+ "refactorer:start",
14
+ "refactorer:end",
15
+ "tdd:result",
16
+ "sonar:start",
17
+ "sonar:end",
18
+ "reviewer:start",
19
+ "reviewer:end",
20
+ "iteration:end",
21
+ "solomon:escalate",
22
+ "question",
23
+ "session:end",
24
+ "dry-run:summary"
25
+ ];
26
+
27
+ export function buildProgressHandler(server) {
28
+ return (event) => {
29
+ try {
30
+ server.sendLoggingMessage({
31
+ level: event.type === "agent:output" ? "debug" : event.status === "fail" ? "error" : "info",
32
+ logger: "karajan",
33
+ data: event
34
+ });
35
+ } catch {
36
+ // best-effort: if logging fails, continue
37
+ }
38
+ };
39
+ }
40
+
41
+ export function buildProgressNotifier(extra) {
42
+ const progressToken = extra?._meta?.progressToken;
43
+ if (progressToken === undefined) return null;
44
+
45
+ const total = PROGRESS_STAGES.length;
46
+ return (event) => {
47
+ const idx = PROGRESS_STAGES.indexOf(event.type);
48
+ if (idx < 0) return;
49
+
50
+ const iteration = event.iteration || event.detail?.iteration;
51
+ const message = iteration
52
+ ? `[${event.iteration}] ${event.message || event.type}`
53
+ : event.message || event.type;
54
+
55
+ try {
56
+ extra.sendNotification({
57
+ method: "notifications/progress",
58
+ params: {
59
+ progressToken,
60
+ progress: idx + 1,
61
+ total,
62
+ message
63
+ }
64
+ });
65
+ } catch {
66
+ // best-effort
67
+ }
68
+ };
69
+ }
@@ -0,0 +1,87 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { execa } from "execa";
4
+
5
+ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
6
+ const CLI_PATH = path.resolve(MODULE_DIR, "..", "cli.js");
7
+
8
+ function normalizeBoolFlag(value, flagName, args) {
9
+ if (value === true) args.push(flagName);
10
+ }
11
+
12
+ function addOptionalValue(args, flag, value) {
13
+ if (value !== undefined && value !== null && value !== "") {
14
+ args.push(flag, String(value));
15
+ }
16
+ }
17
+
18
+ export async function runKjCommand({ command, commandArgs = [], options = {}, env = {} }) {
19
+ const args = [CLI_PATH, command, ...commandArgs];
20
+
21
+ addOptionalValue(args, "--coder", options.coder);
22
+ addOptionalValue(args, "--reviewer", options.reviewer);
23
+ addOptionalValue(args, "--planner", options.planner);
24
+ addOptionalValue(args, "--refactorer", options.refactorer);
25
+ addOptionalValue(args, "--planner-model", options.plannerModel);
26
+ addOptionalValue(args, "--coder-model", options.coderModel);
27
+ addOptionalValue(args, "--reviewer-model", options.reviewerModel);
28
+ addOptionalValue(args, "--refactorer-model", options.refactorerModel);
29
+ addOptionalValue(args, "--reviewer-fallback", options.reviewerFallback);
30
+ addOptionalValue(args, "--reviewer-retries", options.reviewerRetries);
31
+ addOptionalValue(args, "--mode", options.mode);
32
+ addOptionalValue(args, "--max-iterations", options.maxIterations);
33
+ addOptionalValue(args, "--max-iteration-minutes", options.maxIterationMinutes);
34
+ addOptionalValue(args, "--max-total-minutes", options.maxTotalMinutes);
35
+ addOptionalValue(args, "--base-branch", options.baseBranch);
36
+ addOptionalValue(args, "--base-ref", options.baseRef);
37
+ addOptionalValue(args, "--branch-prefix", options.branchPrefix);
38
+ addOptionalValue(args, "--methodology", options.methodology);
39
+ normalizeBoolFlag(options.enablePlanner, "--enable-planner", args);
40
+ normalizeBoolFlag(options.enableReviewer, "--enable-reviewer", args);
41
+ normalizeBoolFlag(options.enableRefactorer, "--enable-refactorer", args);
42
+ normalizeBoolFlag(options.enableResearcher, "--enable-researcher", args);
43
+ normalizeBoolFlag(options.enableTester, "--enable-tester", args);
44
+ normalizeBoolFlag(options.enableSecurity, "--enable-security", args);
45
+ normalizeBoolFlag(options.enableTriage, "--enable-triage", args);
46
+ normalizeBoolFlag(options.enableSerena, "--enable-serena", args);
47
+ normalizeBoolFlag(options.autoCommit, "--auto-commit", args);
48
+ normalizeBoolFlag(options.autoPush, "--auto-push", args);
49
+ normalizeBoolFlag(options.autoPr, "--auto-pr", args);
50
+ if (options.autoRebase === false) args.push("--no-auto-rebase");
51
+ normalizeBoolFlag(options.noSonar, "--no-sonar", args);
52
+ addOptionalValue(args, "--pg-task", options.pgTask);
53
+ addOptionalValue(args, "--pg-project", options.pgProject);
54
+
55
+ const runEnv = {
56
+ ...process.env,
57
+ ...env
58
+ };
59
+
60
+ if (options.kjHome) {
61
+ runEnv.KJ_HOME = options.kjHome;
62
+ }
63
+
64
+ if (options.sonarToken) {
65
+ runEnv.KJ_SONAR_TOKEN = options.sonarToken;
66
+ }
67
+
68
+ const result = await execa("node", args, {
69
+ env: runEnv,
70
+ reject: false,
71
+ timeout: options.timeoutMs ? Number(options.timeoutMs) : undefined
72
+ });
73
+
74
+ const ok = result.exitCode === 0;
75
+ const payload = {
76
+ ok,
77
+ exitCode: result.exitCode,
78
+ stdout: result.stdout,
79
+ stderr: result.stderr
80
+ };
81
+
82
+ if (!ok && result.stderr) {
83
+ payload.errorSummary = result.stderr.split("\n").filter(Boolean).slice(-3).join(" | ");
84
+ }
85
+
86
+ return payload;
87
+ }