karajan-code 2.5.0 → 2.6.1

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 (111) hide show
  1. package/README.md +39 -4
  2. package/package.json +5 -2
  3. package/src/agents/aider-agent.js +1 -2
  4. package/src/agents/base-agent.js +63 -1
  5. package/src/agents/claude-agent.js +3 -4
  6. package/src/agents/codex-agent.js +1 -2
  7. package/src/agents/gemini-agent.js +1 -2
  8. package/src/agents/index.js +11 -2
  9. package/src/agents/opencode-agent.js +1 -2
  10. package/src/brain/decision-tracker.js +123 -0
  11. package/src/brain/decisor.js +243 -0
  12. package/src/brain/solomon-consult.js +171 -0
  13. package/src/budget/comparison.js +96 -0
  14. package/src/checks/binaries.js +123 -0
  15. package/src/checks/ci.js +105 -0
  16. package/src/checks/config-files.js +217 -0
  17. package/src/checks/dir-setup.js +63 -0
  18. package/src/checks/mcp-health.js +157 -0
  19. package/src/checks/node.js +61 -0
  20. package/src/checks/ports.js +110 -0
  21. package/src/checks/rtk.js +36 -0
  22. package/src/checks/runner.js +267 -0
  23. package/src/checks/skills.js +49 -0
  24. package/src/checks/sonar.js +67 -0
  25. package/src/checks/system.js +53 -0
  26. package/src/checks/tokens.js +96 -0
  27. package/src/checks/types.js +105 -0
  28. package/src/cli.js +29 -3
  29. package/src/commands/board.js +23 -0
  30. package/src/commands/doctor.js +130 -362
  31. package/src/commands/resume.js +1 -1
  32. package/src/commands/run.js +2 -1
  33. package/src/commands/skills.js +49 -0
  34. package/src/config/schema.js +222 -0
  35. package/src/config.js +32 -3
  36. package/src/guards/policy-resolver.js +8 -0
  37. package/src/hu/store.js +9 -0
  38. package/src/infrastructure/command-runner.js +44 -0
  39. package/src/infrastructure/environment.js +38 -0
  40. package/src/infrastructure/file-system-service.js +63 -0
  41. package/src/infrastructure/mocks.js +182 -0
  42. package/src/mcp/handlers/management-handlers.js +7 -2
  43. package/src/mcp/tools.js +10 -3
  44. package/src/orchestrator/config-init.js +16 -1
  45. package/src/orchestrator/flow-runner.js +2085 -0
  46. package/src/orchestrator/pipeline-context.js +17 -0
  47. package/src/orchestrator/preflight-checks.js +78 -4
  48. package/src/orchestrator/solomon-escalation.js +55 -0
  49. package/src/orchestrator/stages/stage-executor.js +155 -0
  50. package/src/orchestrator.js +19 -1923
  51. package/src/prompts/architect.js +2 -2
  52. package/src/prompts/coder.js +2 -2
  53. package/src/prompts/prompt-resolver.js +127 -0
  54. package/src/prompts/reviewer.js +2 -2
  55. package/src/roles/agent-role.js +9 -0
  56. package/src/roles/architect-role.js +3 -1
  57. package/src/roles/base-role.js +40 -2
  58. package/src/roles/coder-role.js +3 -1
  59. package/src/roles/security-role.js +12 -0
  60. package/src/roles/tester-role.js +11 -0
  61. package/src/session/journal/decisions-writer.js +200 -0
  62. package/src/session/journal/iteration-logger.js +194 -0
  63. package/src/session/journal/summary-writer.js +176 -0
  64. package/src/session/journal/tree-writer.js +164 -0
  65. package/src/skills/semantic-detector.js +147 -0
  66. package/src/skills/skill-detector.js +186 -8
  67. package/src/skills/skill-loader.js +131 -1
  68. package/src/skills/skills-cache.js +128 -0
  69. package/src/types/agent.js +55 -0
  70. package/src/types/config.js +114 -0
  71. package/src/types/hu.js +47 -0
  72. package/src/types/index.js +38 -0
  73. package/src/types/policy.js +43 -0
  74. package/src/types/session.js +74 -0
  75. package/src/types/solomon.js +87 -0
  76. package/src/types/stage.js +68 -0
  77. package/src/utils/display/event-handlers.js +389 -0
  78. package/src/utils/display/formatters.js +132 -0
  79. package/src/utils/display/header.js +48 -0
  80. package/src/utils/display/session-summary.js +108 -0
  81. package/src/utils/display/solomon.js +42 -0
  82. package/src/utils/port-check.js +45 -0
  83. package/src/utils/port-occupant.js +95 -0
  84. package/src/utils/provider-env.js +116 -0
  85. package/src/utils/with-timeout.js +42 -0
  86. package/templates/roles/architect/anthropic.md +65 -0
  87. package/templates/roles/architect/default.md +63 -0
  88. package/templates/roles/architect/google.md +52 -0
  89. package/templates/roles/architect/openai.md +51 -0
  90. package/templates/roles/architect/opencode.md +50 -0
  91. package/templates/roles/coder/anthropic.md +72 -0
  92. package/templates/roles/coder/default.md +69 -0
  93. package/templates/roles/coder/google.md +78 -0
  94. package/templates/roles/coder/openai.md +59 -0
  95. package/templates/roles/coder/opencode.md +49 -0
  96. package/templates/roles/planner/anthropic.md +50 -0
  97. package/templates/roles/planner/default.md +48 -0
  98. package/templates/roles/planner/google.md +66 -0
  99. package/templates/roles/planner/openai.md +46 -0
  100. package/templates/roles/planner/opencode.md +53 -0
  101. package/templates/roles/reviewer/anthropic.md +61 -0
  102. package/templates/roles/reviewer/default.md +62 -0
  103. package/templates/roles/reviewer/google.md +63 -0
  104. package/templates/roles/reviewer/openai.md +57 -0
  105. package/templates/roles/reviewer/opencode.md +42 -0
  106. package/templates/roles/solomon/anthropic.md +71 -0
  107. package/templates/roles/solomon/default.md +83 -0
  108. package/templates/roles/solomon/google.md +61 -0
  109. package/templates/roles/solomon/openai.md +53 -0
  110. package/templates/roles/solomon/opencode.md +55 -0
  111. package/src/utils/display.js +0 -669
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Port availability checks for services Karajan controls.
3
+ *
4
+ * - SonarQube port (default 9000): the Docker container binds a fixed port,
5
+ * so if it's occupied by a non-Karajan process we cannot "move" it; we
6
+ * report FAIL with the occupant PID/command to help the user stop the
7
+ * conflicting process. Strategy: manual.
8
+ *
9
+ * - HU Board port (default 4000): an in-process Node server we can rebind
10
+ * to any free port in runtime overrides. Strategy: auto.
11
+ */
12
+
13
+ import { isPortAvailable, findAvailablePort } from "../utils/port-check.js";
14
+ import { getPortOccupant } from "../utils/port-occupant.js";
15
+ import { STRATEGY } from "./types.js";
16
+
17
+ const DEFAULT_SONAR_PORT = 9000;
18
+ const DEFAULT_HU_BOARD_PORT = 4000;
19
+
20
+ function extractPortFromHost(host, fallback) {
21
+ const m = String(host || "").match(/:(\d+)(?:\/|$)/);
22
+ return m ? Number(m[1]) : fallback;
23
+ }
24
+
25
+ /**
26
+ * Whether the occupant looks like the Karajan-managed Sonar container.
27
+ * Docker Desktop's proxy is typically `com.docker.backend` / `com.docker.vmnetd`
28
+ * on macOS and `docker-proxy` on Linux. We treat those as OK.
29
+ */
30
+ function isKarajanSonarOccupant(occupant) {
31
+ if (!occupant?.command) return false;
32
+ const cmd = occupant.command.toLowerCase();
33
+ return cmd.includes("docker") || cmd.includes("sonar");
34
+ }
35
+
36
+ export function createSonarPortCheck() {
37
+ return {
38
+ name: "port:sonar",
39
+ label: "SonarQube port",
40
+ strategy: STRATEGY.MANUAL,
41
+ applies: (config) => config?.sonarqube?.enabled !== false,
42
+ async detect({ config }) {
43
+ const port = extractPortFromHost(config?.sonarqube?.host, DEFAULT_SONAR_PORT);
44
+ const free = await isPortAvailable(port);
45
+ if (free) {
46
+ return { ok: true, severity: "info", detail: `Port ${port} is free (Sonar container not yet running)` };
47
+ }
48
+ // Busy — is it our container?
49
+ const occupant = await getPortOccupant(port);
50
+ if (occupant && isKarajanSonarOccupant(occupant)) {
51
+ return {
52
+ ok: true,
53
+ severity: "info",
54
+ detail: `Port ${port} held by Karajan-managed Sonar (${occupant.command} PID ${occupant.pid})`,
55
+ };
56
+ }
57
+ const who = occupant
58
+ ? `${occupant.command || "unknown"} (PID ${occupant.pid ?? "?"})`
59
+ : "unknown process";
60
+ return {
61
+ ok: false,
62
+ severity: "fail",
63
+ detail: `Port ${port} occupied by ${who}`,
64
+ fix: `Stop the process holding port ${port} or change sonarqube.host in kj.config.yml.`,
65
+ extra: { port, occupant },
66
+ };
67
+ },
68
+ };
69
+ }
70
+
71
+ export function createHuBoardPortCheck() {
72
+ return {
73
+ name: "port:hu-board",
74
+ label: "HU Board port",
75
+ strategy: STRATEGY.AUTO,
76
+ applies: (config) => config?.hu_board?.enabled !== false,
77
+ describe: "Rebind HU Board to the next free port",
78
+ async detect({ config }) {
79
+ const port = config?.hu_board?.port || DEFAULT_HU_BOARD_PORT;
80
+ const free = await isPortAvailable(port);
81
+ if (free) {
82
+ return { ok: true, severity: "info", detail: `Port ${port} is free` };
83
+ }
84
+ const occupant = await getPortOccupant(port);
85
+ const who = occupant
86
+ ? `${occupant.command || "unknown"} (PID ${occupant.pid ?? "?"})`
87
+ : "unknown process";
88
+ return {
89
+ ok: false,
90
+ severity: "warn",
91
+ detail: `Port ${port} occupied by ${who}`,
92
+ fix: `Free port ${port} or set hu_board.port in kj.config.yml`,
93
+ extra: { port, occupant },
94
+ };
95
+ },
96
+ async remediate({ extra }) {
97
+ const start = (extra?.port ?? DEFAULT_HU_BOARD_PORT) + 1;
98
+ const found = await findAvailablePort(start, 20);
99
+ return {
100
+ fixed: true,
101
+ detail: `Rebound HU Board to port ${found} (in-memory override)`,
102
+ changes: { hu_board: { port: found } },
103
+ };
104
+ },
105
+ };
106
+ }
107
+
108
+ export function getPortChecks() {
109
+ return [createSonarPortCheck(), createHuBoardPortCheck()];
110
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * RTK (Rust Token Killer) availability check. Manual strategy — installation
3
+ * depends on the host platform and is handled by getInstallCommand().
4
+ */
5
+
6
+ import { runCommand } from "../utils/process.js";
7
+ import { getInstallCommand } from "../utils/os-detect.js";
8
+ import { withDocLink } from "../utils/doc-links.js";
9
+ import { STRATEGY } from "./types.js";
10
+
11
+ export function createRtkCheck() {
12
+ return {
13
+ name: "rtk",
14
+ label: "RTK (Rust Token Killer)",
15
+ strategy: STRATEGY.MANUAL,
16
+ async detect() {
17
+ try {
18
+ const res = await runCommand("rtk", ["--version"]);
19
+ if (res.exitCode === 0) {
20
+ return { ok: true, severity: "info", detail: `${res.stdout.trim()} — token savings active` };
21
+ }
22
+ } catch { /* not installed */ }
23
+ const installCmd = getInstallCommand("rtk");
24
+ return {
25
+ ok: false,
26
+ severity: "warn",
27
+ detail: "Not found — 60-90% token savings available",
28
+ fix: withDocLink(`Install: ${installCmd}`, "rtk_install"),
29
+ };
30
+ },
31
+ };
32
+ }
33
+
34
+ export function getRtkChecks() {
35
+ return [createRtkCheck()];
36
+ }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * Check runner. Orchestrates the detect → remediate → re-verify phases for
3
+ * a list of Check objects.
4
+ *
5
+ * Phase 1 (detect): executed in parallel with per-check timeout. Timeouts
6
+ * are reported as TIMEOUT status (WARN-level), never throw.
7
+ *
8
+ * Phase 2 (remediate): sequential, only for failures with strategy auto or
9
+ * prompt (and user consent for prompt). Skipped entirely when mode is
10
+ * "check-only". Each remediation has its own timeout (5s default, 60s for
11
+ * prompted actions like npm install).
12
+ *
13
+ * Phase 3 (re-verify): re-runs `detect` once for each check that was
14
+ * remediated, to confirm the fix actually worked. If re-verify fails, the
15
+ * status is FAIL (remediation lied).
16
+ *
17
+ * Runtime overrides returned by remediations are merged into `runReport.overrides`
18
+ * for consumers (e.g. preflight → orchestrator) to apply to the active session.
19
+ */
20
+
21
+ import { STATUS, STRATEGY } from "./types.js";
22
+ import { withTimeout, TimeoutError } from "../utils/with-timeout.js";
23
+
24
+ const DEFAULT_DETECT_TIMEOUT_MS = 5000;
25
+ const DEFAULT_REMEDIATE_TIMEOUT_MS = 60000;
26
+
27
+ /**
28
+ * @param {import("./types.js").Check[]} checks
29
+ * @param {{ config: Object }} ctx
30
+ * @param {{
31
+ * mode?: "fix"|"check-only",
32
+ * yes?: boolean,
33
+ * onConfirm?: (describe: string) => Promise<boolean>,
34
+ * timeoutMs?: number,
35
+ * remediateTimeoutMs?: number,
36
+ * signal?: AbortSignal,
37
+ * logger?: { info: Function, warn: Function, error: Function }
38
+ * }} [options]
39
+ * @returns {Promise<import("./types.js").RunReport>}
40
+ */
41
+ export async function runChecks(checks, ctx, options = {}) {
42
+ const {
43
+ mode = "fix",
44
+ yes = false,
45
+ onConfirm = defaultOnConfirm,
46
+ timeoutMs = DEFAULT_DETECT_TIMEOUT_MS,
47
+ remediateTimeoutMs = DEFAULT_REMEDIATE_TIMEOUT_MS,
48
+ signal,
49
+ logger = silentLogger(),
50
+ } = options;
51
+
52
+ const { config } = ctx;
53
+ const startTotal = Date.now();
54
+ const entries = [];
55
+ const overrides = {};
56
+
57
+ // Build applicable entries (skipped checks noted up front).
58
+ for (const check of checks) {
59
+ if (typeof check.applies === "function" && !check.applies(config)) {
60
+ entries.push({
61
+ check,
62
+ status: STATUS.SKIPPED,
63
+ detail: "Not applicable for current configuration",
64
+ runMs: 0,
65
+ detectResult: null,
66
+ remediated: false,
67
+ });
68
+ } else {
69
+ entries.push({ check, status: null, detail: null, runMs: 0, detectResult: null, remediated: false });
70
+ }
71
+ }
72
+
73
+ // Phase 1: detect in parallel with timeout.
74
+ const detectBase = Date.now();
75
+ await Promise.all(
76
+ entries.map(async (entry) => {
77
+ if (entry.status === STATUS.SKIPPED) return;
78
+ const start = Date.now();
79
+ try {
80
+ const result = await withTimeout(entry.check.detect({ config, signal }), timeoutMs, entry.check.name);
81
+ entry.detectResult = result;
82
+ entry.runMs = Date.now() - start;
83
+ entry.status = resolveStatusFromDetect(entry.check, result);
84
+ entry.detail = result.detail;
85
+ } catch (err) {
86
+ entry.runMs = Date.now() - start;
87
+ if (err instanceof TimeoutError) {
88
+ entry.detectResult = { ok: false, severity: "warn", detail: `Timeout after ${timeoutMs}ms` };
89
+ entry.status = STATUS.TIMEOUT;
90
+ entry.detail = entry.detectResult.detail;
91
+ } else {
92
+ entry.detectResult = { ok: false, severity: "fail", detail: `Detection error: ${err.message}` };
93
+ entry.status = STATUS.FAIL;
94
+ entry.detail = entry.detectResult.detail;
95
+ }
96
+ }
97
+ })
98
+ );
99
+ logger.info?.(`Detect phase done in ${Date.now() - detectBase}ms`);
100
+
101
+ // Phase 2: remediate sequentially (only in "fix" mode, only failing checks).
102
+ if (mode !== "check-only") {
103
+ for (const entry of entries) {
104
+ if (!shouldAttemptRemediation(entry)) continue;
105
+ const { check, detectResult } = entry;
106
+ if (check.strategy === STRATEGY.PROMPT && !yes) {
107
+ const approved = await onConfirm(check.describe || `Fix: ${check.name}`);
108
+ if (!approved) {
109
+ logger.info?.(`User declined remediation for ${check.name}`);
110
+ continue;
111
+ }
112
+ }
113
+ const start = Date.now();
114
+ try {
115
+ const rem = await withTimeout(
116
+ check.remediate({ config, extra: detectResult.extra, signal, logger }),
117
+ remediateTimeoutMs,
118
+ `${check.name}:remediate`
119
+ );
120
+ entry.remediated = rem.fixed === true;
121
+ entry.runMs += Date.now() - start;
122
+ if (rem.fixed) {
123
+ if (rem.changes) mergeOverrides(overrides, rem.changes);
124
+ entry.detail = rem.detail;
125
+ entry.status = STATUS.FIXED;
126
+ } else {
127
+ entry.detail = rem.detail || entry.detail;
128
+ entry.status = STATUS.FAIL;
129
+ }
130
+ } catch (err) {
131
+ entry.runMs += Date.now() - start;
132
+ const msg = err instanceof TimeoutError ? `Remediation timeout after ${remediateTimeoutMs}ms` : `Remediation error: ${err.message}`;
133
+ entry.detail = msg;
134
+ entry.status = STATUS.FAIL;
135
+ logger.warn?.(`Remediation failed for ${check.name}: ${msg}`);
136
+ }
137
+ }
138
+
139
+ // Phase 3: re-verify fixed checks using the config merged with any
140
+ // runtime overrides applied by remediations. Without this merge, a check
141
+ // that rebinds to a new port (override) would re-verify against the old
142
+ // config and wrongly report failure.
143
+ const verifyConfig = mergeConfigAndOverrides(config, overrides);
144
+ await Promise.all(
145
+ entries.filter((e) => e.remediated).map(async (entry) => {
146
+ const start = Date.now();
147
+ try {
148
+ const result = await withTimeout(entry.check.detect({ config: verifyConfig, signal }), timeoutMs, `${entry.check.name}:reverify`);
149
+ entry.runMs += Date.now() - start;
150
+ if (!result.ok) {
151
+ entry.status = STATUS.FAIL;
152
+ entry.detail = `${entry.detail} (but re-verify failed: ${result.detail})`;
153
+ }
154
+ } catch (err) {
155
+ entry.runMs += Date.now() - start;
156
+ entry.status = STATUS.FAIL;
157
+ entry.detail = `${entry.detail} (re-verify error: ${err.message})`;
158
+ }
159
+ })
160
+ );
161
+ }
162
+
163
+ // Flatten into reports.
164
+ const reports = entries.map((e) => ({
165
+ name: e.check.name,
166
+ label: e.check.label,
167
+ status: e.status,
168
+ strategy: e.check.strategy,
169
+ detail: e.detail ?? "",
170
+ fix: e.detectResult?.fix,
171
+ runMs: e.runMs,
172
+ }));
173
+
174
+ const summary = summarize(reports);
175
+
176
+ return {
177
+ checks: reports,
178
+ overrides,
179
+ totalMs: Date.now() - startTotal,
180
+ summary,
181
+ };
182
+ }
183
+
184
+ function shouldAttemptRemediation(entry) {
185
+ if (entry.status === STATUS.SKIPPED) return false;
186
+ if (entry.status === STATUS.OK) return false;
187
+ if (entry.status === STATUS.TIMEOUT) return false; // don't remediate when detect itself timed out
188
+ const { check, detectResult } = entry;
189
+ if (!check.remediate || !detectResult) return false;
190
+ if (check.strategy === STRATEGY.MANUAL || check.strategy === STRATEGY.NONE) return false;
191
+ // Soft failures (WARN) with PROMPT strategy: don't nag the user for optional issues.
192
+ if (entry.status === STATUS.WARN && check.strategy === STRATEGY.PROMPT) return false;
193
+ return true;
194
+ }
195
+
196
+ function resolveStatusFromDetect(check, result) {
197
+ if (result.ok) return STATUS.OK;
198
+ const severity = result.severity || "fail";
199
+ if (severity === "warn") return STATUS.WARN;
200
+ if (severity === "info") return STATUS.OK; // info is always non-failing
201
+ if (check.strategy === STRATEGY.NONE) return STATUS.WARN;
202
+ return STATUS.FAIL;
203
+ }
204
+
205
+ function summarize(reports) {
206
+ const summary = { ok: 0, fixed: 0, warn: 0, fail: 0, skipped: 0, timeout: 0 };
207
+ for (const r of reports) {
208
+ const k = r.status.toLowerCase();
209
+ if (k in summary) summary[k]++;
210
+ }
211
+ return summary;
212
+ }
213
+
214
+ /**
215
+ * Deep-merge (shallow for now, one level deep) remediation changes into overrides.
216
+ * Remediation outputs like { hu_board: { port: 4007 } } should not overwrite a
217
+ * sibling key { hu_board: { enabled: true } } that a prior check set.
218
+ */
219
+ function mergeOverrides(target, source) {
220
+ for (const [key, value] of Object.entries(source)) {
221
+ if (value && typeof value === "object" && !Array.isArray(value) && typeof target[key] === "object") {
222
+ Object.assign(target[key], value);
223
+ } else {
224
+ target[key] = value;
225
+ }
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Produce a shallow-merged copy of config + overrides used during re-verify.
231
+ * Does not mutate the original config.
232
+ */
233
+ function mergeConfigAndOverrides(config, overrides) {
234
+ if (!overrides || Object.keys(overrides).length === 0) return config;
235
+ const merged = { ...config };
236
+ for (const [key, value] of Object.entries(overrides)) {
237
+ if (value && typeof value === "object" && !Array.isArray(value) && typeof config[key] === "object") {
238
+ merged[key] = { ...config[key], ...value };
239
+ } else {
240
+ merged[key] = value;
241
+ }
242
+ }
243
+ return merged;
244
+ }
245
+
246
+ function defaultOnConfirm() {
247
+ // Non-interactive by default: refuse prompts unless the caller supplies a real handler.
248
+ return Promise.resolve(false);
249
+ }
250
+
251
+ function silentLogger() {
252
+ return { info() {}, warn() {}, error() {} };
253
+ }
254
+
255
+ /**
256
+ * Flatten a RunReport into the legacy doctor.js shape.
257
+ * OK/FIXED/SKIPPED → ok:true. WARN/FAIL/TIMEOUT → ok:false.
258
+ */
259
+ export function toLegacyShape(runReport) {
260
+ return runReport.checks.map((c) => ({
261
+ name: c.name,
262
+ label: c.label,
263
+ ok: c.status === STATUS.OK || c.status === STATUS.FIXED || c.status === STATUS.SKIPPED,
264
+ detail: c.detail,
265
+ fix: c.fix ?? null,
266
+ }));
267
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * OpenSkills CLI availability check. Only runs when skills are enabled.
3
+ * Strategy: prompt — we can offer to run `npm install -g openskills`,
4
+ * but global installs are invasive enough that they need user consent.
5
+ */
6
+
7
+ import { isOpenSkillsAvailable } from "../skills/openskills-client.js";
8
+ import { runCommand } from "../utils/process.js";
9
+ import { STRATEGY } from "./types.js";
10
+
11
+ export function skillsEnabled(config) {
12
+ return config?.skills?.enabled !== false;
13
+ }
14
+
15
+ export function createOpenSkillsCheck() {
16
+ return {
17
+ name: "openskills",
18
+ label: "OpenSkills CLI",
19
+ strategy: STRATEGY.PROMPT,
20
+ applies: skillsEnabled,
21
+ describe: "Install OpenSkills globally: npm install -g openskills",
22
+ async detect() {
23
+ const available = await isOpenSkillsAvailable();
24
+ if (available) {
25
+ return { ok: true, severity: "info", detail: "Available via `npx openskills`" };
26
+ }
27
+ return {
28
+ ok: false,
29
+ severity: "warn", // skills enabled but missing → degrade gracefully
30
+ detail: "`npx openskills` not available — skill injection will be skipped",
31
+ fix: "Install OpenSkills: npm install -g openskills",
32
+ };
33
+ },
34
+ async remediate() {
35
+ const res = await runCommand("npm", ["install", "-g", "openskills"], { timeout: 60000 });
36
+ if (res.exitCode === 0) {
37
+ return { fixed: true, detail: "Installed openskills globally via npm" };
38
+ }
39
+ return {
40
+ fixed: false,
41
+ detail: `npm install -g openskills failed (exit ${res.exitCode}): ${res.stderr.split("\n")[0]}`,
42
+ };
43
+ },
44
+ };
45
+ }
46
+
47
+ export function getSkillsChecks() {
48
+ return [createOpenSkillsCheck()];
49
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * SonarQube reachability + auth checks.
3
+ *
4
+ * Auto-remediation strategy (preserving existing behavior from
5
+ * preflight-checks.js): if Sonar is not reachable, try `sonarUp()` which
6
+ * starts the Docker container, then poll for readiness. If auth fails but
7
+ * admin credentials exist, generate a fresh token and persist it.
8
+ */
9
+
10
+ import { isSonarReachable } from "../sonar/manager.js";
11
+ import { STRATEGY } from "./types.js";
12
+ import { withDocLink } from "../utils/doc-links.js";
13
+
14
+ /**
15
+ * Whether Sonar is enabled in the active config.
16
+ */
17
+ function sonarEnabled(config) {
18
+ return config.sonarqube?.enabled !== false;
19
+ }
20
+
21
+ function sonarHost(config) {
22
+ return config.sonarqube?.host || "http://localhost:9000";
23
+ }
24
+
25
+ /**
26
+ * Informative check: Sonar enabled/disabled status.
27
+ * Produces an OK result with disabled description when Sonar is off.
28
+ */
29
+ export function createSonarStatusCheck() {
30
+ return {
31
+ name: "sonarqube",
32
+ label: "SonarQube",
33
+ strategy: STRATEGY.NONE,
34
+ async detect({ config }) {
35
+ const host = sonarHost(config);
36
+ if (!sonarEnabled(config)) {
37
+ return { ok: true, severity: "info", detail: "Disabled in config" };
38
+ }
39
+ let reachable = false;
40
+ try {
41
+ reachable = await isSonarReachable(host);
42
+ } catch {
43
+ reachable = false;
44
+ }
45
+ return {
46
+ ok: reachable,
47
+ severity: "fail",
48
+ detail: reachable ? `Reachable at ${host}` : `Not reachable at ${host}`,
49
+ fix: reachable
50
+ ? undefined
51
+ : withDocLink(
52
+ "Run 'kj sonar start' or 'docker start karajan-sonarqube'. Use --no-sonar to skip.",
53
+ "sonar_docker"
54
+ ),
55
+ extra: { host, reachable },
56
+ };
57
+ },
58
+ };
59
+ }
60
+
61
+ /**
62
+ * All Sonar-related checks. Keeps just the status check for commit 1;
63
+ * auth + auto-remediation are layered in commit 3.
64
+ */
65
+ export function getSonarChecks() {
66
+ return [createSonarStatusCheck()];
67
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Miscellaneous system-level checks: Karajan version, git repo presence.
3
+ * Kept in a dedicated module so they're easy to find and extend.
4
+ */
5
+
6
+ import { readFileSync } from "node:fs";
7
+ import { fileURLToPath } from "node:url";
8
+ import path from "node:path";
9
+ import { ensureGitRepo } from "../utils/git.js";
10
+ import { STRATEGY } from "./types.js";
11
+
12
+ function getPackageVersion() {
13
+ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
14
+ return JSON.parse(readFileSync(pkgPath, "utf8")).version;
15
+ }
16
+
17
+ export function createKarajanVersionCheck() {
18
+ return {
19
+ name: "karajan",
20
+ label: "Karajan Code",
21
+ strategy: STRATEGY.NONE,
22
+ async detect() {
23
+ const version = getPackageVersion();
24
+ return { ok: true, severity: "info", detail: `v${version}` };
25
+ },
26
+ };
27
+ }
28
+
29
+ export function createGitRepoCheck() {
30
+ return {
31
+ name: "git",
32
+ label: "Git repository",
33
+ strategy: STRATEGY.MANUAL,
34
+ async detect() {
35
+ let gitOk = false;
36
+ try {
37
+ gitOk = await ensureGitRepo();
38
+ } catch {
39
+ gitOk = false;
40
+ }
41
+ return {
42
+ ok: gitOk,
43
+ severity: "fail",
44
+ detail: gitOk ? "Inside a git repository" : "Not a git repository",
45
+ fix: gitOk ? undefined : "Run 'git init' or navigate to a git-managed project.",
46
+ };
47
+ },
48
+ };
49
+ }
50
+
51
+ export function getSystemChecks() {
52
+ return [createKarajanVersionCheck(), createGitRepoCheck()];
53
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * API token availability checks.
3
+ *
4
+ * Provider tokens (ANTHROPIC_API_KEY / OPENAI_API_KEY / ...) are LAZY:
5
+ * we only check them for providers that are actually used by at least one
6
+ * active role in the current config.
7
+ *
8
+ * Strategy:
9
+ * - Required providers with no key → FAIL, strategy manual (we can't
10
+ * fetch an API key for the user). Fix hint includes the how-to URL.
11
+ * - Optional providers (opencode, aider) without key → WARN.
12
+ * - GH_TOKEN is checked when git.auto_pr=true, strategy prompt (we can
13
+ * launch `gh auth login` with user approval).
14
+ */
15
+
16
+ import { checkBinary } from "../utils/agent-detect.js";
17
+ import { getRequiredProviderEnvs, hasEnvVar } from "../utils/provider-env.js";
18
+ import { STRATEGY } from "./types.js";
19
+
20
+ /**
21
+ * A single provider env check. One is created per active provider.
22
+ */
23
+ function createProviderTokenCheck(entry) {
24
+ return {
25
+ name: `token:${entry.provider}`,
26
+ label: `API token: ${entry.provider}`,
27
+ strategy: STRATEGY.MANUAL,
28
+ async detect() {
29
+ const ok = hasEnvVar(entry);
30
+ if (ok) {
31
+ return { ok: true, severity: "info", detail: `${entry.envVars[0]} is set` };
32
+ }
33
+ return {
34
+ ok: false,
35
+ severity: entry.optional ? "warn" : "fail",
36
+ detail: `${entry.envVars.join(" / ")} not set`,
37
+ fix: `Get an API key at ${entry.howToGetUrl} and export ${entry.envVars[0]}=...`,
38
+ extra: { provider: entry.provider, envVars: entry.envVars, howToGetUrl: entry.howToGetUrl },
39
+ };
40
+ },
41
+ };
42
+ }
43
+
44
+ /**
45
+ * GH token check + auto-remediation via `gh auth login` when it's installed.
46
+ */
47
+ function createGhTokenCheck() {
48
+ return {
49
+ name: "token:gh",
50
+ label: "GitHub token (for auto_pr)",
51
+ strategy: STRATEGY.PROMPT,
52
+ applies: (config) => config?.git?.auto_pr === true,
53
+ describe: "Run 'gh auth login' to authenticate with GitHub",
54
+ async detect() {
55
+ const envOk = !!process.env.GH_TOKEN || !!process.env.GITHUB_TOKEN;
56
+ if (envOk) {
57
+ return { ok: true, severity: "info", detail: "GH_TOKEN or GITHUB_TOKEN is set" };
58
+ }
59
+ // If `gh` is installed and authenticated, it manages its own token.
60
+ const gh = await checkBinary("gh");
61
+ if (!gh.ok) {
62
+ return {
63
+ ok: false,
64
+ severity: "fail",
65
+ detail: "No GH_TOKEN/GITHUB_TOKEN and `gh` CLI not installed",
66
+ fix: "Install GitHub CLI (https://cli.github.com) or export GH_TOKEN=...",
67
+ extra: { hasGh: false },
68
+ };
69
+ }
70
+ return {
71
+ ok: false,
72
+ severity: "fail",
73
+ detail: "No GH_TOKEN/GITHUB_TOKEN; `gh` is installed but auth status unknown",
74
+ fix: "Run 'gh auth login' or export GH_TOKEN=...",
75
+ extra: { hasGh: true },
76
+ };
77
+ },
78
+ // remediate() intentionally omitted: launching `gh auth login` requires an
79
+ // interactive terminal session. The runner's prompt strategy will point
80
+ // the user to the command and exit with FAIL in non-TTY contexts.
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Build the list of token checks for the active config. Each active provider
86
+ * gets one, plus conditional GH token when auto_pr is on.
87
+ *
88
+ * @param {Object} config
89
+ * @returns {import("./types.js").Check[]}
90
+ */
91
+ export function getTokenChecks(config) {
92
+ const required = getRequiredProviderEnvs(config);
93
+ const checks = required.map(createProviderTokenCheck);
94
+ checks.push(createGhTokenCheck());
95
+ return checks;
96
+ }