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.
- package/LICENSE +21 -0
- package/README.md +441 -0
- package/docs/karajan-code-logo-small.png +0 -0
- package/package.json +60 -0
- package/scripts/install.js +898 -0
- package/scripts/install.sh +7 -0
- package/scripts/postinstall.js +117 -0
- package/scripts/setup-multi-instance.sh +150 -0
- package/src/activity-log.js +59 -0
- package/src/agents/aider-agent.js +25 -0
- package/src/agents/availability.js +32 -0
- package/src/agents/base-agent.js +27 -0
- package/src/agents/claude-agent.js +24 -0
- package/src/agents/codex-agent.js +27 -0
- package/src/agents/gemini-agent.js +25 -0
- package/src/agents/index.js +19 -0
- package/src/agents/resolve-bin.js +60 -0
- package/src/cli.js +200 -0
- package/src/commands/code.js +32 -0
- package/src/commands/config.js +74 -0
- package/src/commands/doctor.js +155 -0
- package/src/commands/init.js +181 -0
- package/src/commands/plan.js +67 -0
- package/src/commands/report.js +340 -0
- package/src/commands/resume.js +39 -0
- package/src/commands/review.js +26 -0
- package/src/commands/roles.js +117 -0
- package/src/commands/run.js +91 -0
- package/src/commands/scan.js +18 -0
- package/src/commands/sonar.js +53 -0
- package/src/config.js +322 -0
- package/src/git/automation.js +100 -0
- package/src/mcp/progress.js +69 -0
- package/src/mcp/run-kj.js +87 -0
- package/src/mcp/server-handlers.js +259 -0
- package/src/mcp/server.js +37 -0
- package/src/mcp/tool-arg-normalizers.js +16 -0
- package/src/mcp/tools.js +184 -0
- package/src/orchestrator.js +1277 -0
- package/src/planning-game/adapter.js +105 -0
- package/src/planning-game/client.js +81 -0
- package/src/prompts/coder.js +60 -0
- package/src/prompts/planner.js +26 -0
- package/src/prompts/reviewer.js +45 -0
- package/src/repeat-detector.js +77 -0
- package/src/review/diff-generator.js +22 -0
- package/src/review/parser.js +93 -0
- package/src/review/profiles.js +66 -0
- package/src/review/schema.js +31 -0
- package/src/review/tdd-policy.js +57 -0
- package/src/roles/base-role.js +127 -0
- package/src/roles/coder-role.js +60 -0
- package/src/roles/commiter-role.js +94 -0
- package/src/roles/index.js +12 -0
- package/src/roles/planner-role.js +81 -0
- package/src/roles/refactorer-role.js +66 -0
- package/src/roles/researcher-role.js +134 -0
- package/src/roles/reviewer-role.js +132 -0
- package/src/roles/security-role.js +128 -0
- package/src/roles/solomon-role.js +199 -0
- package/src/roles/sonar-role.js +65 -0
- package/src/roles/tester-role.js +114 -0
- package/src/roles/triage-role.js +128 -0
- package/src/session-store.js +80 -0
- package/src/sonar/api.js +78 -0
- package/src/sonar/enforcer.js +19 -0
- package/src/sonar/manager.js +163 -0
- package/src/sonar/project-key.js +83 -0
- package/src/sonar/scanner.js +267 -0
- package/src/utils/agent-detect.js +32 -0
- package/src/utils/budget.js +123 -0
- package/src/utils/display.js +346 -0
- package/src/utils/events.js +23 -0
- package/src/utils/fs.js +19 -0
- package/src/utils/git.js +101 -0
- package/src/utils/logger.js +86 -0
- package/src/utils/paths.js +18 -0
- package/src/utils/pricing.js +28 -0
- package/src/utils/process.js +67 -0
- package/src/utils/wizard.js +41 -0
- package/templates/coder-rules.md +24 -0
- package/templates/docker-compose.sonar.yml +60 -0
- package/templates/kj.config.yml +82 -0
- package/templates/review-rules.md +11 -0
- package/templates/roles/coder.md +42 -0
- package/templates/roles/commiter.md +44 -0
- package/templates/roles/planner.md +45 -0
- package/templates/roles/refactorer.md +39 -0
- package/templates/roles/researcher.md +37 -0
- package/templates/roles/reviewer-paranoid.md +38 -0
- package/templates/roles/reviewer-relaxed.md +34 -0
- package/templates/roles/reviewer-strict.md +37 -0
- package/templates/roles/reviewer.md +55 -0
- package/templates/roles/security.md +54 -0
- package/templates/roles/solomon.md +106 -0
- package/templates/roles/sonar.md +49 -0
- package/templates/roles/tester.md +41 -0
- 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
|
+
}
|