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
package/src/cli.js ADDED
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { applyRunOverrides, loadConfig, validateConfig } from "./config.js";
4
+ import { createLogger } from "./utils/logger.js";
5
+ import { initCommand } from "./commands/init.js";
6
+ import { configCommand } from "./commands/config.js";
7
+ import { codeCommand } from "./commands/code.js";
8
+ import { reviewCommand } from "./commands/review.js";
9
+ import { scanCommand } from "./commands/scan.js";
10
+ import { doctorCommand } from "./commands/doctor.js";
11
+ import { reportCommand } from "./commands/report.js";
12
+ import { planCommand } from "./commands/plan.js";
13
+ import { runCommandHandler } from "./commands/run.js";
14
+ import { resumeCommand } from "./commands/resume.js";
15
+ import { sonarCommand, sonarOpenCommand } from "./commands/sonar.js";
16
+ import { rolesCommand } from "./commands/roles.js";
17
+
18
+ async function withConfig(commandName, flags, fn) {
19
+ const { config } = await loadConfig();
20
+ const merged = applyRunOverrides(config, flags || {});
21
+ validateConfig(merged, commandName);
22
+ const logger = createLogger(merged.output.log_level);
23
+ await fn({ config: merged, logger, flags });
24
+ }
25
+
26
+ const program = new Command();
27
+ program.name("kj").description("Karajan Code CLI").version("1.2.0");
28
+
29
+ program
30
+ .command("init")
31
+ .description("Initialize config, review rules and SonarQube")
32
+ .option("--no-interactive", "Skip wizard, use defaults (for CI/scripts)")
33
+ .action(async (flags) => {
34
+ await withConfig("init", flags, async ({ config, logger }) => {
35
+ await initCommand({ logger, flags });
36
+ });
37
+ });
38
+
39
+ program
40
+ .command("config")
41
+ .description("Show current config")
42
+ .option("--json", "Show as JSON")
43
+ .option("--edit", "Open config in $EDITOR for editing")
44
+ .action(async (flags) => {
45
+ await configCommand(flags);
46
+ });
47
+
48
+ program
49
+ .command("run")
50
+ .description("Run coder+sonar+reviewer loop")
51
+ .argument("<task>")
52
+ .option("--planner <name>")
53
+ .option("--coder <name>")
54
+ .option("--reviewer <name>")
55
+ .option("--refactorer <name>")
56
+ .option("--planner-model <name>")
57
+ .option("--coder-model <name>")
58
+ .option("--reviewer-model <name>")
59
+ .option("--refactorer-model <name>")
60
+ .option("--enable-planner")
61
+ .option("--enable-reviewer")
62
+ .option("--enable-refactorer")
63
+ .option("--enable-researcher")
64
+ .option("--enable-tester")
65
+ .option("--enable-security")
66
+ .option("--enable-triage")
67
+ .option("--enable-serena")
68
+ .option("--mode <name>")
69
+ .option("--max-iterations <n>")
70
+ .option("--max-iteration-minutes <n>")
71
+ .option("--max-total-minutes <n>")
72
+ .option("--base-branch <name>")
73
+ .option("--base-ref <ref>")
74
+ .option("--reviewer-fallback <name>")
75
+ .option("--reviewer-retries <n>")
76
+ .option("--auto-commit")
77
+ .option("--auto-push")
78
+ .option("--auto-pr")
79
+ .option("--branch-prefix <prefix>")
80
+ .option("--methodology <name>")
81
+ .option("--no-auto-rebase")
82
+ .option("--no-sonar")
83
+ .option("--pg-task <cardId>", "Planning Game card ID (e.g., KJC-TSK-0042)")
84
+ .option("--pg-project <projectId>", "Planning Game project ID")
85
+ .option("--dry-run", "Show what would be executed without running anything")
86
+ .option("--json", "Output JSON only (no styled display)")
87
+ .action(async (task, flags) => {
88
+ await withConfig("run", flags, async ({ config, logger }) => {
89
+ await runCommandHandler({ task, config, logger, flags });
90
+ });
91
+ });
92
+
93
+ program
94
+ .command("code")
95
+ .description("Run only coder")
96
+ .argument("<task>")
97
+ .option("--coder <name>")
98
+ .option("--coder-model <name>")
99
+ .action(async (task, flags) => {
100
+ await withConfig("code", flags, async ({ config, logger }) => {
101
+ await codeCommand({ task, config, logger });
102
+ });
103
+ });
104
+
105
+ program
106
+ .command("review")
107
+ .description("Run only reviewer")
108
+ .argument("<task>")
109
+ .option("--reviewer <name>")
110
+ .option("--reviewer-model <name>")
111
+ .option("--base-ref <ref>")
112
+ .action(async (task, flags) => {
113
+ await withConfig("review", flags, async ({ config, logger }) => {
114
+ await reviewCommand({ task, config, logger, baseRef: flags.baseRef });
115
+ });
116
+ });
117
+
118
+ program
119
+ .command("scan")
120
+ .description("Run SonarQube scan")
121
+ .action(async () => {
122
+ await withConfig("scan", {}, scanCommand);
123
+ });
124
+
125
+ program
126
+ .command("doctor")
127
+ .description("Check environment requirements")
128
+ .action(async () => {
129
+ await withConfig("doctor", {}, doctorCommand);
130
+ });
131
+
132
+ program
133
+ .command("report")
134
+ .description("Show latest session report")
135
+ .option("--list", "List session ids")
136
+ .option("--session-id <id>", "Show report for a specific session ID")
137
+ .option("--format <type>", "Output format: text|json", "text")
138
+ .option("--trace", "Show chronological trace of all pipeline stages")
139
+ .option("--currency <code>", "Display costs in currency: usd|eur", "usd")
140
+ .action(async (flags) => {
141
+ await reportCommand(flags);
142
+ });
143
+
144
+ program
145
+ .command("roles [subcommand] [role]")
146
+ .description("List pipeline roles or show role template instructions")
147
+ .action(async (subcommand, role) => {
148
+ await withConfig("roles", {}, async ({ config }) => {
149
+ await rolesCommand({ config, subcommand: subcommand || "list", roleName: role });
150
+ });
151
+ });
152
+
153
+ program
154
+ .command("plan")
155
+ .description("Generate implementation plan")
156
+ .argument("<task>")
157
+ .option("--planner <name>")
158
+ .option("--planner-model <name>")
159
+ .option("--context <text>", "Additional context for the planner")
160
+ .option("--json", "Output raw JSON plan")
161
+ .action(async (task, flags) => {
162
+ await withConfig("plan", flags, async ({ config, logger }) => {
163
+ await planCommand({ task, config, logger, json: flags.json, context: flags.context });
164
+ });
165
+ });
166
+
167
+ program
168
+ .command("resume")
169
+ .description("Resume a paused session")
170
+ .argument("<sessionId>")
171
+ .option("--answer <text>", "Answer to the question that caused the pause")
172
+ .option("--json", "Output JSON only")
173
+ .action(async (sessionId, flags) => {
174
+ await withConfig("resume", flags, async ({ config, logger }) => {
175
+ await resumeCommand({ sessionId, answer: flags.answer, config, logger, flags });
176
+ });
177
+ });
178
+
179
+ const sonar = program.command("sonar").description("Manage SonarQube container");
180
+ sonar.command("status").action(async () => sonarCommand({ action: "status" }));
181
+ sonar.command("start").action(async () => sonarCommand({ action: "start" }));
182
+ sonar.command("stop").action(async () => sonarCommand({ action: "stop" }));
183
+ sonar.command("logs").action(async () => sonarCommand({ action: "logs" }));
184
+ sonar
185
+ .command("open")
186
+ .description("Open SonarQube dashboard in the browser")
187
+ .action(async () => {
188
+ const { config } = await loadConfig();
189
+ const result = await sonarOpenCommand({ config });
190
+ if (!result.ok) {
191
+ console.error(result.error);
192
+ process.exit(1);
193
+ }
194
+ console.log(`Opened ${result.url}`);
195
+ });
196
+
197
+ program.parseAsync().catch((error) => {
198
+ console.error(error.message);
199
+ process.exit(1);
200
+ });
@@ -0,0 +1,32 @@
1
+ import fs from "node:fs/promises";
2
+ import { createAgent } from "../agents/index.js";
3
+ import { assertAgentsAvailable } from "../agents/availability.js";
4
+ import { buildCoderPrompt } from "../prompts/coder.js";
5
+ import { resolveRole } from "../config.js";
6
+
7
+ export async function codeCommand({ task, config, logger }) {
8
+ const coderRole = resolveRole(config, "coder");
9
+ await assertAgentsAvailable([coderRole.provider]);
10
+ logger.info(`Coder (${coderRole.provider}) starting...`);
11
+ const coder = createAgent(coderRole.provider, config, logger);
12
+ let coderRules = null;
13
+ if (config.coder_rules) {
14
+ try {
15
+ coderRules = await fs.readFile(config.coder_rules, "utf8");
16
+ } catch { /* no coder rules file, that's ok */ }
17
+ }
18
+ const prompt = buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology || "tdd" });
19
+ const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
20
+ const result = await coder.runTask({ prompt, onOutput, role: "coder" });
21
+ if (!result.ok) {
22
+ if (result.error) logger.error(result.error);
23
+ throw new Error(result.error || result.output || `Coder failed (exit ${result.exitCode})`);
24
+ }
25
+ if (result.output) {
26
+ console.log(result.output);
27
+ }
28
+ if (result.error) {
29
+ logger.warn(result.error);
30
+ }
31
+ logger.info(`Coder completed (exit ${result.exitCode})`);
32
+ }
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs/promises";
2
+ import { spawnSync } from "node:child_process";
3
+ import yaml from "js-yaml";
4
+ import { loadConfig, validateConfig, getConfigPath, writeConfig } from "../config.js";
5
+ import { ensureDir } from "../utils/fs.js";
6
+ import path from "node:path";
7
+
8
+ function resolveEditor() {
9
+ const raw = process.env.VISUAL || process.env.EDITOR || "vi";
10
+ const parts = raw.split(/\s+/);
11
+ return { cmd: parts[0], args: parts.slice(1) };
12
+ }
13
+
14
+ export async function editConfigOnce(configPath) {
15
+ const { cmd, args } = resolveEditor();
16
+ const result = spawnSync(cmd, [...args, configPath], { stdio: "inherit" });
17
+
18
+ if (result.status !== 0) {
19
+ return { ok: false, error: `Editor exited with code ${result.status}` };
20
+ }
21
+
22
+ let raw;
23
+ try {
24
+ raw = await fs.readFile(configPath, "utf8");
25
+ } catch (err) {
26
+ return { ok: false, error: `Could not read config: ${err.message}` };
27
+ }
28
+
29
+ let parsed;
30
+ try {
31
+ parsed = yaml.load(raw);
32
+ } catch (err) {
33
+ return { ok: false, error: `Invalid YAML: ${err.message}` };
34
+ }
35
+
36
+ try {
37
+ validateConfig(parsed || {});
38
+ } catch (err) {
39
+ return { ok: false, error: err.message };
40
+ }
41
+
42
+ return { ok: true, config: parsed };
43
+ }
44
+
45
+ export async function configCommand({ json = false, edit = false }) {
46
+ if (edit) {
47
+ const configPath = getConfigPath();
48
+ await ensureDir(path.dirname(configPath));
49
+ const { exists: configExists } = await loadConfig();
50
+ if (!configExists) {
51
+ const { config: defaults } = await loadConfig();
52
+ await writeConfig(configPath, defaults);
53
+ }
54
+
55
+ const result = await editConfigOnce(configPath);
56
+ if (result.ok) {
57
+ console.log("Configuration saved and validated.");
58
+ } else {
59
+ console.error(`Validation error: ${result.error}`);
60
+ process.exitCode = 1;
61
+ }
62
+ return;
63
+ }
64
+
65
+ const { config, path: cfgPath, exists } = await loadConfig();
66
+ if (json) {
67
+ console.log(JSON.stringify({ path: cfgPath, exists, config }, null, 2));
68
+ return;
69
+ }
70
+
71
+ console.log(`Config path: ${cfgPath}`);
72
+ console.log(`Exists: ${exists}`);
73
+ console.log(JSON.stringify(config, null, 2));
74
+ }
@@ -0,0 +1,155 @@
1
+ import { runCommand } from "../utils/process.js";
2
+ import { exists } from "../utils/fs.js";
3
+ import { getConfigPath } from "../config.js";
4
+ import { isSonarReachable } from "../sonar/manager.js";
5
+ import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
6
+ import { ensureGitRepo } from "../utils/git.js";
7
+ import { checkBinary, KNOWN_AGENTS } from "../utils/agent-detect.js";
8
+
9
+ export async function runChecks({ config }) {
10
+ const checks = [];
11
+
12
+ // 1. Config file
13
+ const configPath = getConfigPath();
14
+ const configExists = await exists(configPath);
15
+ checks.push({
16
+ name: "config",
17
+ label: "Config file",
18
+ ok: configExists,
19
+ detail: configExists ? configPath : "Not found",
20
+ fix: configExists ? null : "Run 'kj init' to create the config file."
21
+ });
22
+
23
+ // 2. Git repository
24
+ let gitOk = false;
25
+ try {
26
+ gitOk = await ensureGitRepo();
27
+ } catch {
28
+ gitOk = false;
29
+ }
30
+ checks.push({
31
+ name: "git",
32
+ label: "Git repository",
33
+ ok: gitOk,
34
+ detail: gitOk ? "Inside a git repository" : "Not a git repository",
35
+ fix: gitOk ? null : "Run 'git init' or navigate to a git-managed project."
36
+ });
37
+
38
+ // 3. Docker
39
+ const docker = await checkBinary("docker", "--version");
40
+ checks.push({
41
+ name: "docker",
42
+ label: "Docker",
43
+ ok: docker.ok,
44
+ detail: docker.ok ? docker.version : "Not found",
45
+ fix: docker.ok ? null : "Install Docker: https://docs.docker.com/get-docker/"
46
+ });
47
+
48
+ // 4. SonarQube reachability
49
+ const sonarHost = config.sonarqube?.host || "http://localhost:9000";
50
+ let sonarOk = false;
51
+ if (config.sonarqube?.enabled !== false) {
52
+ try {
53
+ sonarOk = await isSonarReachable(sonarHost);
54
+ } catch {
55
+ sonarOk = false;
56
+ }
57
+ }
58
+ checks.push({
59
+ name: "sonarqube",
60
+ label: "SonarQube",
61
+ ok: sonarOk || config.sonarqube?.enabled === false,
62
+ detail: config.sonarqube?.enabled === false
63
+ ? "Disabled in config"
64
+ : sonarOk
65
+ ? `Reachable at ${sonarHost}`
66
+ : `Not reachable at ${sonarHost}`,
67
+ fix: sonarOk || config.sonarqube?.enabled === false
68
+ ? null
69
+ : "Run 'kj sonar start' or 'docker start karajan-sonarqube'. Use --no-sonar to skip."
70
+ });
71
+
72
+ // 5. Agent CLIs
73
+ for (const agent of KNOWN_AGENTS) {
74
+ const result = await checkBinary(agent.name);
75
+ checks.push({
76
+ name: `agent:${agent.name}`,
77
+ label: `Agent: ${agent.name}`,
78
+ ok: result.ok,
79
+ detail: result.ok ? `${result.version} (${result.path})` : "Not found",
80
+ fix: result.ok ? null : `Install: ${agent.install}`
81
+ });
82
+ }
83
+
84
+ // 6. Core binaries
85
+ for (const bin of ["node", "npm", "git"]) {
86
+ const result = await checkBinary(bin);
87
+ checks.push({
88
+ name: bin,
89
+ label: bin,
90
+ ok: result.ok,
91
+ detail: result.ok ? result.version : "Not found",
92
+ fix: result.ok ? null : `Install ${bin} from its official website.`
93
+ });
94
+ }
95
+
96
+ // 7. Serena MCP
97
+ if (config.serena?.enabled) {
98
+ let serenaOk = false;
99
+ try {
100
+ const serenaCheck = await runCommand("serena", ["--version"]);
101
+ serenaOk = serenaCheck.exitCode === 0;
102
+ } catch {
103
+ serenaOk = false;
104
+ }
105
+ checks.push({
106
+ name: "serena",
107
+ label: "Serena MCP",
108
+ ok: serenaOk,
109
+ detail: serenaOk ? "Available" : "Not found (prompts will still include Serena instructions)",
110
+ fix: serenaOk ? null : "Install Serena: uvx --from git+https://github.com/oraios/serena serena --help"
111
+ });
112
+ }
113
+
114
+ // 8. Review rules / Coder rules
115
+ const projectDir = config.projectDir || process.cwd();
116
+ const reviewRules = await loadFirstExisting(resolveRoleMdPath("reviewer", projectDir));
117
+ const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
118
+ checks.push({
119
+ name: "review-rules",
120
+ label: "Reviewer rules (.md)",
121
+ ok: Boolean(reviewRules),
122
+ detail: reviewRules ? "Found" : "Not found (will use defaults)",
123
+ fix: null
124
+ });
125
+ checks.push({
126
+ name: "coder-rules",
127
+ label: "Coder rules (.md)",
128
+ ok: Boolean(coderRules),
129
+ detail: coderRules ? "Found" : "Not found (will use defaults)",
130
+ fix: null
131
+ });
132
+
133
+ return checks;
134
+ }
135
+
136
+ export async function doctorCommand({ config }) {
137
+ const checks = await runChecks({ config });
138
+
139
+ for (const check of checks) {
140
+ const icon = check.ok ? "OK " : "MISS";
141
+ console.log(`${icon} ${check.label}: ${check.detail}`);
142
+ if (check.fix) {
143
+ console.log(` -> ${check.fix}`);
144
+ }
145
+ }
146
+
147
+ const failures = checks.filter((c) => !c.ok && c.fix);
148
+ if (failures.length === 0) {
149
+ console.log("\nAll checks passed.");
150
+ } else {
151
+ console.log(`\n${failures.length} issue(s) found.`);
152
+ }
153
+
154
+ return checks;
155
+ }
@@ -0,0 +1,181 @@
1
+ import fs from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { getConfigPath, loadConfig, writeConfig } from "../config.js";
5
+ import { sonarUp, checkVmMaxMapCount } from "../sonar/manager.js";
6
+ import { exists, ensureDir } from "../utils/fs.js";
7
+ import { getKarajanHome } from "../utils/paths.js";
8
+ import { detectAvailableAgents } from "../utils/agent-detect.js";
9
+ import { createWizard, isTTY } from "../utils/wizard.js";
10
+
11
+ async function runWizard(config, logger) {
12
+ const agents = await detectAvailableAgents();
13
+ const available = agents.filter((a) => a.available);
14
+ const unavailable = agents.filter((a) => !a.available);
15
+
16
+ logger.info("\n=== Karajan Code Setup Wizard ===\n");
17
+
18
+ if (available.length === 0) {
19
+ logger.warn("No AI agents detected. Install at least one:");
20
+ for (const agent of unavailable) {
21
+ logger.warn(` ${agent.name}: ${agent.install}`);
22
+ }
23
+ logger.info("\nGenerating config with defaults (claude). You can change it later in kj.config.yml.\n");
24
+ return config;
25
+ }
26
+
27
+ logger.info("Detected agents:");
28
+ for (const agent of available) {
29
+ logger.info(` OK ${agent.name} (${agent.version})`);
30
+ }
31
+ for (const agent of unavailable) {
32
+ logger.info(` MISS ${agent.name}`);
33
+ }
34
+ logger.info("");
35
+
36
+ if (available.length === 1) {
37
+ const only = available[0].name;
38
+ logger.info(`Only one agent available: ${only}. Using it for all roles.\n`);
39
+ config.coder = only;
40
+ config.reviewer = only;
41
+ config.roles.coder.provider = only;
42
+ config.roles.reviewer.provider = only;
43
+ return config;
44
+ }
45
+
46
+ const wizard = createWizard();
47
+ try {
48
+ const agentOptions = available.map((a) => ({
49
+ label: `${a.name} (${a.version})`,
50
+ value: a.name,
51
+ available: true
52
+ }));
53
+
54
+ const coder = await wizard.select("Select default CODER agent:", agentOptions);
55
+ config.coder = coder;
56
+ config.roles.coder.provider = coder;
57
+ logger.info(` -> Coder: ${coder}`);
58
+
59
+ const reviewer = await wizard.select("Select default REVIEWER agent:", agentOptions);
60
+ config.reviewer = reviewer;
61
+ config.roles.reviewer.provider = reviewer;
62
+ logger.info(` -> Reviewer: ${reviewer}`);
63
+
64
+ const enableTriage = await wizard.confirm("Enable triage (auto-classify task complexity)?", false);
65
+ config.pipeline.triage = config.pipeline.triage || {};
66
+ config.pipeline.triage.enabled = enableTriage;
67
+ logger.info(` -> Triage: ${enableTriage ? "enabled" : "disabled"}`);
68
+
69
+ const enableSonar = await wizard.confirm("Enable SonarQube analysis?", true);
70
+ config.sonarqube.enabled = enableSonar;
71
+ logger.info(` -> SonarQube: ${enableSonar ? "enabled" : "disabled"}`);
72
+
73
+ const methodology = await wizard.select("Development methodology:", [
74
+ { label: "TDD (test-driven development)", value: "tdd", available: true },
75
+ { label: "Standard (no TDD enforcement)", value: "standard", available: true }
76
+ ]);
77
+ config.development.methodology = methodology;
78
+ config.development.require_test_changes = methodology === "tdd";
79
+ logger.info(` -> Methodology: ${methodology}`);
80
+
81
+ logger.info("");
82
+ } finally {
83
+ wizard.close();
84
+ }
85
+
86
+ return config;
87
+ }
88
+
89
+ export async function initCommand({ logger, flags = {} }) {
90
+ const karajanHome = getKarajanHome();
91
+ await ensureDir(karajanHome);
92
+ logger.info(`Ensured ${karajanHome} exists`);
93
+
94
+ const configPath = getConfigPath();
95
+ const reviewRulesPath = path.resolve(process.cwd(), "review-rules.md");
96
+ const coderRulesPath = path.resolve(process.cwd(), "coder-rules.md");
97
+
98
+ const { config, exists: configExists } = await loadConfig();
99
+ const interactive = flags.noInteractive !== true && isTTY();
100
+
101
+ if (configExists && interactive) {
102
+ const wizard = createWizard();
103
+ try {
104
+ const reconfigure = await wizard.confirm("Config already exists. Reconfigure?", false);
105
+ if (reconfigure) {
106
+ const updated = await runWizard(config, logger);
107
+ await writeConfig(configPath, updated);
108
+ logger.info(`Updated ${configPath}`);
109
+ } else {
110
+ logger.info("Keeping existing config.");
111
+ }
112
+ } finally {
113
+ wizard.close();
114
+ }
115
+ } else if (!configExists && interactive) {
116
+ const updated = await runWizard(config, logger);
117
+ await writeConfig(configPath, updated);
118
+ logger.info(`Created ${configPath}`);
119
+ } else if (!configExists) {
120
+ await writeConfig(configPath, config);
121
+ logger.info(`Created ${configPath}`);
122
+ }
123
+
124
+ if (!(await exists(reviewRulesPath))) {
125
+ await fs.writeFile(
126
+ reviewRulesPath,
127
+ "# Review Rules\n\n- Focus on security, correctness, and test coverage.\n",
128
+ "utf8"
129
+ );
130
+ logger.info("Created review-rules.md");
131
+ }
132
+
133
+ if (!(await exists(coderRulesPath))) {
134
+ const templatePath = path.resolve(import.meta.dirname, "../../templates/coder-rules.md");
135
+ let content;
136
+ try {
137
+ content = await fs.readFile(templatePath, "utf8");
138
+ } catch {
139
+ content = [
140
+ "# Coder Rules",
141
+ "",
142
+ "## File modification safety",
143
+ "",
144
+ "- NEVER overwrite existing files entirely. Always make targeted, minimal edits.",
145
+ "- After each edit, verify with `git diff` that ONLY the intended lines changed.",
146
+ "- Do not modify code unrelated to the task.",
147
+ ""
148
+ ].join("\n");
149
+ }
150
+ await fs.writeFile(coderRulesPath, content, "utf8");
151
+ logger.info("Created coder-rules.md");
152
+ }
153
+
154
+ if (config.sonarqube?.enabled !== false) {
155
+ const vmCheck = await checkVmMaxMapCount(os.platform());
156
+ if (!vmCheck.ok) {
157
+ logger.warn(`vm.max_map_count check failed: ${vmCheck.reason}`);
158
+ if (vmCheck.fix) {
159
+ logger.warn(`Fix: ${vmCheck.fix}`);
160
+ }
161
+ }
162
+
163
+ const sonar = await sonarUp();
164
+ if (sonar.exitCode !== 0) {
165
+ throw new Error(`Failed to start SonarQube: ${sonar.stderr || sonar.stdout}`);
166
+ }
167
+
168
+ logger.info("SonarQube container started");
169
+
170
+ logger.info("");
171
+ logger.info("To configure the SonarQube token:");
172
+ logger.info(" 1. Open http://localhost:9000");
173
+ logger.info(" 2. Log in (default credentials: admin / admin)");
174
+ logger.info(" 3. Go to: My Account > Security > Generate Token");
175
+ logger.info(" 4. Name: karajan-cli, Type: Global Analysis Token");
176
+ logger.info(" 5. Set the token in ~/.karajan/kj.config.yml under sonarqube.token");
177
+ logger.info(' or export KJ_SONAR_TOKEN="<your-token>"');
178
+ } else {
179
+ logger.info("SonarQube disabled — skipping container setup.");
180
+ }
181
+ }